Greasy Fork is available in English.

Swiggy & Zomato: Non Veg dishes only

On Swiggy and Zomato you can select to show vegetarian dishes only, this script does the reverse: it allows you to hide vegetarian dishes. Rate individual dishes and keep a private history of what you like and what you hated

  1. // ==UserScript==
  2. // @name Swiggy & Zomato: Non Veg dishes only
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.2.5
  5. // @description On Swiggy and Zomato you can select to show vegetarian dishes only, this script does the reverse: it allows you to hide vegetarian dishes. Rate individual dishes and keep a private history of what you like and what you hated
  6. // @author cuzi
  7. // @copyright 2021, cuzi (https://openuserjs.org/users/cuzi)
  8. // @license GPL-3.0-or-later
  9. // @match https://www.swiggy.com/*
  10. // @match https://www.zomato.com/*
  11. // @icon https://res.cloudinary.com/swiggy/image/upload/portal/c/icon-192x192.png
  12. // @grant GM.getValue
  13. // @grant GM.setValue
  14. // @grant GM_getResourceText
  15. // @require https://cdn.jsdelivr.net/npm/string-similarity@4.0.4/umd/string-similarity.min.js
  16. // @resource thumbUp https://cdn.jsdelivr.net/npm/openmoji@14.0.0/color/svg/1F44D.svg
  17. // @resource thumbDown https://cdn.jsdelivr.net/npm/openmoji@14.0.0/color/svg/1F44E.svg
  18. // @resource star https://cdn.jsdelivr.net/npm/openmoji@14.0.0/color/svg/2B50.svg
  19. // ==/UserScript==
  20.  
  21. /*
  22. Copyright (C) 2021, cuzi (https://openuserjs.org/users/cuzi)
  23. This program is free software: you can redistribute it and/or modify
  24. it under the terms of the GNU General Public License as published by
  25. the Free Software Foundation, either version 3 of the License, or
  26. (at your option) any later version.
  27. This program is distributed in the hope that it will be useful,
  28. but WITHOUT ANY WARRANTY; without even the implied warranty of
  29. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  30. GNU General Public License for more details.
  31. You should have received a copy of the GNU General Public License
  32. along with this program. If not, see <https://www.gnu.org/licenses/>.
  33. */
  34.  
  35. /* globals Node, GM, GM_getResourceText, stringSimilarity */
  36.  
  37. (function () {
  38. 'use strict'
  39.  
  40. const DEFAULT_DATA = '{"restaurants": {}}'
  41.  
  42. function timeSince (date) {
  43. // https://stackoverflow.com/a/72973090/
  44. const MINUTE = 60
  45. const HOUR = MINUTE * 60
  46. const DAY = HOUR * 24
  47. const WEEK = DAY * 7
  48. const MONTH = DAY * 30
  49. const YEAR = DAY * 365
  50. const secondsAgo = Math.round((Date.now() - Number(date)) / 1000)
  51. if (secondsAgo < MINUTE) {
  52. return secondsAgo + ` second${secondsAgo !== 1 ? 's' : ''} ago`
  53. }
  54. let divisor
  55. let unit = ''
  56. if (secondsAgo < HOUR) {
  57. [divisor, unit] = [MINUTE, 'minute']
  58. } else if (secondsAgo < DAY) {
  59. [divisor, unit] = [HOUR, 'hour']
  60. } else if (secondsAgo < WEEK) {
  61. [divisor, unit] = [DAY, 'day']
  62. } else if (secondsAgo < MONTH) {
  63. [divisor, unit] = [WEEK, 'week']
  64. } else if (secondsAgo < YEAR) {
  65. [divisor, unit] = [MONTH, 'month']
  66. } else {
  67. [divisor, unit] = [YEAR, 'year']
  68. }
  69. const count = Math.floor(secondsAgo / divisor)
  70. return `${count} ${unit}${count > 1 ? 's' : ''} ago`
  71. }
  72.  
  73. function symmetricDifference (setA, setB) {
  74. const _difference = new Set(setA)
  75. for (const elem of setB) {
  76. if (_difference.has(elem)) {
  77. _difference.delete(elem)
  78. } else {
  79. _difference.add(elem)
  80. }
  81. }
  82. return _difference
  83. }
  84.  
  85. function compareNames (s0, s1) {
  86. let r = 0
  87. s0 = s0.toLowerCase().trim()
  88. s1 = s1.toLowerCase().trim()
  89. if (s0 === s1) {
  90. return 2
  91. }
  92. const set0 = new Set(s0.split(/\s+/))
  93. const set1 = new Set(s1.split(/\s+/))
  94. r -= symmetricDifference(set0, set1).size
  95. if (r < 0) {
  96. r += stringSimilarity.compareTwoStrings(s0, s1)
  97. }
  98. return r
  99. }
  100.  
  101. function getThumbs (onUpClick, onDownClick) {
  102. const thumbs = document.createElement('div')
  103. thumbs.classList.add('thumbscontainer')
  104. const thumbUpSVG = document.createElement('div')
  105. thumbUpSVG.style.width = '40px'
  106. thumbUpSVG.style.height = '40px'
  107. thumbUpSVG.style.float = 'left'
  108. thumbUpSVG.style.cursor = 'pointer'
  109. thumbUpSVG.innerHTML = GM_getResourceText('thumbUp').replace('id="emoji"', 'id="thumbUp' + Math.random() + '"')
  110. thumbUpSVG.querySelector('#skin polygon').setAttribute('fill', '#cccccc')
  111. thumbUpSVG.addEventListener('click', onUpClick)
  112. thumbs.appendChild(thumbUpSVG)
  113. const thumbDownSVG = document.createElement('div')
  114. thumbDownSVG.style.width = '40px'
  115. thumbDownSVG.style.height = '40px'
  116. thumbDownSVG.style.float = 'left'
  117. thumbDownSVG.style.cursor = 'pointer'
  118. thumbDownSVG.innerHTML = GM_getResourceText('thumbDown').replace('id="emoji"', 'id="thumbDown' + Math.random() + '"')
  119. thumbDownSVG.querySelector('#skin polygon').setAttribute('fill', '#cccccc')
  120. thumbDownSVG.addEventListener('click', onDownClick)
  121. thumbs.appendChild(thumbDownSVG)
  122. thumbs.appendChild(document.createElement('div')).style.clear = 'left'
  123. return [thumbs, thumbUpSVG, thumbDownSVG]
  124. }
  125.  
  126. function clearAllRatings () {
  127. const promises = []
  128. for (const gmKey of ['swiggy', 'zomato']) {
  129. promises.push(GM.setValue(gmKey, DEFAULT_DATA))
  130. }
  131. Promise.all(promises).then(() => {
  132. window.alert('All ratings cleared\n\nReload the page to see the changes')
  133. document.location.reload()
  134. })
  135. }
  136. async function clearRestaurantRatings (node) {
  137. const gmKey = node.dataset.gmKey
  138. const restaurantId = node.dataset.restaurantId
  139. const restaurantName = node.dataset.restaurantName
  140.  
  141. if (!gmKey || !restaurantId) {
  142. return false
  143. }
  144.  
  145. if (!window.confirm('Clear all ratings for this restaurant?\n\n' + restaurantName + '\n\nThis cannot be undone!')) {
  146. return false
  147. }
  148.  
  149. const data = JSON.parse(await GM.getValue(gmKey, DEFAULT_DATA))
  150. if ((restaurantId in data.restaurants)) {
  151. delete data.restaurants[restaurantId]
  152. }
  153. await GM.setValue(gmKey, JSON.stringify(data))
  154. return true
  155. }
  156.  
  157. async function listRatings (mGmKey, selectedRestaurantId) {
  158. const showRestaurantDishes = function (data, listDiv, restaurantId, gmKey) {
  159. const info = data.restaurants[restaurantId].info
  160. const dishes = data.restaurants[restaurantId].dishes
  161. if (!dishes) {
  162. return
  163. }
  164. const restaDiv = listDiv.appendChild(document.createElement('div'))
  165. restaDiv.classList.add('restaurant_container')
  166. const metaDiv = restaDiv.appendChild(document.createElement('div'))
  167. metaDiv.classList.add('ratings_meta')
  168. const ra = metaDiv.appendChild(document.createElement('a'))
  169. ra.href = info.url
  170. const label = 'name' in info ? info.name : info.url
  171. ra.appendChild(document.createTextNode(label))
  172. if ('location' in info && info.location && info.location.trim()) {
  173. const span = metaDiv.appendChild(document.createElement('span'))
  174. span.appendChild(document.createTextNode(` (${info.location})`))
  175. }
  176. const lastOverallRatingSpan = metaDiv.appendChild(document.createElement('span'))
  177. const clearButton = metaDiv.appendChild(document.createElement('button'))
  178. clearButton.style.fontSize = 'small'
  179. clearButton.style.marginLeft = '3px'
  180. clearButton.addEventListener('click', function () {
  181. clearRestaurantRatings(this).then(function (cleared) {
  182. if (cleared) {
  183. document.location.reload()
  184. }
  185. })
  186. })
  187. clearButton.dataset.restaurantId = restaurantId
  188. clearButton.dataset.gmKey = gmKey
  189. clearButton.dataset.restaurantName = label
  190. clearButton.appendChild(document.createTextNode('Clear'))
  191. const listDivUp = restaDiv.appendChild(document.createElement('div'))
  192. const listDivDown = restaDiv.appendChild(document.createElement('div'))
  193. listDivUp.classList.add('ratings_list', 'up')
  194. listDivDown.classList.add('ratings_list', 'down')
  195. restaDiv.appendChild(document.createElement('div')).style.clear = 'left'
  196. let lastRating = null
  197. for (const dishName in dishes) {
  198. const dish = dishes[dishName]
  199. const div = dish.rating > 0 ? listDivUp : listDivDown
  200. const le = div.appendChild(document.createElement('div'))
  201. le.classList.add('ratings_item')
  202. le.appendChild(document.createTextNode(dishName))
  203. if ('price' in dish && dish.price) {
  204. le.appendChild(document.createTextNode(` ${dish.price}`))
  205. }
  206. if ('veg' in dish && dish.veg) {
  207. const span = le.appendChild(document.createElement('span'))
  208. if (dish.veg === 'veg') {
  209. span.classList.add('veggy_icon')
  210. span.appendChild(document.createTextNode('\u23FA'))
  211. } else {
  212. span.classList.add('nonveggy_icon')
  213. span.appendChild(document.createTextNode('\u2BC5'))
  214. }
  215. }
  216. const date = new Date(dish.lastRating)
  217. const dateStr = 'Rated: ' + date.toLocaleDateString() + ' ' + timeSince(date)
  218. le.setAttribute('title', dateStr)
  219. if (lastRating == null || date > lastRating) {
  220. lastRating = date
  221. }
  222. }
  223. if (lastRating) {
  224. const dateStr = ' ' + lastRating.toLocaleDateString() + ' ' + timeSince(lastRating)
  225. lastOverallRatingSpan.appendChild(document.createTextNode(dateStr))
  226. }
  227. }
  228.  
  229. let listDiv = document.getElementById('ratings_container')
  230. if (!listDiv) {
  231. createMainContainer(mGmKey, selectedRestaurantId)
  232. listDiv = document.getElementById('ratings_container')
  233. }
  234. listDiv.innerHTML = ''
  235.  
  236. for (const gmKey of ['swiggy', 'zomato']) {
  237. const data = JSON.parse(await GM.getValue(gmKey, DEFAULT_DATA))
  238. if (selectedRestaurantId && selectedRestaurantId in data.restaurants) {
  239. // Show current restaurant first
  240. showRestaurantDishes(data, listDiv, selectedRestaurantId, gmKey)
  241. }
  242. for (const restaurantId in data.restaurants) {
  243. if (!selectedRestaurantId || selectedRestaurantId !== restaurantId) {
  244. showRestaurantDishes(data, listDiv, restaurantId, gmKey)
  245. }
  246. }
  247. }
  248. }
  249.  
  250. function crossCheckNames (name, data) {
  251. const results = []
  252. for (const restaurantId in data.restaurants) {
  253. if (!('name' in data.restaurants[restaurantId].info)) {
  254. continue
  255. }
  256. const r = compareNames(data.restaurants[restaurantId].info.name, name)
  257. if (r > -2) {
  258. results.push([r, data.restaurants[restaurantId]])
  259. }
  260. }
  261. return results.sort((a, b) => b[0] - a[0]).map(v => v[1])
  262. }
  263.  
  264. async function crossCheck (restaurantId, restaurantInfo, gmKey) {
  265. if (!('name' in restaurantInfo) || !restaurantInfo.name) {
  266. return
  267. }
  268.  
  269. const data = JSON.parse(await GM.getValue(gmKey === 'swiggy' ? 'zomato' : 'swiggy', DEFAULT_DATA))
  270.  
  271. const results = crossCheckNames(restaurantInfo.name, data)
  272. showCrossCheckResults(gmKey, restaurantId, results)
  273. }
  274.  
  275. function showCrossCheckResultsWide () {
  276. document.getElementById('cross_check_results').classList.add('fullscreen')
  277. try {
  278. this.remove()
  279. } catch (e) {}
  280.  
  281. document.head.appendChild(document.createElement('style')).innerHTML = `
  282. #cross_check_results.fullscreen {
  283. top: 5px;
  284. right:5px;
  285. height: 95%;
  286. width: 95%;
  287. max-width: 95%;
  288. max-height: 95%;
  289. }
  290.  
  291. #cross_check_results.fullscreen .ratings_list {
  292. width:45%;
  293. float:left;
  294. }
  295. `
  296. }
  297.  
  298. function showCrossCheckResults (gmKey, restaurantId, results) {
  299. if (!results.length) {
  300. return
  301. }
  302. const div = createMainContainer(gmKey, restaurantId)
  303.  
  304. const resultsHead = div.appendChild(document.createElement('div'))
  305. resultsHead.appendChild(document.createTextNode('Similar named restaurants you voted on ' + (gmKey === 'swiggy' ? 'Zomato' : 'Swiggy')))
  306. resultsHead.style.fontWeight = 'bold'
  307.  
  308. const resultsDiv = div.appendChild(document.createElement('div'))
  309. results.forEach(function (restaurant, i) {
  310. const restaurantDiv = resultsDiv.appendChild(document.createElement('div'))
  311. if (i % 2 === 0) {
  312. restaurantDiv.style.backgroundColor = '#ddd'
  313. }
  314. const restaurantName = restaurantDiv.appendChild(document.createElement('div'))
  315. restaurantName.appendChild(document.createTextNode(restaurant.info.name))
  316. const restaurantLoc = restaurantDiv.appendChild(document.createElement('div'))
  317. restaurantLoc.appendChild(document.createTextNode(restaurant.info.location || ''))
  318. restaurantLoc.style.fontSize = '10pt'
  319. const restaurantLink = restaurantDiv.appendChild(document.createElement('a'))
  320. restaurantLink.appendChild(document.createTextNode(restaurant.info.url))
  321. restaurantLink.setAttribute('href', restaurant.info.url)
  322. restaurantLink.style.fontSize = '7pt'
  323. })
  324. }
  325.  
  326. function createMainContainer (gmKey = 'swiggy', restaurantId = null, clear = false) {
  327. let div = document.getElementById('cross_check_results')
  328. if (!div) {
  329. div = document.body.appendChild(document.createElement('div'))
  330. div.setAttribute('id', 'cross_check_results')
  331. document.head.appendChild(document.createElement('style')).innerHTML = `
  332. #cross_check_results {
  333. z-index:1200;
  334. position:fixed;
  335. top: 100px;
  336. right:5px;
  337. max-height: 70%;
  338. max-width: 20%;
  339. overflow: auto;
  340. border:2px solid #223075;
  341. background:white;
  342. font-size:12pt
  343. }
  344. #cross_check_results button {
  345. border: 1px solid #777;
  346. border-radius: 4px;
  347. background: #e0e0e0;
  348. }
  349. #cross_check_results button:hover {
  350. border: 1px solid #000;
  351. border-radius: 4px;
  352. background: #f0f0f0;
  353. }
  354.  
  355. #cross_check_results a:link {
  356. text-decoration:underline;
  357. color:#06c;
  358. }
  359. #cross_check_results a:visited {
  360. text-decoration:underline;
  361. color:#06c;
  362. }
  363.  
  364. #cross_check_results .restaurant_container {
  365. border-bottom: 2px solid #848484;
  366. }
  367.  
  368. #cross_check_results .ratings_meta {
  369. background-color:#f4e9bc;
  370. background-image: linear-gradient(to right, white , #f4e9bc);
  371. margin-top:3px;
  372. }
  373.  
  374. #cross_check_results .ratings_list {
  375. float:left;
  376. margin: 2px;
  377. }
  378. #cross_check_results .ratings_list.up {
  379. background-color:#e6ffe6;
  380. }
  381. #cross_check_results .ratings_list.down {
  382. background-color:#fbd5d5;
  383. margin-left: 5px;
  384. }
  385. #cross_check_results .ratings_item:nth-child(2n+2){
  386. background-color:#0000000f;
  387. }
  388. #cross_check_results .veggy_icon {
  389. color: #0f8a65;
  390. border: 2px solid #0f8a65;
  391. font-size: 8px;
  392. height: 13px;
  393. display: inline-block;
  394. font-weight: 1000;
  395. width: 12px;
  396. vertical-align: middle;
  397. margin: 1px;
  398. }
  399. #cross_check_results .nonveggy_icon {
  400. color: #e43b4f;
  401. border: 2px solid #e43b4f;
  402. font-size: 8px;
  403. height: 13px;
  404. display: inline-block;
  405. font-weight: 1000;
  406. width: 12px;
  407. vertical-align: middle;
  408. margin: 1px;
  409. }
  410.  
  411. #cross_check_results .ratings_meta span {
  412. color: #555;
  413. font-size: 10pt;
  414. }
  415.  
  416. `
  417.  
  418. const controlsDiv = div.appendChild(document.createElement('div'))
  419. controlsDiv.setAttribute('id', 'controls_container')
  420.  
  421. const closeButton = controlsDiv.appendChild(document.createElement('button'))
  422. closeButton.appendChild(document.createTextNode('Close'))
  423. closeButton.addEventListener('click', function () {
  424. removeMainContainer()
  425. showCrossCheckResults(gmKey, restaurantId, [])
  426. })
  427.  
  428. const clearButton = controlsDiv.appendChild(document.createElement('button'))
  429. clearButton.appendChild(document.createTextNode('Clear all'))
  430. clearButton.addEventListener('click', function () {
  431. if (window.confirm('Delete ratings for all restaurants?') && window.confirm('Delete ratings for ALL restaurants?\n\nAre you sure?')) {
  432. clearAllRatings()
  433. }
  434. })
  435.  
  436. const fullscreenButton = controlsDiv.appendChild(document.createElement('button'))
  437. fullscreenButton.appendChild(document.createTextNode('\u27F7'))
  438. fullscreenButton.addEventListener('click', showCrossCheckResultsWide)
  439.  
  440. const listDiv = div.appendChild(document.createElement('div'))
  441. listDiv.setAttribute('id', 'ratings_container')
  442. const listButton = listDiv.appendChild(document.createElement('button'))
  443. listButton.appendChild(document.createTextNode('View ratings'))
  444. listButton.addEventListener('click', () => listRatings(gmKey, restaurantId))
  445. }
  446.  
  447. if (clear) {
  448. div.classList.remove('fullscreen')
  449. div.innerHTML = ''
  450. }
  451.  
  452. div.style.display = 'block'
  453.  
  454. return div
  455. }
  456.  
  457. function removeMainContainer () {
  458. const div = document.getElementById('cross_check_results')
  459. if (div) {
  460. div.remove()
  461. }
  462. }
  463.  
  464. if (document.location.hostname.endsWith('.swiggy.com')) {
  465. let crossCheckDone = false
  466. const getRestaurantInfo = function () {
  467. const results = {}
  468. const h1 = document.querySelector('[class*="RestaurantNameAddress_name"]')
  469. if (h1) {
  470. results.name = h1.textContent.trim()
  471. }
  472. try {
  473. results.location = document.querySelector('[class*="RestaurantNameAddress_area"]').textContent.trim()
  474. } catch (e) {
  475. console.log(e)
  476. }
  477. return results
  478. }
  479. const addRatingsButton = function () {
  480. if (document.getElementById('nav_rating_button')) {
  481. return
  482. }
  483. if (document.querySelector('.global-nav a[href*="/support"]')) {
  484. const orgLi = document.querySelector('.global-nav a[href*="/support"]').parentNode.parentNode
  485. const li = orgLi.cloneNode(true)
  486. orgLi.parentNode.appendChild(li)
  487. li.setAttribute('id', 'nav_rating_button')
  488. li.addEventListener('click', function (ev) {
  489. ev.preventDefault()
  490. listRatings('swiggy', null)
  491. })
  492. li.querySelector('a').href = '#'
  493. const svg = li.querySelector('svg')
  494. const span = svg.parentNode
  495. span.parentNode.replaceChild(document.createTextNode('Ratings'), span.nextSibling)
  496. const starSVG = document.createElement('div')
  497. starSVG.style.width = '22px'
  498. starSVG.style.height = '22px'
  499. starSVG.style.cursor = 'pointer'
  500. starSVG.innerHTML = GM_getResourceText('star').replace('id="emoji"', 'id="starSVG' + Math.random() + '"')
  501. starSVG.querySelector('#color polygon').setAttribute('fill', '#ffffff')
  502. starSVG.querySelector('#line polygon').setAttribute('stroke', '#3d4152')
  503. starSVG.querySelector('#line polygon').setAttribute('stroke-width', '6')
  504. span.replaceChild(starSVG, svg)
  505. } else if (!document.getElementById('cross_check_results')) {
  506. createMainContainer('swiggy', null)
  507. }
  508. }
  509. const addRatings = async function () {
  510. const m = document.location.pathname.match(/\/restaurants\/[\w-]+-(\d+)/)
  511. if (!m) {
  512. return
  513. }
  514. const restaurantId = m[1]
  515.  
  516. let data = JSON.parse(await GM.getValue('swiggy', DEFAULT_DATA))
  517. if (!(restaurantId in data.restaurants)) {
  518. data.restaurants[restaurantId] = { dishes: {}, info: { id: restaurantId, url: document.location.href } }
  519. }
  520.  
  521. if (!crossCheckDone) {
  522. crossCheckDone = true
  523. crossCheck(restaurantId, getRestaurantInfo(), 'swiggy')
  524. }
  525.  
  526. document.querySelectorAll('[data-testid*="dish-item"]').forEach(function (menuItem) {
  527. if ('userscriptprocessed' in menuItem.dataset) {
  528. return
  529. }
  530. menuItem.dataset.userscriptprocessed = 1
  531. const dishName = menuItem.querySelector('[class*=itemNameText]').textContent.trim()
  532. const saveRating = async function (rating) {
  533. let price = null
  534. try {
  535. price = parseInt(menuItem.querySelector('.rupee').textContent.trim())
  536. } catch (e) {
  537. console.log(e)
  538. }
  539. let veg = null
  540. const icon = menuItem.querySelector('[class*=styles_icon]')
  541. if (icon && icon.className.match(/icon-?([a-z]+)/i)) {
  542. veg = icon.className.match(/icon-?([a-z]+)/i)[1].toLowerCase() // "veg", "nonveg"
  543. }
  544. data = JSON.parse(await GM.getValue('swiggy', DEFAULT_DATA))
  545. if (!(restaurantId in data.restaurants)) {
  546. data.restaurants[restaurantId] = { dishes: {}, info: { id: restaurantId, url: document.location.href } }
  547. }
  548. if (!(dishName in data.restaurants[restaurantId].dishes)) {
  549. data.restaurants[restaurantId].dishes[dishName] = {
  550. name: dishName,
  551. price,
  552. veg,
  553. lastRating: new Date().toJSON().toString()
  554. }
  555. }
  556. data.restaurants[restaurantId].dishes[dishName].rating = rating
  557. data.restaurants[restaurantId].info = Object.assign(data.restaurants[restaurantId].info, getRestaurantInfo())
  558. await GM.setValue('swiggy', JSON.stringify(data))
  559. }
  560.  
  561. const onUp = function () {
  562. saveRating(1).then(function () {
  563. thumbUp.querySelector('#skin polygon').setAttribute('fill', '#50c020')
  564. thumbDown.querySelector('#skin polygon').setAttribute('fill', '#cccccc')
  565. })
  566. }
  567. const onDown = function () {
  568. saveRating(-1).then(function () {
  569. thumbUp.querySelector('#skin polygon').setAttribute('fill', '#cccccc')
  570. thumbDown.querySelector('#skin polygon').setAttribute('fill', '#e60000')
  571. })
  572. }
  573.  
  574. const [thumbs, thumbUp, thumbDown] = getThumbs(onUp, onDown)
  575. const parentContainer = menuItem.querySelector('[class*=itemImageContainer]')
  576. thumbs.style.position = 'relative'
  577. thumbs.style.zIndex = 1
  578. if (parentContainer.className.indexOf('NoImage') === -1) {
  579. thumbs.style.marginTop = '20pt'
  580. }
  581. parentContainer.appendChild(thumbs)
  582. if (dishName in data.restaurants[restaurantId].dishes) {
  583. if (data.restaurants[restaurantId].dishes[dishName].rating > 0) {
  584. thumbUp.querySelector('#skin polygon').setAttribute('fill', '#50c020')
  585. } else if (data.restaurants[restaurantId].dishes[dishName].rating < 0) {
  586. thumbDown.querySelector('#skin polygon').setAttribute('fill', '#e60000')
  587. }
  588. const dateDiv = thumbs.appendChild(document.createElement('div'))
  589. const date = new Date(data.restaurants[restaurantId].dishes[dishName].lastRating)
  590. const dateStr = date.toLocaleDateString() + ' ' + timeSince(date)
  591. dateDiv.style.fontSize = '10px'
  592. dateDiv.appendChild(document.createTextNode(dateStr))
  593. }
  594. })
  595. }
  596. const addNonVegToggle = function () {
  597. let orgDiv
  598. let newDiv
  599. let isActive
  600. const orgClick = function () {
  601. if (isActive) {
  602. console.debug('orgClick: already non-veg, reset it')
  603. resetNonVeg()
  604. }
  605. }
  606. const resetNonVeg = function () {
  607. document.querySelectorAll('.hiddenbyscript').forEach(function (menuItem) {
  608. menuItem.classList.remove('hiddenbyscript')
  609. menuItem.style.display = ''
  610. })
  611. isActive = false
  612. newDiv.querySelector('[class*="ToggleSwitch_toggleBar"]').style.backgroundColor = ''
  613. newDiv.querySelector('[class*="ToggleSwitch_toggleThumb"]').style.backgroundColor = ''
  614. newDiv.querySelector('[class*="ToggleSwitch_toggleThumb"]').style.transform = ''
  615. }
  616. const enableNonVeg = function (ev) {
  617. if (ev) {
  618. ev.preventDefault()
  619. ev.stopPropagation()
  620. }
  621. if (isActive) {
  622. console.debug('enableNonVeg: already non-veg, reset it')
  623. window.setTimeout(resetNonVeg, 100)
  624. return
  625. }
  626.  
  627. if (orgDiv.querySelector('[class*="toggleThumbActive"]')) {
  628. console.debug('enableNonVeg: org checkbox is checked, click it and wait')
  629. orgDiv.querySelector('button').click()
  630. window.setTimeout(enableNonVeg, 500)
  631. newDiv.querySelector('[class*="ToggleSwitch_toggleBar"]').style.backgroundColor = '#87d'
  632. return
  633. }
  634.  
  635. console.debug('enableNonVeg: hide menu items')
  636. document.querySelectorAll('[data-testid*="dish-item"]').forEach(function (menuItem) {
  637. const icon = menuItem.querySelector('[class*=styles_icon]')
  638. if (icon && icon.className.match(/icon-?veg/i)) {
  639. menuItem.classList.add('hiddenbyscript')
  640. menuItem.style.display = 'none'
  641. }
  642. })
  643. isActive = true
  644. newDiv.querySelector('[class*="ToggleSwitch_toggleBar"]').style.backgroundColor = '#e43b4f'
  645. newDiv.querySelector('[class*="ToggleSwitch_toggleThumb"]').style.backgroundColor = '#e43b4f'
  646. newDiv.querySelector('[class*="ToggleSwitch_toggleThumb"]').style.transform = 'translate3d(18px,0,0)'
  647. }
  648. const labels = document.querySelectorAll('[data-testid*="filter-switch"]')
  649. labels.forEach(function (label) {
  650. if (label.className.indexOf('vegOnly') !== -1) {
  651. orgDiv = label
  652. orgDiv.parentNode.style.justifyContent = 'flex-start'
  653. newDiv = orgDiv.parentNode.appendChild(label.cloneNode(true))
  654. newDiv.style.marginLeft = '1em'
  655. newDiv.setAttribute('id', 'nonVegToggle')
  656. newDiv.querySelector('[class*="Label_"]').textContent = 'Non veg'
  657. newDiv.querySelector('button').addEventListener('click', enableNonVeg)
  658. orgDiv.querySelector('button').addEventListener('click', orgClick)
  659. }
  660. })
  661. }
  662. window.setInterval(function () {
  663. addRatingsButton()
  664. addRatings()
  665. if (!document.getElementById('nonVegToggle')) {
  666. addNonVegToggle()
  667. }
  668. }, 1000)
  669. } else if (document.location.hostname.endsWith('.zomato.com')) {
  670. let crossCheckDone = false
  671. const getRestaurantInfo = function () {
  672. const results = {}
  673. const h1 = document.querySelector('div#root main section>div>div>div>h1')
  674. if (h1) {
  675. results.name = h1.textContent.trim()
  676. }
  677. try {
  678. results.location = h1.parentNode.nextElementSibling.firstChild.nextElementSibling.textContent.trim()
  679. } catch (e) {
  680. console.log(e)
  681. }
  682. return results
  683. }
  684. const addRatingsButton = function () {
  685. if (document.getElementById('nav_rating_button')) {
  686. return
  687. }
  688. if (document.querySelector('ul[id*=navigation]')) {
  689. const orgLi = document.querySelector('ul[id*=navigation]').querySelector('li:last-child')
  690. const li = orgLi.cloneNode(true)
  691. orgLi.parentNode.appendChild(li)
  692. li.setAttribute('id', 'nav_rating_button')
  693. li.addEventListener('click', function (ev) {
  694. ev.preventDefault()
  695. listRatings('zomato', null)
  696. })
  697. const a = li.querySelector('a')
  698. a.innerHTML = ''
  699. a.style.fontSize = '10px'
  700. const starSVG = document.createElement('div')
  701. starSVG.style.width = '22px'
  702. starSVG.style.height = '22px'
  703. starSVG.style.cursor = 'pointer'
  704. starSVG.style.margin = 'auto'
  705. starSVG.style.marginTop = '-35px'
  706. starSVG.innerHTML = GM_getResourceText('star').replace('id="emoji"', 'id="starSVG' + Math.random() + '"')
  707. starSVG.querySelector('#color polygon').setAttribute('fill', '#EF4F5F')
  708. a.appendChild(starSVG)
  709. a.appendChild(document.createTextNode('Ratings'))
  710. } else if (!document.getElementById('cross_check_results')) {
  711. createMainContainer('zomato', null)
  712. }
  713. }
  714. const addRatings = async function () {
  715. const m = document.location.pathname.match(/([\w-]+\/[\w-]+)\/order/)
  716. if (!m) {
  717. return
  718. }
  719. const restaurantId = m[1]
  720.  
  721. let data = JSON.parse(await GM.getValue('zomato', DEFAULT_DATA))
  722. if (!(restaurantId in data.restaurants)) {
  723. data.restaurants[restaurantId] = { dishes: {}, info: { id: restaurantId, url: document.location.href } }
  724. }
  725.  
  726. if (!crossCheckDone) {
  727. crossCheckDone = true
  728. crossCheck(restaurantId, getRestaurantInfo(), 'zomato')
  729. }
  730.  
  731. document.querySelectorAll('[type="veg"],[type="non-veg"]').forEach(function (symbol) {
  732. const menuItem = symbol.parentNode.parentNode.parentNode
  733. if ('userscriptprocessed' in menuItem.dataset) {
  734. return
  735. }
  736. menuItem.dataset.userscriptprocessed = 1
  737. const dishName = menuItem.querySelector('h4').textContent.trim()
  738. const saveRating = async function (rating) {
  739. let price = null
  740. try {
  741. price = parseInt(menuItem.textContent.match(/₹\s*(\d+)/)[1])
  742. } catch (e) {
  743. console.log(e)
  744. }
  745. const veg = symbol.getAttribute('type').toLowerCase().replace('-', '') // "veg", "nonveg"
  746. data = JSON.parse(await GM.getValue('zomato', DEFAULT_DATA))
  747. if (!(restaurantId in data.restaurants)) {
  748. data.restaurants[restaurantId] = { dishes: {}, info: { id: restaurantId, url: document.location.href } }
  749. }
  750. if (!(dishName in data.restaurants[restaurantId].dishes)) {
  751. data.restaurants[restaurantId].dishes[dishName] = {
  752. name: dishName,
  753. price,
  754. veg, // "veg", "nonveg"
  755. lastRating: new Date().toJSON().toString()
  756. }
  757. }
  758. data.restaurants[restaurantId].dishes[dishName].rating = rating
  759. data.restaurants[restaurantId].info = Object.assign(data.restaurants[restaurantId].info, getRestaurantInfo())
  760. await GM.setValue('zomato', JSON.stringify(data))
  761. }
  762.  
  763. const onUp = function () {
  764. saveRating(1).then(function () {
  765. thumbUp.querySelector('#skin polygon').setAttribute('fill', '#50c020')
  766. thumbDown.querySelector('#skin polygon').setAttribute('fill', '#cccccc')
  767. })
  768. }
  769. const onDown = function () {
  770. saveRating(-1).then(function () {
  771. thumbUp.querySelector('#skin polygon').setAttribute('fill', '#cccccc')
  772. thumbDown.querySelector('#skin polygon').setAttribute('fill', '#e60000')
  773. })
  774. }
  775.  
  776. const [thumbs, thumbUp, thumbDown] = getThumbs(onUp, onDown)
  777. thumbs.style.marginTop = '20pt'
  778. menuItem.firstChild.appendChild(thumbs)
  779. if (dishName in data.restaurants[restaurantId].dishes) {
  780. if (data.restaurants[restaurantId].dishes[dishName].rating > 0) {
  781. thumbUp.querySelector('#skin polygon').setAttribute('fill', '#50c020')
  782. } else if (data.restaurants[restaurantId].dishes[dishName].rating < 0) {
  783. thumbDown.querySelector('#skin polygon').setAttribute('fill', '#e60000')
  784. }
  785. const dateDiv = thumbs.appendChild(document.createElement('div'))
  786. const date = new Date(data.restaurants[restaurantId].dishes[dishName].lastRating)
  787. const dateStr = date.toLocaleDateString() + ' ' + timeSince(date)
  788. dateDiv.style.fontSize = '10px'
  789. dateDiv.appendChild(document.createTextNode(dateStr))
  790. }
  791. })
  792. }
  793. const addNonVegToggle = function () {
  794. let label
  795. let orgDiv
  796. let newDiv
  797. let newCheckbox
  798. const orgClick = function () {
  799. if (newCheckbox.checked) {
  800. console.debug('orgClick: already non-veg, reset it')
  801. resetNonVeg()
  802. }
  803. }
  804. const resetNonVeg = function () {
  805. document.querySelectorAll('.hiddenbyscript').forEach(function (menuItem) {
  806. menuItem.classList.remove('hiddenbyscript')
  807. menuItem.style.display = ''
  808. })
  809. newCheckbox.checked = false
  810. newCheckbox.style.backgroundColor = ''
  811. }
  812. const enableNonVeg = function (ev) {
  813. if (ev) {
  814. ev.preventDefault()
  815. ev.stopPropagation()
  816. }
  817. newCheckbox.style.backgroundColor = '#87d'
  818. window.setTimeout(function () {
  819. if (newCheckbox.checked) {
  820. console.debug('enableNonVeg: already non-veg, reset it')
  821. window.setTimeout(resetNonVeg, 200)
  822. return
  823. }
  824.  
  825. if (orgDiv.checked) {
  826. console.debug('enableNonVeg: org checkbox is checked, click it and wait')
  827. orgDiv.click()
  828. window.setTimeout(enableNonVeg, 500)
  829. return
  830. }
  831.  
  832. console.debug('enableNonVeg: hide menu items')
  833. document.querySelectorAll('[type="veg"]').forEach(function (symbol) {
  834. const menuItem = symbol.parentNode.parentNode.parentNode
  835. menuItem.classList.add('hiddenbyscript')
  836. menuItem.style.display = 'none'
  837. })
  838. newCheckbox.checked = true
  839. newCheckbox.style.backgroundColor = ''
  840. }, 100)
  841. }
  842. const labels = document.querySelectorAll('label')
  843. labels.forEach(function (l) {
  844. if (l.textContent.toLowerCase().indexOf('veg') !== -1 && l.textContent.toLowerCase().indexOf('only') !== -1) {
  845. label = l
  846. orgDiv = label
  847. newDiv = orgDiv.cloneNode(true)
  848. label.parentNode.appendChild(newDiv)
  849. label.parentNode.style.width = (label.parentNode.clientWidth + newDiv.clientWidth + 17) + 'px'
  850. newCheckbox = newDiv.querySelector('input[type=checkbox]')
  851. newCheckbox.checked = false
  852. newDiv.setAttribute('id', 'nonVegToggle')
  853. newDiv.childNodes.forEach(function (c) {
  854. if (c.nodeType === Node.TEXT_NODE && c.textContent.toLowerCase().indexOf('veg') !== -1) {
  855. c.textContent = 'Non veg'
  856. }
  857. })
  858. newDiv.addEventListener('click', enableNonVeg)
  859. orgDiv.addEventListener('click', orgClick)
  860. }
  861. })
  862. }
  863. window.setInterval(function () {
  864. addRatingsButton()
  865. addRatings()
  866. if (!document.getElementById('nonVegToggle')) {
  867. addNonVegToggle()
  868. }
  869. }, 1000)
  870. }
  871. })()