Beerizer Systembolaget export

Adds an Systembolaget export button to the top of the Beerizer.com cart.

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

  1. // ==UserScript==
  2. // @name Beerizer Systembolaget export
  3. // @namespace https://github.com/Row/beerizer-export-systembolaget
  4. // @version 0.7
  5. // @description Adds an Systembolaget export button to the top of the Beerizer.com cart.
  6. // The export result can be verifed in the Systembolaget.se cart.
  7. // @author Row
  8. // @match https://beerizer.com/*
  9. // @match https://www.systembolaget.se/*
  10. // @grant GM.setValue
  11. // @grant GM.getValue
  12. // @run-at document-body
  13. // ==/UserScript==
  14.  
  15. const STATE_KEY = 'STATE_KEY';
  16. const STATE_UNDEF = null;
  17. const STATE_INIT = 'INIT';
  18. const STATE_PENDING = 'PENDING';
  19. const STATE_DONE = 'DONE';
  20. const STATE_ERROR = 'ERROR';
  21. const STATE_CANCEL = 'CANCEL';
  22. const INITIAL_STATE = {
  23. state: STATE_UNDEF,
  24. index: 0,
  25. beers: [],
  26. };
  27.  
  28. const PROGRESS_ID = 'beerizer-progress';
  29.  
  30. const makeTag = tag => parent => parent.appendChild(document.createElement(tag));
  31.  
  32. const a = makeTag('a');
  33. const button = makeTag('button');
  34. const div = makeTag('div');
  35. const table = makeTag('table');
  36. const td = makeTag('td');
  37. const tr = makeTag('tr');
  38. const aLink = (parent, { href, title }) => {
  39. let el;
  40. if (href) {
  41. el = a(parent);
  42. el.setAttribute('href', href);
  43. } else {
  44. el = parent;
  45. }
  46. el.innerText = title || 'Unknown';
  47. return el;
  48. };
  49. const tdr = parent => {
  50. const t = td(parent);
  51. t.style.padding = '0.3em';
  52. return t;
  53. };
  54.  
  55. const cancelExport = async (state) => {
  56. const { index, beers } = state;
  57. for (let i = index; i < beers.length; i += 1) {
  58. beers[i].state = STATE_CANCEL;
  59. beers[i].error = 'cancelled';
  60. }
  61. doneSystemBolaget({ ...state, index: beers.length - 1 });
  62. };
  63.  
  64. const renderProgress = (state) => {
  65. const overlay = div(document.body);
  66. overlay.id = PROGRESS_ID;
  67. overlay.style.cssText = `
  68. align-items: center;
  69. background: #FFF;
  70. display: flex;
  71. flex-flow: column;
  72. height: 100vh;
  73. justify-content: center;
  74. left: 0;
  75. position: fixed;
  76. top: 0;
  77. transition: height 0.3s;
  78. width: 100vw;
  79. z-index: 1337;
  80. `;
  81. const done = state.beers.filter(({ state }) => state !== STATE_INIT).length;
  82. const total = state.beers.length;
  83. const percent = (done / total) * 100;
  84. const bar = div(overlay);
  85. bar.style.cssText = `
  86. margin: 0 20em;
  87. background: lightgrey;
  88. `;
  89. const progress = div(bar);
  90. progress.style.cssText = `
  91. background: #fbd533;
  92. color: #fff;
  93. overflow: visible;
  94. padding: 1em;
  95. text-align: right;
  96. text-shadow: rgb(95 92 92) 1px 1px 2px;
  97. white-space: nowrap;
  98. width: ${percent}%;
  99. `;
  100. progress.innerText = `EXPORTING BEER ${done} OF ${total}`;
  101. const cancelButton = button(overlay);
  102. cancelButton.innerText = 'Cancel export';
  103. cancelButton.style.cssText = `
  104. background: white;
  105. border: 1px solid red;
  106. color: red;
  107. cursor: pointer;
  108. font-size: 0.7rem;
  109. margin-top: 1rem;
  110. `;
  111. cancelButton.addEventListener('click', () => cancelExport(state));
  112. };
  113.  
  114. const renderResult = async (state) => {
  115. const div = document.createElement('div');
  116. const basket = `
  117. //div[
  118. text()="Varukorgen är tom."
  119. or (starts-with(text(), "Du har ") and contains(text(), "varor i korgen"))
  120. ]`;
  121. await waitForElement(basket);
  122. const insertPoint = await waitForElement('//h1[./span[text()="Varukorg"] or text()="Varukorg"]');
  123. insertPoint.after(div);
  124. div.innerHTML = `<h2>Beerizer exported ${state.beers.length} beers</h2>`;
  125. const exportTable = table(div);
  126. state.beers.map(({
  127. beerizerHref,
  128. beerizerTitle,
  129. error,
  130. state,
  131. systemBolagetHref,
  132. systemBolagetTitle,
  133. }, index) => {
  134. const row = tr(exportTable);
  135. tdr(row).innerText = index + 1;
  136. aLink(tdr(row), {
  137. href: beerizerHref,
  138. title: beerizerTitle,
  139. });
  140. tdr(row).innerText = '➜';
  141. aLink(tdr(row), {
  142. href: systemBolagetHref,
  143. title: systemBolagetTitle,
  144. });
  145. tdr(row).innerText = state === STATE_DONE ? '✅' : '⚠️';
  146. tdr(row).innerText = error ? error : state;
  147. });
  148. };
  149.  
  150. const getElementByXpath = (xpath) =>
  151. document.evaluate(
  152. xpath,
  153. document,
  154. null,
  155. XPathResult.FIRST_ORDERED_NODE_TYPE,
  156. null,
  157. ).singleNodeValue;
  158.  
  159. const waitForElement = (xpath, timeout = 5000, interval = 100, shouldInverse = false) => {
  160. const start = (new Date()).getTime();
  161. return new Promise((resolve, reject) => {
  162. const tryElement = () => {
  163. const element = getElementByXpath(xpath);
  164. if ((!!element) !== shouldInverse) {
  165. resolve(element);
  166. return;
  167. }
  168. if (((new Date()).getTime() - start) > timeout) {
  169. reject(xpath);
  170. }
  171. window.setTimeout(tryElement, interval);
  172. };
  173. tryElement();
  174. });
  175. };
  176.  
  177. const doneSystemBolaget = async (state) => {
  178. GM.setValue(STATE_KEY, { ...state, state: STATE_DONE });
  179. window.location.href = 'https://www.systembolaget.se/varukorg';
  180. };
  181.  
  182. const initSystemBolaget = async (state) => {
  183. if (state.beers.length === 0) {
  184. await doneSystemBolaget(state);
  185. } else {
  186. try {
  187. const CONFIRM_AGE = '//button[text()="Jag har fyllt 20 år"]';
  188. const btn = await waitForElement(CONFIRM_AGE, 2000);
  189. btn.click();
  190. } catch (e) {
  191. console.log('tried to accept age');
  192. }
  193. try {
  194. const CONFIRM_COOKIE = '//button[text()="Slå på och acceptera alla kakor"]';
  195. const btn = await waitForElement(CONFIRM_COOKIE, 2000);
  196. btn.click();
  197. } catch (e) {
  198. console.log('tried to accept cookie');
  199. }
  200. await GM.setValue(STATE_KEY, { ...state, state: STATE_PENDING });
  201. window.location.href = state.beers[0].href;
  202. }
  203. };
  204.  
  205. const addBeerSystembolaget = async (state) => {
  206. const addToCartXpath = '//button[./div[text()="Lägg i varukorg"]]';
  207. const verifyXpath = '//button[./div[text()="Tillagd"]]';
  208. const { index, beers } = state;
  209. const beer = state.beers[index];
  210. beer.systemBolagetHref = window.location.href;
  211. try {
  212. const beerHeader = getElementByXpath('//h1[./span]');
  213. if (!beerHeader) {
  214. throw Error('Beer not found?');
  215. }
  216. beer.systemBolagetTitle = beerHeader.innerText;
  217. const cartBtn = await waitForElement(addToCartXpath);
  218. cartBtn.click();
  219. try {
  220. await waitForElement(verifyXpath, 2000, 100);
  221. } catch (e) {
  222. if (!getElementByXpath('//div[text()="Välj leveranssätt "]')) throw e;
  223. const progress = document.getElementById(PROGRESS_ID);
  224. progress.style.height = '100px';
  225. const closeModalButton = '//button[@id="initialTgmFocus"]';
  226. await waitForElement(closeModalButton, 1000 * 120, 100, true);
  227. const cartBtn = await waitForElement(addToCartXpath);
  228. cartBtn.click();
  229. await waitForElement(verifyXpath, 1000 * 120, 100);
  230. progress.style.height = '100vh';
  231. }
  232. beer.state = STATE_DONE;
  233. } catch (error) {
  234. beer.state = STATE_ERROR;
  235. beer.error = error.message;
  236. }
  237. const nextIndex = index + 1;
  238. if (beers.length <= nextIndex) {
  239. await doneSystemBolaget(state);
  240. } else {
  241. await GM.setValue(STATE_KEY, { ...state, index: nextIndex });
  242. window.location.href = beers[nextIndex].href;
  243. }
  244. };
  245.  
  246. const handleSystembolaget = async () => {
  247. const state = await GM.getValue(STATE_KEY, INITIAL_STATE);
  248. if (state.beers.length > 0 && state.state !== STATE_DONE) {
  249. renderProgress(state);
  250. }
  251. if (state.state === STATE_INIT) {
  252. await initSystemBolaget(state);
  253. } else if (state.state === STATE_PENDING) {
  254. window.addEventListener('load', () => {
  255. addBeerSystembolaget(state);
  256. });
  257. }
  258. if (window.location.pathname === '/varukorg/') {
  259. renderResult(state);
  260. }
  261. };
  262.  
  263. // Beerizer parts
  264. const exportCart = async () => {
  265. const links = 'a[title="To Systembolaget"]';
  266. const titleLinks = '.CartProductTable td.name>a';
  267. const titles = [...document.querySelectorAll(titleLinks)].map(l => ({
  268. beerizerHref: l.href,
  269. beerizerTitle: l.innerText,
  270. }));
  271. const hrefs = [...document.querySelectorAll(links)].map(l => l.href);
  272. const beers = [...new Set(hrefs)].map((href, i) => ({
  273. ...titles[i],
  274. href,
  275. state: STATE_INIT,
  276. }));
  277. const state = {
  278. ...INITIAL_STATE,
  279. state: STATE_INIT,
  280. beers,
  281. };
  282. await GM.setValue(STATE_KEY, state);
  283. const w = window.open('', 'systembolaget');
  284. w.location = 'https://www.systembolaget.se';
  285. };
  286.  
  287. const renderButton = () => {
  288. const cl = getElementByXpath('//a[@class="cart-link" and ./span[text()="Share"]]');
  289. if (!cl) return;
  290. const e = cl.cloneNode(2);
  291. cl.after(e);
  292. e.querySelector('span').innerText = 'Export Systembolaget';
  293. e.addEventListener('click', exportCart);
  294. };
  295.  
  296. const handleBeerizer = () => {
  297. const btn = 'div.cart-wrapper.collapsed>div.summary';
  298. const btnEl = document.querySelector(btn);
  299. btnEl.addEventListener('click', () => {
  300. window.setTimeout(renderButton, 100);
  301. });
  302. };
  303.  
  304. // initialize
  305. (() => {
  306. 'use strict';
  307. const hostname = window.location.hostname;
  308. if (hostname.includes('beerizer')) {
  309. window.addEventListener('load', () => {
  310. handleBeerizer();
  311. });
  312. }
  313. if (hostname.includes('systembolaget')) {
  314. handleSystembolaget();
  315. }
  316. })();