GoOut: Add Event to Google Calendar

Displays Calendar button to easily add events to Google Calendar

  1. // ==UserScript==
  2. // @id gooutcalendar@jnv.github.io
  3. // @name GoOut: Add Event to Google Calendar
  4. // @description Displays Calendar button to easily add events to Google Calendar
  5. // @namespace https://jnv.github.io
  6. // @domain goout.net
  7. // @include https://goout.net/*
  8. // @version 2018.11.28
  9. // @grant none
  10. // @run-at document-idle
  11. // @screenshot https://gist.github.com/jnv/b1891f33fb7b6f6d03dd435ba7dc3266/raw/screenshot.png
  12. // @license CC0 1.0; https://creativecommons.org/publicdomain/zero/1.0/
  13. // ==/UserScript==
  14. 'use strict'
  15.  
  16. const SEL = {
  17. parent: '[itemtype="http://schema.org/Event"]',
  18. name: '[itemprop=name]',
  19. description: '[itemprop=description]',
  20. venue: '[itemprop=location] [itemprop=name]',
  21. address: '[itemprop=address]',
  22. streetAddress: '[itemprop=streetAddress]',
  23. addressLocality: '[itemprop=addressLocality]',
  24. linkContainer: '#itemEvent .functionButtons',
  25. startDate: '[itemprop=startDate]',
  26. endDate: '[itemprop=endDate]',
  27. }
  28. const LINK_EL = document.createElement('a')
  29. const FALLBACK_END_TIME = '23:59:00'
  30.  
  31. function debounce(func, wait, immediate) {
  32. var timeout;
  33. return function() {
  34. var context = this, args = arguments;
  35. clearTimeout(timeout);
  36. timeout = setTimeout(function() {
  37. timeout = null;
  38. if (!immediate) func.apply(context, args);
  39. }, wait);
  40. if (immediate && !timeout) func.apply(context, args);
  41. };
  42. }
  43.  
  44. function dateFromLocationhash () {
  45. if (!location.hash) {
  46. return ''
  47. }
  48. const hshParams = decodeURIComponent(location.hash.replace('#', ''))
  49. const match = hshParams.match(/"(\d\d.*)"/)
  50. if (!match) {
  51. return ''
  52. }
  53. return match[1]
  54. }
  55.  
  56. function textExtractor (parent) {
  57. return (selector, firstChild = false) => {
  58. const el = parent.querySelector(selector)
  59. if (!el) {
  60. return ''
  61. }
  62. if (firstChild) {
  63. return el.firstChild.nodeValue.trim()
  64. }
  65. if (el.tagName === 'META') {
  66. return el.content
  67. }
  68. if (el.tagName === 'TIME') {
  69. return el.dateTime.replace(' ', 'T')
  70. }
  71. return el.innerText
  72. }
  73. }
  74.  
  75. function endTime (startTimeStr, lengthMinutes = 0) {
  76. const start = new Date(startTimeStr)
  77. if (lengthMinutes > 0) {
  78. const end = new Date(start.getTime() + lengthMinutes * 60000)
  79. return end.toISOString()
  80. }
  81. // 0 length given, use fallback time
  82. return startTimeStr.replace(/T.*$/i, `T${FALLBACK_END_TIME}`)
  83. }
  84.  
  85. const LENGTH_REGEX = /(Délka|Długość|Length)\s+(\d+)/
  86. function extractLength (parent) {
  87. const rows = parent.querySelector('.basic_row_info')
  88. if (!rows) {
  89. return 0
  90. }
  91. const rowsText = rows.innerText
  92. const match = rowsText.match(LENGTH_REGEX)
  93. if (match) {
  94. return parseInt(match[2], 10)
  95. }
  96. return 0
  97. }
  98.  
  99. function extractData () {
  100. const parent = document.querySelector(SEL.parent)
  101. if (!parent) {
  102. return null
  103. }
  104. const ex = textExtractor(parent)
  105. let dateStart = dateFromLocationhash()
  106. if (!dateStart) {
  107. dateStart = ex(SEL.startDate)
  108. }
  109. let address = ex(SEL.address)
  110. if (!address) {
  111. address = `${ex(SEL.streetAddress)}, ${ex(SEL.addressLocality)}`
  112. }
  113. const length = extractLength(parent)
  114. let dateEnd = ex(SEL.endDate)
  115. if (length || !dateEnd) {
  116. dateEnd = endTime(dateStart, length)
  117. }
  118. let description = ex(SEL.description)
  119. if (description) {
  120. description += '\n\n'
  121. }
  122. description += location.href
  123. const data = {
  124. name: ex(SEL.name, true),
  125. description: description,
  126. venue: ex(SEL.venue),
  127. address: address,
  128. dateStart: dateStart,
  129. dateEnd: dateEnd,
  130. }
  131. return data
  132. }
  133.  
  134. function queryParams(data) {
  135. return Object.keys(data).map(function(key) {
  136. return [key, data[key]].map(encodeURIComponent).join("=");
  137. }).join("&");
  138. }
  139.  
  140. function googleCalendarDate(isoDateStr) {
  141. return isoDateStr.replace(/-|:|\.\d\d\d/g, '').toUpperCase()
  142. }
  143.  
  144. function googleCalendarUrl(data) {
  145. const params = {
  146. action: 'TEMPLATE',
  147. text: data.name,
  148. details: data.description,
  149. sprop: 'name:GoOut',
  150. dates: `${googleCalendarDate(data.dateStart)}/${googleCalendarDate(data.dateEnd)}`,
  151. location: `${data.venue}, ${data.address}`,
  152. pli: 1,
  153. sfi: 'true'
  154. }
  155. return `https://www.google.com/calendar/render?${queryParams(params)}`
  156. }
  157.  
  158. function appendLink (data) {
  159. const a = LINK_EL
  160. a.className = 'buttonOval buttonWhite iconfont'
  161. a.innerText = 'calendar'
  162. a.target = '_blank'
  163. a.title = 'Add to Google Calendar'
  164. a.href = googleCalendarUrl(data)
  165. const parent = document.querySelector(SEL.linkContainer)
  166. if (parent && a.parentElement != parent) {
  167. parent.prepend(a)
  168. }
  169. }
  170.  
  171. function mainHandler () {
  172. try {
  173. const data = extractData()
  174. console.log(data)
  175. if (data) {
  176. appendLink(data)
  177. }
  178. } catch (e) {
  179. console.error(e)
  180. }
  181. }
  182.  
  183. const debounceMain = debounce(mainHandler, 500)
  184.  
  185. // Initial load: when hash is already set
  186. debounceMain()
  187.  
  188. // On date change (not triggered by pushState)
  189. window.addEventListener('hashchange', debounceMain)
  190.  
  191. // Monkey-patch pushstate to trigger changes
  192. const pushState = window.history.pushState
  193. window.history.pushState = function pushStateWrapper () {
  194. debounceMain()
  195. return pushState.apply(window.history, arguments)
  196. }

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址