AO3 Floating Comment Box

Floating comment box for AO3

  1. // ==UserScript==
  2. // @name AO3 Floating Comment Box
  3. // @description Floating comment box for AO3
  4. // @match *://archiveofourown.org/*works/*
  5. // @match *://archiveofourown.gay/*works/*
  6. // @namespace https://greasyfork.org/en/scripts/395902-ao3-floating-comment-box
  7. // @version 0.9.1
  8. // @run-at document-end
  9. // @grant GM.getValue
  10. // @grant GM.setValue
  11. // @grant GM.deleteValue
  12. // ==/UserScript==
  13.  
  14.  
  15. 'use strict';
  16.  
  17. const primary = "#0275d8"
  18. const success = "#5cb85c"
  19. const danger = "#d9534f"
  20.  
  21. let curURL = document.URL
  22. if(curURL.includes("#")){
  23. curURL = document.URL.slice(0,document.URL.indexOf("#"))
  24. }
  25. let newURL = curURL
  26.  
  27. const addStyles = () => {
  28. const styles = document.createElement("style")
  29. styles.innerHTML = fullStyles() + "\n" + addMediaStyles()
  30. return styles
  31. }
  32.  
  33. const fullStyles = () => {
  34. let full = ""
  35. for(let [key, value] of Object.entries(allStyles)){
  36. let newStyle = key + " {"
  37. for(let [key2, val2] of Object.entries(value)){
  38. newStyle += "\n" + key2 + ": " + val2 + ";"
  39. }
  40. newStyle += "\n}\n"
  41. full += newStyle
  42. }
  43. return full
  44. }
  45.  
  46. const addMediaStyles = () => {
  47. let full = ""
  48. for(let [key, value] of Object.entries(mediaStyles)){
  49. let newStyle = key + "{"
  50. for(let [key2, val2] of Object.entries(value)){
  51. newStyle += "\n" + key2 + "{"
  52. for (let [key3, val3] of Object.entries(val2)){
  53. newStyle += "\n" + key3 + ": " + val3 + ";"
  54. }
  55. newStyle +="\n}\n"
  56. }
  57. newStyle += "\n}\n"
  58. full += newStyle
  59. }
  60. return full
  61. }
  62.  
  63. const mediaStyles = {
  64. "@media (min-width: 1375px)": {
  65. ".float-div": {
  66. "width": "80%",
  67. "max-width": "80%",
  68. "left": "10%"
  69. },
  70. ".float-cmt-btn": {
  71. "font-size": "1em"
  72. },
  73. "#openCmtBtn": {
  74. "font-size": "1.15em",
  75. "padding": "2px 4px"
  76. }
  77. },
  78. "@media (min-width: 1575px)": {
  79. ".float-div": {
  80. "width": "70%",
  81. "max-width": "70%",
  82. "left": "15%"
  83. },
  84. ".float-cmt-btn": {
  85. "font-size": "1em"
  86. },
  87. "#openCmtBtn": {
  88. "font-size": "1.3em",
  89. "padding": "4px 8px"
  90. }
  91. },
  92. "@media (min-width: 1850px)": {
  93. ".float-div": {
  94. "width": "60%",
  95. "max-width": "60%",
  96. "left": "20%"
  97. },
  98. ".float-cmt-btn": {
  99. "font-size": "1.1em"
  100. },
  101. "#openCmtBtn": {
  102. "font-size": "1.5em",
  103. "padding": "5px 10px"
  104. }
  105. }
  106. }
  107.  
  108. const allStyles = {
  109. ".float-div": {
  110. "display": "none",
  111. "position": "fixed",
  112. "z-index": "1",
  113. "bottom": ".5%",
  114. "width": "98%",
  115. "height": "30%",
  116. "background-color": "#ddd",
  117. "border-style": "double",
  118. "border-color": "grey",
  119. "padding": "5px",
  120. "resize": "both",
  121. "overflow": "auto",
  122. "border-radius": "25px",
  123. "border-width": "5px"
  124. },
  125. ".btn-div": {
  126. "display": "flex",
  127. "justify-content": "space-around",
  128. "top": "0px",
  129. "width": "100%",
  130. "max-width": "100%",
  131. "height": "15%"
  132. },
  133. ".char-count": {
  134. "font-size": ".8em"
  135. },
  136. ".float-box": {
  137. "min-height": "70%",
  138. "max-width": "98%",
  139. "background-color": "white"
  140. },
  141. ".float-cmt-btn": {
  142. "border": "none",
  143. "text-align": "center",
  144. "text-decoration": "none",
  145. "display": "inline-block",
  146. "font-size": ".8em",
  147. "padding": ".2% 3%",
  148. "top": "10%",
  149. "bottom": "10%",
  150. "height": "70%"
  151. },
  152. "#openCmtBtn": {
  153. "position": "fixed",
  154. "z-index": "1",
  155. "top": "0px",
  156. "left": "0px",
  157. "font-size": ".9em",
  158. "padding": "1px 2px",
  159. "border": "none",
  160. "text-align": "center",
  161. "text-decoration": "none",
  162. "display": "inline-block",
  163. "background": primary
  164. },
  165. "#addCmtBtn": {
  166. "background": primary
  167. },
  168. "#delCmtBtn": {
  169. "background": danger
  170. },
  171. "#insCmtBtn": {
  172. "background": primary
  173. },
  174. ".font-select": {
  175. "float": "right",
  176. "top": "10%",
  177. "bottom": "10%",
  178. "width": "10%",
  179. "height": "80%"
  180.  
  181. },
  182. ".btn-font": {
  183. "color": "white"
  184. }
  185.  
  186. }
  187.  
  188. const createBox = () => {
  189. const textBox = document.createElement("textarea")
  190. textBox.className = "float-box"
  191. textBox.addEventListener("keyup", async () => {
  192. await GM.setValue(newURL, textBox.value)
  193. const addBtn = document.querySelector("#addCmtBtn")
  194. const charCount = document.querySelector(".char-count")
  195. const newCount = 10000 - textBox.value.length
  196. charCount.textContent = `Characters left: ${newCount}`
  197. addBtn.style.background = primary
  198. addBtn.textContent = "Add to Comment Box"
  199. })
  200.  
  201. return textBox
  202. }
  203.  
  204. const createChangeFontSize = () => {
  205. const selectMenu = document.createElement("select")
  206. selectMenu.className = "font-select"
  207. const optNums = [".5em",".7em", ".85em", "1em", "1.25em", "1.5em"]
  208. for(let num of optNums){
  209. const opt = document.createElement("option")
  210. opt.value = num
  211. opt.className = "font-option"
  212. opt.style.fontSize = num
  213. opt.textContent = "Font size"
  214. selectMenu.appendChild(opt)
  215. }
  216. selectMenu.addEventListener("click", () => {
  217. const textBox = document.querySelector(".float-box")
  218. textBox.style.fontSize = selectMenu.value
  219. })
  220. return selectMenu
  221. }
  222.  
  223. const charCount = () => {
  224. const newDiv = document.createElement("div")
  225. newDiv.className = "char-count"
  226. newDiv.textContent = "Characters left: 10000"
  227. return newDiv
  228. }
  229.  
  230.  
  231. const createButton = () => {
  232. const newButton = document.createElement("button")
  233. newButton.className = "btn-font"
  234. newButton.id = "openCmtBtn"
  235. newButton.textContent = "O"
  236. newButton.addEventListener("click", () => {
  237. const div = document.querySelector(".float-div")
  238. if(div.style.display === "block"){
  239. div.style.display = "none"
  240. newButton.textContent = "O"
  241. newButton.style.background = primary
  242. } else {
  243. div.style.display = "block"
  244. newButton.textContent = "X"
  245. newButton.style.background = danger
  246. const textBox = document.querySelector(".float-box")
  247. textBox.scrollTop = textBox.scrollHeight
  248. }
  249.  
  250. })
  251. return newButton
  252. }
  253.  
  254. const createMainDiv = () => {
  255. const newDiv = document.createElement("div")
  256. newDiv.className = "float-div"
  257. const btnDiv = document.createElement("div")
  258. btnDiv.className = "btn-div"
  259. btnDiv.appendChild(insertButton())
  260. btnDiv.appendChild(addButton())
  261. btnDiv.appendChild(createDelete())
  262. btnDiv.appendChild(chapterRadio())
  263. btnDiv.appendChild(createChangeFontSize())
  264. newDiv.appendChild(btnDiv)
  265. newDiv.appendChild(createBox())
  266. newDiv.appendChild(charCount())
  267. return newDiv
  268. }
  269.  
  270. const createDelete = () => {
  271. const newButton = document.createElement("button")
  272. newButton.textContent = "Delete"
  273. newButton.className = "float-cmt-btn btn-font"
  274. newButton.id = "delCmtBtn"
  275. newButton.addEventListener("click", async () => {
  276. if(confirm("Are you sure you want to delete your comment?")){
  277. if((await GM.getValue(newURL, "noCmtHere")) !== "noCmtHere"){
  278. await GM.deleteValue(newURL)
  279. document.querySelector(".float-box").value = ""
  280. document.querySelector("textarea[id^='comment_content_for']").value = ""
  281. }
  282. }
  283. })
  284. return newButton
  285. }
  286.  
  287. const chapterRadio = () => {
  288. const radioDiv = document.createElement("div")
  289. radioDiv.className = "radio-div"
  290. const radioOne = document.createElement("input")
  291. const radioTwo = document.createElement("input")
  292. radioOne.type = "radio"
  293. radioTwo.type = "radio"
  294. radioOne.name = "chapters"
  295. radioTwo.name = "chapters"
  296. radioOne.className = "chapter-toggle"
  297. radioTwo.className = "chapter-toggle"
  298. radioOne.id = "entireCmt"
  299. radioTwo.id = "chapterCmt"
  300. const labelOne = document.createElement("label")
  301. const labelTwo = document.createElement("label")
  302. labelOne.setAttribute("for", "entireCmt")
  303. labelTwo.setAttribute("for","chapterCmt")
  304. labelOne.textContent = "Full Work"
  305. labelTwo.textContent = "By Chapter"
  306.  
  307. if(curURL.includes("chapters")){
  308. radioOne.checked = false
  309. radioTwo.checked = true
  310. } else {
  311. radioDiv.style.display = "none"
  312. radioOne.disabled = true
  313. radioTwo.disabled = true
  314. }
  315.  
  316. radioOne.addEventListener("click", () => {
  317. if(newURL.includes("chapters")){
  318. newURL = curURL.slice(0,curURL.indexOf("/chapters"))
  319. addStoredText()
  320. }
  321. })
  322. radioTwo.addEventListener("click", () => {
  323. if(!newURL.includes("chapters")){
  324. newURL = curURL
  325. addStoredText()
  326. }
  327.  
  328. })
  329. radioDiv.appendChild(radioOne)
  330. radioDiv.appendChild(labelOne)
  331. radioDiv.appendChild(radioTwo)
  332. radioDiv.appendChild(labelTwo)
  333. return radioDiv
  334. }
  335.  
  336. const addButton = () => {
  337. const newButton = document.createElement("button")
  338. newButton.textContent = "Add to Comment Box"
  339. newButton.className = "float-cmt-btn btn-font"
  340. newButton.id = "addCmtBtn"
  341. const realCmtBox = document.querySelector("textarea[id^='comment_content_for']")
  342. newButton.addEventListener("click", async () => {
  343. realCmtBox.value = document.querySelector(".float-box").value
  344. newButton.style.background = success
  345. newButton.textContent = "Added to Comment Box"
  346. })
  347. return newButton
  348. }
  349.  
  350. const insertButton = () => {
  351. const newButton = document.createElement("button")
  352. newButton.textContent = "Insert Selection"
  353. newButton.className = "float-cmt-btn btn-font"
  354. newButton.id = "insCmtBtn"
  355. newButton.addEventListener("click", async () => {
  356. const selection = `<blockquote><i>${window.getSelection().toString().trim()}</i></blockquote>`
  357. const textBox = document.querySelector(".float-box")
  358. const newText = `${textBox.value}${selection}\n`
  359. textBox.value = newText
  360. await GM.setValue(newURL, newText)
  361. })
  362. return newButton
  363. }
  364.  
  365. const addStoredText = async () => {
  366. const textBox = document.querySelector(".float-box")
  367. if(curURL.includes("full")){
  368. newURL = curURL.slice(0, curURL.indexOf("?"))
  369. }
  370. const storedText = await GM.getValue(newURL,"")
  371. textBox.value = storedText
  372. }
  373.  
  374.  
  375. const init = () => {
  376. const body = document.body
  377. body.appendChild(createButton())
  378. body.appendChild(addStyles())
  379. body.appendChild(createMainDiv())
  380. addStoredText()
  381. }
  382.  
  383. init()