Soundgasm Improvements

Restyles and adds new functionality to Soundgasm --- dark mode/keyboard shortcuts/quick download/and more

  1. // ==UserScript==
  2. // @name Soundgasm Improvements
  3. // @namespace V.L
  4. // @version 1.0
  5. // @description Restyles and adds new functionality to Soundgasm --- dark mode/keyboard shortcuts/quick download/and more
  6. // @author Valerio Lyndon
  7. // @homepageURL https://github.com/ValerioLyndon/Soundgasm-Improvements
  8. // @supportURL https://github.com/ValerioLyndon/Soundgasm-Improvements/issues
  9. // @license GPL-3.0-only
  10. // @match https://soundgasm.net/*
  11. // @run-at document-start
  12. // @grant GM_getValue
  13. // @grant GM_setValue
  14. // ==/UserScript==
  15.  
  16. 'use strict';
  17.  
  18. document.addEventListener("DOMContentLoaded", domLoaded);
  19.  
  20. // Dark or Light mode
  21.  
  22. const theme = GM_getValue('theme', 'dark');
  23. document.documentElement.classList.add(theme);
  24.  
  25. // CSS
  26.  
  27. const css = document.createElement('style');
  28. css.textContent = `
  29. html {
  30. font-size: 1px;
  31.  
  32. --icons: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAAGQCAYAAADr8i+wAAAACXBIWXMAAAsTAAALEwEAmpwYAAAMjmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOmI2Mjc5ZTQ3LTNiYzAtM2Q0ZC1iNzUyLWRiNmM4OGU5MDliYyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo5N2EzMzA5ZC03NWE4LWRhNDktYjQyNi04NmU1ZDBlM2ZiZTgiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0iOTc4MTgzNEJDNEFBMTFCODE1QzQzMjUyQkJGQkIwNUEiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0iIiB0aWZmOkltYWdlV2lkdGg9IjIwMCIgdGlmZjpJbWFnZUxlbmd0aD0iNDAwIiB0aWZmOlBob3RvbWV0cmljSW50ZXJwcmV0YXRpb249IjIiIHRpZmY6U2FtcGxlc1BlclBpeGVsPSIzIiB0aWZmOlhSZXNvbHV0aW9uPSIxMDAvMSIgdGlmZjpZUmVzb2x1dGlvbj0iMTAwLzEiIHRpZmY6UmVzb2x1dGlvblVuaXQ9IjEiIGV4aWY6RXhpZlZlcnNpb249IjAyMjEiIGV4aWY6Q29sb3JTcGFjZT0iNjU1MzUiIGV4aWY6UGl4ZWxYRGltZW5zaW9uPSIyMDAiIGV4aWY6UGl4ZWxZRGltZW5zaW9uPSI0MDAiIHhtcDpDcmVhdGVEYXRlPSIyMDIyLTAyLTA4VDIwOjExOjU5LTA4OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMi0wMi0wOFQyMTowMDoyMS0wODowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMi0wMi0wOFQyMTowMDoyMS0wODowMCI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjM5ZmM5YWM1LTg2NWMtZjU0Mi05MDM5LTUyNTRmMDg3OTA3MSIgc3RFdnQ6d2hlbj0iMjAyMi0wMi0wOFQyMDoxNzoyNS0wODowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY29udmVydGVkIiBzdEV2dDpwYXJhbWV0ZXJzPSJmcm9tIGltYWdlL2pwZWcgdG8gYXBwbGljYXRpb24vdm5kLmFkb2JlLnBob3Rvc2hvcCIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iZGVyaXZlZCIgc3RFdnQ6cGFyYW1ldGVycz0iY29udmVydGVkIGZyb20gaW1hZ2UvanBlZyB0byBhcHBsaWNhdGlvbi92bmQuYWRvYmUucGhvdG9zaG9wIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo3ZWU4ZTcwNS1kYTBjLTI4NDMtYTQ1My05Y2U4ZDdiMjk0ZTIiIHN0RXZ0OndoZW49IjIwMjItMDItMDhUMjA6MTc6MjUtMDg6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDQyAoV2luZG93cykiIHN0RXZ0OmNoYW5nZWQ9Ii8iLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjk4ZTlmMDE1LTg3OGMtODc0Yi04MjBkLWE0NjdjZjEzMTZkOCIgc3RFdnQ6d2hlbj0iMjAyMi0wMi0wOFQyMTowMDoyMS0wODowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY29udmVydGVkIiBzdEV2dDpwYXJhbWV0ZXJzPSJmcm9tIGFwcGxpY2F0aW9uL3ZuZC5hZG9iZS5waG90b3Nob3AgdG8gaW1hZ2UvcG5nIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJkZXJpdmVkIiBzdEV2dDpwYXJhbWV0ZXJzPSJjb252ZXJ0ZWQgZnJvbSBhcHBsaWNhdGlvbi92bmQuYWRvYmUucGhvdG9zaG9wIHRvIGltYWdlL3BuZyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6OTdhMzMwOWQtNzVhOC1kYTQ5LWI0MjYtODZlNWQwZTNmYmU4IiBzdEV2dDp3aGVuPSIyMDIyLTAyLTA4VDIxOjAwOjIxLTA4OjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDwvcmRm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);
  33. }
  34.  
  35. html.dark {
  36. --background: hsl(0, 0%, 6.5%);
  37. --foreground-1: hsl(0, 0%, 12%);
  38. --foreground-bar-2: hsl(0, 0%, 15%);
  39. --foreground-bar: hsl(0, 0%, 27%);
  40. --foreground-2: hsl(0, 0%, 17.6%);
  41. --border: var(--foreground-bar-2);
  42. --text-low: hsl(0, 0%, 65%);
  43. --text-medium: hsl(0, 0%, 80%);
  44. --text-high: hsl(0, 0%, 98%);
  45. --accent: hsl(310, 30%, 30%);
  46. }
  47. html.light {
  48. --background: hsl(0, 0%, 96%);
  49. --foreground-1: hsl(0, 0%, 100%);
  50. --foreground-bar-2: hsl(0, 0%, 13.3%);
  51. --foreground-bar: hsl(0, 0%, 9%);
  52. --foreground-2: hsl(0, 0%, 94%);
  53. --border: hsl(0, 0%, 94%);
  54. --text-low: hsl(0, 0%, 25%);
  55. --text-medium: hsl(0, 0%, 7%);
  56. --text-high: hsl(0, 0%, 0%);
  57. --accent: hsl(310, 30%, 70%);
  58. }
  59.  
  60. html body {
  61. min-width: 424rem;
  62. max-width: 1200rem;
  63. padding: 40rem 20rem;
  64. margin: 0 auto;
  65. background: var(--background);
  66. font-size: 12rem;
  67. color: var(--text-low);
  68. }
  69.  
  70. a {
  71. color: var(--text-medium) !important;
  72. text-decoration: none;
  73. } a:hover {
  74. color: var(--text-high) !important;
  75. }
  76.  
  77. html *::selection {
  78. background-color: var(--accent);
  79. }
  80.  
  81. body input,
  82. body textarea {
  83. background: var(--foreground-2);
  84. border: 1px solid var(--border);
  85. color: var(--text-medium);
  86. resize: vertical;
  87. }
  88.  
  89. body input[type="submit"]:hover,
  90. body input[type="submit"]:active {
  91. cursor: pointer;
  92. border-color: var(--accent);
  93. }
  94.  
  95. /* Header */
  96.  
  97. body header {
  98. min-height: 20rem;
  99. padding-bottom: 40rem;
  100. text-align: center;
  101. }
  102. nav a {
  103. display: inline-block;
  104. }
  105. body .logo {
  106. display: none;
  107. }
  108.  
  109. nav a[href="https://soundgasm.net/logout"] {
  110. font-size: 0;
  111. }
  112. nav a[href="https://soundgasm.net/logout"]::before {
  113. content: "Logout";
  114. font-size: 16px;
  115. }
  116.  
  117. /* Multiple-page rules */
  118.  
  119. body #container,
  120. .vl-container,
  121. body .sound-details,
  122. #jp_container_1,
  123. body .uploadform,
  124. body .contactform,
  125. body .loginform,
  126. body .signupform,
  127. body .passwordresetform,
  128. .vl-sidebar {
  129. background: var(--foreground-1);
  130. box-shadow:
  131. 0 2rem 4rem var(--background),
  132. 0 4rem 10rem hsla(0,0%,0%,10%);
  133. border-color: var(--border);
  134. margin: 0 auto;
  135. }
  136.  
  137. #container h1,
  138. body h1 {
  139. border-color: var(--border);
  140. color: var(--text-low);
  141. }
  142.  
  143. /* Generic Containers */
  144.  
  145. body p.footer {
  146. border-color: var(--border);
  147. }
  148.  
  149. body #container {
  150. max-width: 800rem;
  151. }
  152.  
  153. .vl-container {
  154. max-width: 770rem;
  155. padding: 15rem;
  156. border-size: 1rem;
  157. margin: 30rem auto;
  158. }
  159.  
  160. .vl-container-header {
  161. padding-bottom: 10rem;
  162. border-bottom: 1px solid var(--border);
  163. margin: 0 0 14rem;
  164. font-size: 16px;
  165. font-weight: normal;
  166. }
  167.  
  168. .vl-column {
  169. display: flex;
  170. flex-flow: column nowrap;
  171. gap: 5rem 10rem;
  172. margin-bottom: 5rem;
  173. align-items: start;
  174. }
  175.  
  176. .vl-link {
  177. color: var(--text-medium);
  178. }
  179. .vl-link:hover {
  180. color: var(--text-high);
  181. text-decoration: underline;
  182. }
  183.  
  184.  
  185. .vl-paragraph {
  186. margin: 0 0 5rem;
  187. font-size: 12rem;
  188. line-height: 1.35;
  189. }
  190.  
  191. /* Home Page */
  192.  
  193. .vl-user-list {
  194. display: grid;
  195. grid-template-columns: repeat(4, 1fr);
  196. gap: 5rem;
  197. justify-items: start;
  198. align-items: start;
  199. }
  200.  
  201. /* User Page */
  202.  
  203. body .sound-details {
  204. display: grid;
  205. width: calc(100% - 22rem);
  206. padding: 10rem;
  207. border-radius: 4rem;
  208. margin: 0 auto 12rem;
  209. grid-template-columns: 1fr 70rem;
  210. grid-template-rows: auto auto;
  211. grid-template-areas:
  212. "title plays"
  213. "description description";
  214. grid-auto-flow: column;
  215. }
  216.  
  217.  
  218. .sound-details > a {
  219. grid-area: title;
  220. justify-self: start;
  221. font-size: 16rem;
  222. font-weight: bold;
  223. white-space: normal;
  224. }
  225.  
  226. .playCount {
  227. grid-area: plays;
  228. max-width: 70rem;
  229. margin-left: auto;
  230. text-align: right;
  231. }
  232.  
  233. .playCount::before {
  234. content: "";
  235. display: inline-block;
  236. border-color: transparent;
  237. border-left-color: var(--text-low);
  238. border-style: solid;
  239. border-width: .45em .65em;
  240. margin-right: -0.4em;
  241. vertical-align: middle;
  242. }
  243.  
  244. .soundDescription {
  245. grid-area: description;
  246. order: 3;
  247. width: 100%;
  248. margin-top: 6rem;
  249. }
  250.  
  251. /* Split Content + Sidebar */
  252.  
  253. .vl-split {
  254. display: grid;
  255. max-width: calc(640rem + 20rem + 240rem);
  256. gap: 20rem;
  257. margin: 0 auto;
  258. }
  259.  
  260. .vl-directory {
  261. display: grid;
  262. width: 100vw;
  263. max-width: 620rem;
  264. grid-auto-flow: row;
  265. margin: 0 auto;
  266. }
  267.  
  268. .vl-sidebar {
  269. padding: 10rem;
  270. border-radius: 4rem;
  271. overflow-y: auto;
  272. }
  273.  
  274. @media (max-width: 941px) {
  275. .vl-split {
  276. grid-auto-flow: row;
  277. }
  278. .vl-sidebar {
  279. max-width: 600rem;
  280. max-height: 80vh;
  281. border: 1px solid var(--border);
  282. }
  283. }
  284. @media (min-width: 940px) {
  285. .vl-split {
  286. grid-auto-flow: column;
  287. align-items: start;
  288. }
  289. .vl-sidebar {
  290. position: sticky;
  291. top: 20rem;
  292. width: 220rem;
  293. max-height: calc(100vh - 160rem);
  294. order: 1;
  295. }
  296. }
  297.  
  298. /* Filters */
  299.  
  300. .vl-hidden,
  301. .vl-hidden-by-search,
  302. .vl-hidden-by-tag {
  303. display: none !important;
  304. }
  305.  
  306. .vl-filters {
  307. font-size: 12rem;
  308. }
  309.  
  310. .vl-filter-section {
  311. margin-bottom: 15rem;
  312. }
  313.  
  314. .vl-filter-header {
  315. border-bottom: 1rem solid var(--border);
  316. margin: 0 0 10rem;
  317. font-size: 14rem;
  318. line-height: 1.25em;
  319. font-weight: bold;
  320. }
  321.  
  322. .vl-sort-btn::before {
  323. content: "• ";
  324. }
  325.  
  326. .vl-sort-btn.is-active {
  327. font-weight: bold;
  328. }
  329. .vl-sort-btn.is-active::after {
  330. content: attr(data-direction);
  331. color: var(--text-low);
  332. font-size: 10rem;
  333. margin-left: 5rem;
  334. }
  335.  
  336. .vl-search {
  337. padding: 4rem;
  338. border-radius: 3px;
  339. margin: 0 0 5rem;
  340. }
  341.  
  342. .vl-tag-list {
  343. width: 100%;
  344. text-align: left;
  345. }
  346. .vl-tag-list th {
  347. font-size: 12rem;
  348. }
  349. .vl-tag-list tr > *:last-child {
  350. text-align: right;
  351. }
  352.  
  353. .vl-tag-btn {
  354. display: inline;
  355. padding: 2rem 4rem;
  356. background: none;
  357. border: none;
  358. border-radius: 2.5rem;
  359. color: var(--text-medium);
  360. font-size: 11rem;
  361. text-align: left;
  362. text-transform: capitalize;
  363. cursor: pointer;
  364. }
  365. .vl-tag-btn.is-active,
  366. .vl-tag-btn:hover {
  367. background: var(--foreground-2);
  368. color: var(--text-high);
  369. }
  370. .vl-tag-btn.is-active {
  371. font-weight: bold;
  372. }
  373.  
  374. .vl-tag-count {
  375. padding: 1rem;
  376. font-size: 12rem;
  377. }
  378.  
  379. /* Player Page */
  380.  
  381. div[style="margin:10px 0"] {
  382. margin: 0 0 25rem !important;
  383. font-size: 18rem;
  384. text-align: center;
  385. }
  386.  
  387. #jp_container_1,
  388. .jp-audio .jp-audio-stream,
  389. .jp-audio .jp-video {
  390. border: 2rem solid var(--border);
  391. color: var(--text-low);
  392. }
  393. #jp_container_1 {
  394. width: 420rem;
  395. }
  396. .jp-interface {
  397. background: var(--foreground-bar);
  398. }
  399. .jp-audio .jp-details {
  400. background: var(--foreground-bar-2);
  401. }
  402. .jp-details .jp-title {
  403. font-size: 12rem;
  404. }
  405. .light .jp-details .jp-title {
  406. color: var(--background);
  407. }
  408. .jp-description {
  409. padding: 0 10rem;
  410. font-size: 12rem;
  411. }
  412.  
  413. /* Player */
  414.  
  415. .jp-state-muted .jp-unmute {
  416. background: url("../image/jplayer.blue.monday.jpg") -60px -170px no-repeat;
  417. }
  418. .jp-state-muted .jp-unmute:focus {
  419. background: url("../image/jplayer.blue.monday.jpg") -79px -170px no-repeat;
  420. }
  421.  
  422. #jp_container_1 button,
  423. .jp-gui .jp-seek-bar,
  424. .jp-gui .jp-play-bar,
  425. .jp-gui .jp-volume-bar,
  426. .jp-gui .jp-volume-bar-value {
  427. background-image: var(--icons);
  428. }
  429.  
  430. .jp-gui .jp-progress {
  431. background: none;
  432. border-radius: 2.5rem;
  433. }
  434.  
  435. .jp-progress .jp-seeking-bg {
  436. background: var(--icons) 0 -202px repeat-x;
  437. animation: seeking .8s ease-in-out infinite alternate;
  438. }
  439. @keyframes seeking {
  440. 0% {
  441. opacity: 1;
  442. }
  443. 100% {
  444. opacity: 0.3;
  445. }
  446. }
  447.  
  448. .jp-gui .jp-volume-controls {
  449. width: 0;
  450. }
  451.  
  452. .dark .jp-current-time, .dark .jp-duration {
  453. color: var(--text-medium);
  454. }
  455. .light .jp-current-time, .light .jp-duration {
  456. color: var(--background);
  457. }
  458.  
  459. /* Description */
  460.  
  461. .vl-desc-container {
  462. margin: 12rem 0 0;
  463. }
  464. .sound-details .vl-desc-container {
  465. display: inline;
  466. margin: 0;
  467. }
  468.  
  469. .vl-desc-new, .vl-desc-raw {
  470. margin: 12rem 0;
  471. }
  472. .sound-details .vl-desc-new, .sound-details .vl-desc-raw:not([style*="none"]) {
  473. display: inline !important;
  474. white-space: normal;
  475. }
  476.  
  477. .vl-tag {
  478. display: inline-block;
  479. padding: 2rem 4rem;
  480. background: var(--foreground-2);
  481. border-radius: 2.5rem;
  482. margin: 0 4rem 4rem 0;
  483. color: var(--text-medium);
  484. font-size: 11rem;
  485. text-transform: capitalize;
  486. }
  487.  
  488. .vl-showraw {
  489. display: inline-block;
  490. opacity: 0.5;
  491. }
  492. .vl-showraw:hover {
  493. opacity: 1;
  494. }
  495. .jp-audio .vl-showraw {
  496. margin-bottom: 12rem;
  497. }
  498. .sound-details .vl-showraw {
  499. float: right;
  500. }
  501.  
  502. /* Contact page */
  503.  
  504. header + ul {
  505. width: 414rem;
  506. padding-left: 16rem;
  507. margin: 12rem auto;
  508. word-break: break-word;
  509. }
  510.  
  511.  
  512. /* Footer */
  513.  
  514. .vl-footer {
  515. width: 420rem;
  516. margin: 0 auto;
  517. text-align: center;
  518. padding-top: 30rem;
  519. }
  520.  
  521. .vl-footer a {
  522. padding: 0 15rem;
  523. }
  524.  
  525.  
  526. /* fixes */
  527. .patreon-widget {
  528. width: 300px !important;
  529. height: 36px !important;
  530. }
  531. `;
  532.  
  533. document.documentElement.appendChild(css);
  534.  
  535. // Functions & Classes
  536.  
  537. function paragraph( text ){
  538. let p = document.createElement('p');
  539. p.className = 'vl-paragraph';
  540. p.textContent = text;
  541. return p;
  542. }
  543.  
  544. var users = new class UserDatabase {
  545. constructor( ){
  546. let storage = GM_getValue('knownUsernames', '[]');
  547. this.names = JSON.parse(storage);
  548. }
  549.  
  550. generateUserList( ){
  551. }
  552.  
  553. add( name ){
  554. let index = this.names.indexOf(name);
  555. if( index === -1 ){
  556. this.names.push(name);
  557. this.save();
  558. }
  559. }
  560.  
  561. remove( name ){
  562. let index = this.names.indexOf(name);
  563. if( index > -1 ){
  564. this.names.splice(index, 1);
  565. this.save();
  566. }
  567. }
  568.  
  569. save( ){
  570. console.log('saving', JSON.stringify(this.names));
  571. GM_setValue('knownUsernames', JSON.stringify(this.names));
  572. }
  573. }
  574.  
  575. class AudioListing {
  576. constructor({ element, titleSelector, descriptionSelector, playCountSelector = false, order = 0 }){
  577. // Process description
  578. let titleDestination = element.querySelector(titleSelector);
  579. let descDestination = element.querySelector(descriptionSelector);
  580. let desc = descDestination.textContent;
  581. let title = titleDestination.textContent;
  582. let originalTitle = title;
  583. let originalDesc = desc;
  584. let processedTitleDiv = document.createElement('span');
  585. let rawTitleDiv = document.createElement('span');
  586. let processedDescDiv = document.createElement('div');
  587. let rawDescDiv = document.createElement('p');
  588. let tagsDiv = document.createElement('div');
  589. let descDiv = document.createElement('p');
  590. let tags = new Set();
  591. // match all words inside brackets [] {}. also matches parentheses () but only for one-word sections to try and avoid false positives
  592. const extractTagsRegex = /[\[\{](.*?)[\]\}]|\(([^\s]+)\)/g;
  593. // same as the extraction regex but with extra whitespace matching rules
  594. const removeTagsRegex = /\s*(?:[\[\{].*?[\]\}]|\([^\s]+\))\s*/g;
  595.  
  596. rawTitleDiv.textContent = title;
  597. rawTitleDiv.style.display = 'none';
  598.  
  599. processedDescDiv.classList.add('vl-desc-container');
  600. tagsDiv.classList.add('vl-tags');
  601. descDiv.classList.add('vl-desc-new');
  602. processedDescDiv.appendChild(tagsDiv);
  603. processedDescDiv.appendChild(descDiv);
  604.  
  605. rawDescDiv.classList.add('vl-desc-raw');
  606. rawDescDiv.textContent = desc;
  607. rawDescDiv.style.display = 'none';
  608.  
  609. let combined = title + desc;
  610. var tagMatches = combined.matchAll(extractTagsRegex);
  611.  
  612. for( let match of tagMatches ){
  613. let tag = match[1] === undefined ? match[2] : match[1];
  614. if( tag.length > 0 ){
  615. tags.add(tag);
  616. }
  617. }
  618.  
  619. // remove tags from text
  620. title = title.replaceAll(removeTagsRegex, '')
  621. desc = desc.replaceAll(removeTagsRegex, '')
  622.  
  623. // sort the tags by length
  624. tags = Array.from(tags);
  625. tags.sort( (a, b) => { return a.length - b.length; } );
  626.  
  627. // create the element
  628. for( let i = 0; i < tags.length; i++ ){
  629. var tagSpan = document.createElement('span');
  630. tagSpan.classList.add('vl-tag');
  631. tagSpan.textContent = tags[i];
  632. tagsDiv.appendChild(tagSpan);
  633. }
  634.  
  635. // Create "view raw" button
  636. var viewRawBtn = document.createElement('a');
  637. viewRawBtn.href = 'javascript:void(0);';
  638. viewRawBtn.classList.add('vl-showraw');
  639. viewRawBtn.textContent = 'Show raw.'
  640. viewRawBtn.onclick = ()=>{
  641. if( processedDescDiv.style.display === 'none' ){
  642. processedTitleDiv.style.display = 'inline';
  643. rawTitleDiv.style.display = 'none';
  644. processedDescDiv.style.display = 'block';
  645. rawDescDiv.style.display = 'none';
  646. viewRawBtn.textContent = 'Show raw.';
  647. } else {
  648. processedTitleDiv.style.display = 'none';
  649. rawTitleDiv.style.display = 'inline';
  650. processedDescDiv.style.display = 'none';
  651. rawDescDiv.style.display = 'block';
  652. viewRawBtn.textContent = 'Show processed.';
  653. }
  654. }
  655.  
  656. // finish up with tags & description
  657. descDiv.textContent = desc.trim();
  658. processedTitleDiv.textContent = title.trim();
  659.  
  660. // Add everything back to DOM unless it is identical
  661. if( title !== originalTitle || desc !== originalDesc ){
  662. titleDestination.replaceChildren(processedTitleDiv, rawTitleDiv);
  663. descDestination.replaceChildren(processedDescDiv, rawDescDiv, viewRawBtn);
  664. }
  665.  
  666. // parse play count and change html
  667. let plays = false;
  668. if( playCountSelector ){
  669. // this if else and element.append are place here due to weird HTML bugs on the default website
  670. let playElement = element.querySelector(playCountSelector);
  671. if( !playElement ){
  672. plays = 0;
  673. playElement = document.createElement('span');
  674. playElement.className = 'playCount';
  675. playElement.textContent = 'unknown';
  676. }
  677. else {
  678. plays = playElement.textContent.split(': ')[1];
  679.  
  680. let playText = String(plays);
  681. if( plays.length > 3 ) {
  682. playElement.title = `played ${plays} times`;
  683. playText = playText.substring(0, plays.length - 3) + 'k';
  684. }
  685. playElement.textContent = playText;
  686. }
  687. element.append(playElement);
  688. }
  689.  
  690. // Assign variables for use in AudioDirectory classes
  691. this.element = element;
  692. this.title = title;
  693. this.description = desc;
  694. this.plays = plays;
  695. this.order = order;
  696. this.tags = tags;
  697. }
  698. }
  699.  
  700. class AudioDirectory {
  701. constructor({ elements, titleSelector, descriptionSelector, playCountSelector = false, filterElement = false }){
  702. this.audios = []; // used to iterate through audio info
  703. this.tags = {}; // used as a reference for which items have which tags
  704. this.selectedTags = []; // used to keep track of current filters
  705. this.availableElements = []; // used to update tag counts in the sidebar
  706. this.tagButtons = []; // used to update tag counts in the sidebar
  707.  
  708. // process all audios
  709. for( let index = 0; index < elements.length; index++ ){
  710. let audio = new AudioListing({
  711. element: elements[index],
  712. titleSelector: titleSelector,
  713. descriptionSelector: descriptionSelector,
  714. playCountSelector: playCountSelector,
  715. order: index
  716. });
  717. this.audios.push(audio);
  718. for( let tag of audio.tags ){
  719. let tagName = tag.toLowerCase();
  720. if(!( tagName in this.tags )){ this.tags[tagName] = []; }
  721. this.tags[tagName].push(audio.element);
  722. }
  723. }
  724.  
  725. // intialise filters and sorting
  726. if( filterElement ){
  727. // set up DOM
  728. this.filterElement = filterElement;
  729. this.filterElement.classList.add('vl-filters');
  730.  
  731. this.sortElement = document.createElement('div');
  732. this.sortElement.className = 'vl-filter-section';
  733. this.searchElement = document.createElement('div');
  734. this.searchElement.className = 'vl-filter-section';
  735. this.tagElement = document.createElement('div');
  736. this.tagElement.className = 'vl-filter-section';
  737.  
  738. let sortHeader = document.createElement('h6');
  739. sortHeader.className = 'vl-filter-header';
  740. sortHeader.textContent = 'Sort by...';
  741. this.sortElement.append(sortHeader);
  742.  
  743. let searchHeader = document.createElement('h6');
  744. searchHeader.className = 'vl-filter-header';
  745. searchHeader.textContent = 'Search...';
  746. this.searchElement.append(searchHeader);
  747.  
  748. let tagHeader = document.createElement('h6');
  749. tagHeader.className = 'vl-filter-header';
  750. tagHeader.textContent = 'Filter by tag...';
  751. this.tagElement.append(tagHeader);
  752.  
  753. this.sortList = document.createElement('div');
  754. this.sortList.className = 'vl-column';
  755. this.sortElement.append(this.sortList);
  756.  
  757. this.tagList = document.createElement('table');
  758. this.tagList.className = 'vl-tag-list';
  759. this.tagList.insertAdjacentHTML("afterbegin", `
  760. <tr><th>Name</th><th>Count</th></tr>
  761. `);
  762. this.tagElement.append(this.tagList);
  763.  
  764. // set up sorting
  765. this.calculatedSorts = {};
  766. this.sortButtons = {};
  767.  
  768. this.createSortButton('Title', 'title', 'ascending');
  769. this.createSortButton('Play Count', 'plays', 'descending');
  770. this.createSortButton('Date', 'order', 'descending');
  771.  
  772. this.sort();
  773.  
  774. // set up search
  775.  
  776. this.searchBar = document.createElement('input');
  777. this.searchBar.type = 'search';
  778. this.searchBar.className = 'vl-search';
  779. this.searchBar.placeholder = 'Search here.';
  780. this.searchElement.append(this.searchBar);
  781. this.searchElement.append(paragraph(`Search supports some basic operators. For example: "phrase" to require an exact string or word and -word to excluse a word. Un-quoted words are treated as OR.`));
  782.  
  783. this.searchBar.addEventListener('input', ()=>{ this.search() });
  784. this.searchTimeout = setTimeout(null, 0);
  785.  
  786. // set up tag filters
  787.  
  788. let sortedTags = Object.keys(this.tags).sort((first,second)=>{
  789. // sort primarily by number of elements that match the tag with a secondary alphabetical sort
  790. let tagCount = this.tags[second].length - this.tags[first].length
  791. let tagName = first < second ? -1 : first > second ? 1 : 0;
  792. return tagCount === 0 ? tagName : tagCount;
  793. });
  794. for( let tag of sortedTags ){
  795. this.createTagButton(tag);
  796. }
  797.  
  798. // Append all workspace items to DOM
  799. this.filterElement.append(this.sortElement, this.searchElement, this.tagElement);
  800. }
  801.  
  802. if( Object.keys(this.tags).length === 0 ){
  803. this.tagElement.remove();
  804. }
  805. }
  806.  
  807. createSortButton( title, column, direction ){
  808. let button = document.createElement('a');
  809. button.href = 'javascript:void(0);';
  810. button.textContent = title;
  811. button.className = 'vl-sort-btn';
  812. button.dataset.column = column;
  813. button.dataset.direction = direction;
  814.  
  815. button.addEventListener('click', ()=>{
  816. this.sort(column, direction);
  817. });
  818. this.sortList.append(button);
  819. this.sortButtons[column] = button;
  820. }
  821.  
  822. createTagButton( tag ){
  823. let row = document.createElement('tr');
  824. let cell1 = document.createElement('td');
  825. let cell2 = document.createElement('td');
  826.  
  827. let button = document.createElement('button');
  828. button.type = 'button';
  829. button.textContent = tag;
  830. button.className = 'vl-tag-btn';
  831. cell1.append(button)
  832.  
  833. let count = this.tags[tag].length;
  834. let countElement = document.createElement('span');
  835. countElement.className = 'vl-tag-count';
  836. countElement.textContent = count;
  837. cell2.append(countElement);
  838. row.append(cell1, cell2);
  839. this.tagList.append(row);
  840.  
  841. button.addEventListener('click', ()=>{
  842. let selected = this.selectedTags.indexOf(tag);
  843. if( selected > -1 ){
  844. button.classList.remove('is-active');
  845. this.selectedTags.splice(selected, 1);
  846. }
  847. else {
  848. button.classList.add('is-active');
  849. this.selectedTags.push(tag);
  850. }
  851. this.applySelectedTags();
  852. });
  853. this.tagButtons.push({
  854. 'element': row,
  855. 'tag': tag,
  856. 'countElement': countElement
  857. });
  858. }
  859.  
  860. sort( column = 'order', direction = 'descending' ){
  861. // flip direction if already sorting this way
  862. if( this?.sorted?.column === column && this?.sorted?.direction === direction ){
  863. direction = direction === 'descending' ? 'ascending' : 'descending';
  864. }
  865.  
  866. this.sorted = { column, direction };
  867.  
  868. for( let button of Object.values(this.sortButtons) ){
  869. if( button.dataset.column === column ){
  870. button.classList.add('is-active');
  871. button.dataset.direction = direction;
  872. }
  873. else {
  874. button.classList.remove('is-active');
  875. }
  876. }
  877.  
  878. // if list was already sorted once, just re-use the previous sort
  879. let array = [];
  880. if( `${column}-${direction}` in this.calculatedSorts ){
  881. array = this.calculatedSorts[`${column}-${direction}`];
  882. }
  883.  
  884. // if not sorted yet, choose correct function and sort
  885. else {
  886. // 'order' column gets sorted in reverse due to being front-facingly labelled as date
  887. let sortFunction = () => { throw new Error('unknown sort'); };
  888. if( column === 'plays' && direction === 'ascending'
  889. || column === 'order' && direction === 'descending' ){
  890. sortFunction = (first, second) => { return first['value'] - second['value']; };
  891. }
  892. else if( column === 'plays' && direction === 'descending'
  893. || column === 'order' && direction === 'ascending' ){
  894. sortFunction = (first, second) => { return second['value'] - first['value']; };
  895. }
  896. else if( column === 'title' && direction === 'ascending' ){
  897. sortFunction = (first, second) => {
  898. let a = first['value'];
  899. let b = second['value'];
  900. return (a < b) ? -1 : (a > b) ? 1 : 0;
  901. };
  902. }
  903. else if( column === 'title' && direction === 'descending' ){
  904. sortFunction = (first, second) => {
  905. let a = first['value'].toLowerCase();
  906. let b = second['value'].toLowerCase();
  907. return (b < a) ? -1 : (b > a) ? 1 : 0;
  908. };
  909. }
  910.  
  911. for( let audio of this.audios ){
  912. array.push({'element': audio.element, 'value': audio[column]});
  913. }
  914. array.sort(sortFunction);
  915.  
  916. this.calculatedSorts[`${column}-${direction}`] = array;
  917. }
  918.  
  919. // apply sort to items using CSS 'order' values
  920. for( let i = 0; i < array.length; i++ ){
  921. array[i]['element'].style.order = i;
  922. }
  923. }
  924.  
  925. search( ){
  926. clearTimeout(this.searchTimeout);
  927.  
  928. // parse query
  929. const exclusionRegex = /(?:^|\s)+-(\w+)/g;
  930. const phraseRegex = /"([^"]+)"/g;
  931. let query = this.searchBar.value.toLowerCase();
  932.  
  933. let failMatches = query.matchAll(exclusionRegex);
  934. let exclusions = Array.from(failMatches).map( match => match[1] );
  935. query = query.replaceAll(exclusionRegex, '').trim();
  936.  
  937. let phraseMatches = query.matchAll(phraseRegex);
  938. let phrases = Array.from(phraseMatches).map( match => match[1] );
  939. query = query.replaceAll(phraseRegex, '').trim();
  940.  
  941. let words = query.split(' ');
  942.  
  943. this.searchTimeout = setTimeout(()=>{
  944. for( let audio of this.audios ){
  945. if( this.passesSearch(audio, phrases, words, exclusions) ){
  946. audio.element.classList.remove('vl-hidden-by-search');
  947. this.updateAvailableElements(audio.element);
  948. }
  949. else {
  950. audio.element.classList.add('vl-hidden-by-search');
  951. this.updateAvailableElements(audio.element);
  952. }
  953. }
  954. this.updateTagCounts();
  955. }, 350);
  956. }
  957.  
  958. passesSearch( audioListing, phrases, words, exclusions ){
  959. console.log(phrases, words, exclusions);
  960. const title = audioListing.title.toLowerCase();
  961. const tags = audioListing.tags.join(' ').toLowerCase();
  962. const any = title + tags;
  963.  
  964. // cannot match any exclusions
  965. for( let str of exclusions ){
  966. if( any.includes(str) ){
  967. return false;
  968. }
  969. }
  970.  
  971. // must match all phrases
  972. for( let phrase of phrases ){
  973. if(! any.includes(phrase) ){
  974. return false;
  975. }
  976. }
  977.  
  978. // can match any word
  979. for( let word of words ){
  980. if( any.includes(word) ){
  981. return true;
  982. }
  983. }
  984. }
  985.  
  986. applySelectedTags( ){
  987. if( this.selectedTags.length === 0 ){
  988. for( let audio of this.audios ){
  989. audio.element.classList.remove('vl-hidden-by-tag');
  990. this.updateAvailableElements( audio.element );
  991. }
  992. this.updateTagCounts();
  993. return;
  994. }
  995.  
  996. for( let audio of this.audios ){
  997. let passesAllTags = true;
  998. for( let tag of this.selectedTags ){
  999. if(! this.tags[tag].includes(audio.element) ){
  1000. passesAllTags = false;
  1001. break;
  1002. }
  1003. }
  1004. if( passesAllTags ){
  1005. audio.element.classList.remove('vl-hidden-by-tag');
  1006. this.updateAvailableElements( audio.element );
  1007. }
  1008. else {
  1009. audio.element.classList.add('vl-hidden-by-tag');
  1010. this.updateAvailableElements( audio.element );
  1011. }
  1012. }
  1013. this.updateTagCounts();
  1014. }
  1015.  
  1016. updateAvailableElements( element ){
  1017. let index = this.availableElements.indexOf(element);
  1018. let hidden = element.classList.contains('vl-hidden-by-tag') | element.classList.contains('vl-hidden-by-search')
  1019. if( hidden && index > -1 ){
  1020. this.availableElements.splice(index, 1);
  1021. }
  1022. else if( !hidden && index === -1 ){
  1023. this.availableElements.push(element);
  1024. }
  1025. }
  1026.  
  1027. updateTagCounts( ){
  1028. for( let data of this.tagButtons ){
  1029. let tag = data.tag;
  1030. let availableCount = 0;
  1031. for( let element of this.availableElements ){
  1032. if( this.tags[tag].includes(element) ){
  1033. availableCount++;
  1034. }
  1035. }
  1036.  
  1037. if( availableCount > 0 ){
  1038. data.countElement.textContent = availableCount;
  1039. data.element.classList.remove('vl-hidden');
  1040. }
  1041. else {
  1042. data.element.classList.add('vl-hidden');
  1043. }
  1044. }
  1045. }
  1046. }
  1047.  
  1048. // Begin modifying page
  1049.  
  1050. function domLoaded() {
  1051. // If content is blank
  1052. let content = document.querySelector('body > div');
  1053. if( content === null ) {
  1054. let blank = document.createElement('div');
  1055. blank.id = 'container';
  1056. blank.innerHTML = `<div id="body"><p>There's nothing here.</p></div>`;
  1057. document.body.appendChild(blank);
  1058. }
  1059.  
  1060. // Add footer
  1061. let footer = document.createElement('footer');
  1062. footer.classList.add('vl-footer');
  1063.  
  1064. // Theme switcher
  1065. let themeSwitcher = document.createElement('a');
  1066. themeSwitcher.textContent = 'Theme';
  1067. themeSwitcher.href = 'javascript:void(0);';
  1068. themeSwitcher.onclick = function() {
  1069. if(GM_getValue('theme', 'dark') === 'dark') {
  1070. GM_setValue('theme', 'light');
  1071. document.documentElement.classList.add('light');
  1072. document.documentElement.classList.remove('dark');
  1073. } else {
  1074. GM_setValue('theme', 'dark');
  1075. document.documentElement.classList.add('dark');
  1076. document.documentElement.classList.remove('light');
  1077. }
  1078. };
  1079. footer.appendChild(themeSwitcher);
  1080.  
  1081. document.body.appendChild(footer);
  1082.  
  1083. var path = window.location.pathname;
  1084.  
  1085. // Per-page sections
  1086.  
  1087. // Any page with a user as long as it has content
  1088. if( content && path.startsWith('/u/') && path.split('/').length > 2 ){
  1089. let username = path.split('/')[2];
  1090. users.add(username);
  1091. }
  1092.  
  1093. // Homepage
  1094. if( path === '/' ){
  1095. document.querySelector('h1').textContent = 'Welcome to Soundgasm.net, improved!';
  1096.  
  1097. let container = document.createElement('div');
  1098. container.className = 'vl-container';
  1099. container.style.marginBottom = '0';
  1100. let header = document.createElement('h3');
  1101. header.className = 'vl-container-header';
  1102. header.textContent = 'Known user list.';
  1103. container.append(header);
  1104.  
  1105. if( users.names.length === 0 ){
  1106. container.append(paragraph(`The script will remember usernames from the audio and user pages you visit. Once you've opened a few, you can always come back here to find them all!`));
  1107. }
  1108. else {
  1109. let userList = document.createElement('div');
  1110. userList.classList.add('vl-user-list');
  1111. userList.style.fontSize = '14rem';
  1112.  
  1113. for( let username of users.names.sort() ){
  1114. let link = document.createElement('a');
  1115. link.href = `/u/${username}`;
  1116. link.textContent = `• ${username}`;
  1117. link.className = 'vl-link';
  1118. userList.append(link);
  1119. }
  1120.  
  1121. container.append(userList);
  1122. }
  1123. footer.insertAdjacentElement('beforebegin', container);
  1124. }
  1125.  
  1126. // User pages
  1127. if( content && path.startsWith('/u/') && path.split('/').length < 4 ){
  1128. // Prep DOM for filters
  1129. let container = document.createElement('div');
  1130. container.className = 'vl-split';
  1131.  
  1132. let directory = document.createElement('main');
  1133. directory.className = 'vl-directory';
  1134. let sidebar = document.createElement('aside');
  1135. sidebar.className = 'vl-sidebar';
  1136.  
  1137. let frag = new DocumentFragment();
  1138. let items = document.querySelectorAll('.sound-details');
  1139. for( let item of items ){
  1140. frag.append(item);
  1141. }
  1142. directory.append(frag);
  1143. container.append(sidebar, directory);
  1144. document.getElementsByTagName('footer')[0].insertAdjacentElement('beforebegin', container);
  1145. // catch any oddities such as patron links and other things from descriptions
  1146. let patreon = document.querySelector('.soundDescription .patreon-widget');
  1147. if( patreon ){
  1148. let container = document.createElement('div');
  1149. container.className = 'vl-container';
  1150. container.style.marginTop = '0';
  1151. container.style.width = 'calc(100% - 30rem)';
  1152. let header = document.createElement('h3');
  1153. header.className = 'vl-container-header';
  1154. header.textContent = 'Extra user info.';
  1155. container.append(header, patreon);
  1156.  
  1157. directory.style.order = '-1';
  1158. directory.prepend(container);
  1159. }
  1160.  
  1161. // Process audio listings
  1162. new AudioDirectory({
  1163. elements: items,
  1164. titleSelector: 'a',
  1165. descriptionSelector: '.soundDescription',
  1166. playCountSelector: '.playCount',
  1167. filterElement: sidebar
  1168. });
  1169. }
  1170.  
  1171. // Player page
  1172. if( path.startsWith('/u/') && path.split('/').length > 3 ){
  1173. // Add custom descriptions
  1174. new AudioListing({
  1175. element: document.querySelector('.jp-type-single'),
  1176. titleSelector: '.jp-title',
  1177. descriptionSelector: '.jp-description'
  1178. });
  1179.  
  1180. let stop = document.querySelector('.jp-stop');
  1181. let title = document.querySelector('.jp-title');
  1182. let author = document.querySelector('div[style="margin:10px 0"] a');
  1183. let audio = document.querySelector('audio');
  1184.  
  1185. // Keypress handler
  1186. function setKeybinds() {
  1187. window.addEventListener('keydown', (e) => {
  1188. let k = e.key.toLowerCase();
  1189. if(e.key === ' ') {
  1190. e.preventDefault();
  1191. }
  1192. });
  1193.  
  1194. window.addEventListener('keyup', (e) => {
  1195. let k = e.key.toLowerCase();
  1196. let ctrl = e.ctrlKey;
  1197.  
  1198. let time = 5.0;
  1199. if(ctrl){
  1200. time = 15.0;
  1201. }
  1202.  
  1203. if(k === 'p' || k === 'k' || k === ' ') {
  1204. if(!audio.paused) {
  1205. audio.pause();
  1206. } else {
  1207. audio.play();
  1208. }
  1209. }
  1210. else if(k === 's') {
  1211. stop.click();
  1212. }
  1213. else if(k === 'd') {
  1214. document.querySelector('.dl').click();
  1215. }
  1216. else if(k === 'arrowleft') {
  1217. audio.currentTime -= time;
  1218. }
  1219. else if(k === 'arrowright') {
  1220. audio.currentTime += time;
  1221. }
  1222. else if(k === 'arrowup') {
  1223. let newVol = audio.volume + 0.1;
  1224. if(newVol > 1) {
  1225. newVol = 1.0;
  1226. }
  1227. audio.volume = newVol;
  1228. }
  1229. else if(k === 'arrowdown') {
  1230. let newVol = audio.volume - 0.1;
  1231. if(newVol < 0) {
  1232. newVol = 0.0;
  1233. }
  1234. audio.volume = newVol;
  1235. }
  1236. else if(k === '0') {
  1237. audio.currentTime = 0.0;
  1238. }
  1239. else if(k === '1') {
  1240. audio.currentTime = audio.duration / 10;
  1241. }
  1242. else if(k === '2') {
  1243. audio.currentTime = audio.duration / 10 * 2;
  1244. }
  1245. else if(k === '3') {
  1246. audio.currentTime = audio.duration / 10 * 3;
  1247. }
  1248. else if(k === '4') {
  1249. audio.currentTime = audio.duration / 10 * 4;
  1250. }
  1251. else if(k === '5') {
  1252. audio.currentTime = audio.duration / 10 * 5;
  1253. }
  1254. else if(k === '6') {
  1255. audio.currentTime = audio.duration / 10 * 6;
  1256. }
  1257. else if(k === '7') {
  1258. audio.currentTime = audio.duration / 10 * 7;
  1259. }
  1260. else if(k === '8') {
  1261. audio.currentTime = audio.duration / 10 * 8;
  1262. }
  1263. else if(k === '9') {
  1264. audio.currentTime = audio.duration / 10 * 9;
  1265. }
  1266. });
  1267. }
  1268.  
  1269. // Download button
  1270. function addDownload() {
  1271. let audio = document.querySelector('audio');
  1272. let src = audio.getAttribute('src');
  1273. let ext = src.split('.').pop();
  1274. let dl = document.createElement('a');
  1275.  
  1276. dl.classList.add('dl');
  1277. footer.appendChild(dl);
  1278. dl.href = src;
  1279. dl.setAttribute("download", title.innerText + ' by ' + author.innerText + '.' + ext);
  1280. dl.setAttribute("target", "_blank");
  1281. dl.textContent = 'Download this audio';
  1282. }
  1283. function audioLoaded() {
  1284. audio = document.querySelector('audio');
  1285. if(audio !== null && audio.getAttribute('src') !== null) {
  1286. // observer.disconnect();
  1287. addDownload();
  1288. setKeybinds();
  1289. } else {
  1290. setTimeout(audioLoaded, 100);
  1291. }
  1292. }
  1293.  
  1294. // Wait for audio to load
  1295. if(audio !== null && audio.getAttribute('src') !== null) {
  1296. addDownload();
  1297. } else {
  1298. audioLoaded();
  1299. }
  1300. }
  1301.  
  1302. // signup page
  1303. if(window.location.pathname.startsWith('/signup')) {
  1304. let h1 = document.querySelector('h1');
  1305. let form = document.querySelector('.signupform');
  1306. form.prepend(h1);
  1307. }
  1308. }