Backloggd Button Utility

Adds customizable buttons to backloggd.

  1. // ==UserScript==
  2. // @name Backloggd Button Utility
  3. // @namespace http://vers.works/
  4. // @version 2.56
  5. // @icon https://pbs.twimg.com/profile_images/1541908760607821824/3Am5dmsx_400x400.jpg
  6. // @description Adds customizable buttons to backloggd.
  7. // @author VERS
  8. // @match https://www.backloggd.com/*
  9. // @match *://backloggd.com/*
  10. // @match *://*.backloggd.com/*
  11. // @exclude https://backloggd.com/search/*
  12. // @grant GM_setValue
  13. // @grant GM_getValue
  14. // @license MIT
  15. // ==/UserScript==
  16.  
  17. // Version 2.53:
  18. // Fixed loading issues and GOG logo, thank you to DarkH15.
  19.  
  20.  
  21. (function() {
  22. 'use strict';
  23.  
  24. const whitelist = ['Steam', 'SteamDB', 'SteamGridDB', 'Epic Games Store', 'GOG', 'Twitch', 'Youtube', 'XBOX', 'Playstation', 'Nintendo',];
  25.  
  26. let buttons = GM_getValue('buttons', [
  27. {
  28. name: 'Steam',
  29. iconUrl: 'https://i.imgur.com/9NFBdz4.png',
  30. searchUrl: 'https://store.steampowered.com/search/?term=',
  31. color: '#1a9fff',
  32. status: true
  33. },
  34. {
  35. name: 'SteamDB',
  36. iconUrl: 'https://i.imgur.com/t6rV9xA.png',
  37. searchUrl: 'https://steamdb.info/search/?a=all&q=',
  38. color: '#3e76cb',
  39. status: true
  40. },
  41. {
  42. name: 'SteamGridDB',
  43. iconUrl: 'https://i.imgur.com/oluhFHS.png',
  44. searchUrl: 'https://www.steamgriddb.com/search/grids?term=',
  45. color: '#3a6e92',
  46. status: true
  47. },
  48. {
  49. name: 'Epic Games Store',
  50. iconUrl: 'https://cdn2.unrealengine.com/Unreal+Engine%2Feg-logo-filled-1255x1272-0eb9d144a0f981d1cbaaa1eb957de7a3207b31bb.png',
  51. searchUrl: 'https://store.epicgames.com/en-US/expanded-search-results?q=',
  52. color: '#000000',
  53. status: true
  54. },
  55. {
  56. name: 'GOG',
  57. iconUrl: 'https://yt3.googleusercontent.com/7fRS7D_McYJmwXG4hrzJsZyKgAcVU7WmZrGkYaAyadqMj5LGD-lQXkCZ5ybWAklQSKDSiVbUIA=s900-c-k-c0x00ffffff-no-rj',
  58. searchUrl: 'https://www.gog.com/en/games?query=',
  59. color: '#fff',
  60. status: true
  61. },
  62. {
  63. name: 'Twitch',
  64. iconUrl: 'https://i.imgur.com/UVuf0iF.png',
  65. searchUrl: 'https://www.twitch.tv/search?term=',
  66. color: '#9046fd',
  67. status: true
  68. },
  69. {
  70. name: 'Youtube',
  71. iconUrl: 'https://i.imgur.com/C0Ux2Y3.png',
  72. searchUrl: 'https://www.youtube.com/results?search_query=',
  73. color: '#ff0000',
  74. status: true
  75. },
  76. {
  77. name: 'XBOX',
  78. iconUrl: 'https://i.imgur.com/jrItCUM.png',
  79. searchUrl: 'https://www.xbox.com/Search/Results?q=',
  80. color: '#107b10',
  81. status: true
  82. },
  83. {
  84. name: 'Playstation',
  85. iconUrl: 'https://i.imgur.com/wvB5DF8.png',
  86. searchUrl: 'https://www.playstation.com/search/?q=',
  87. color: '#0070d1',
  88. status: true
  89. },
  90. {
  91. name: 'Nintendo',
  92. iconUrl: 'https://i.imgur.com/7cGs7D6.png',
  93. searchUrl: 'https://www.nintendo.com/us/search/#q=',
  94. color: '#e70819',
  95. status: true
  96. },
  97. {
  98. name: 'Settings',
  99. iconUrl: 'https://i.imgur.com/WvM8EHQ.png',
  100. color: '#16181c',
  101. status: true,
  102. isSettings: true
  103. },
  104. {
  105. name: 'Remove Button',
  106. iconUrl: 'https://via.placeholder.com/24',
  107. color: '#ff0000',
  108. status: true,
  109. isSettings: true,
  110. isRemovable: false
  111. }
  112. ]);
  113.  
  114. function addButton(iconUrl, searchUrl, color, status, name) {
  115. if (!status) {
  116. return;
  117. }
  118.  
  119. const button = document.createElement('button');
  120. button.className = 'btn btn-main journal-btn custom-button';
  121. button.style.width = '34px';
  122. button.style.height = '34px';
  123. button.style.margin = '7px 4px 3px 4px';
  124. button.style.backgroundColor = color;
  125. button.style.border = 'none';
  126. button.style.display = 'inline-flex';
  127. button.style.justifyContent = 'center';
  128. button.style.alignItems = 'center';
  129.  
  130. const icon = document.createElement('img');
  131. icon.src = iconUrl;
  132. icon.alt = 'Search';
  133. icon.style.width = '24px';
  134. icon.style.height = '24px';
  135. button.appendChild(icon);
  136.  
  137. button.addEventListener('click', function() {
  138. if (name === 'Settings') {
  139. openSettingsModal();
  140. } else {
  141. const gameTitleElement = document.querySelector('#title h1');
  142. if (gameTitleElement) {
  143. const gameTitle = encodeURIComponent(gameTitleElement.textContent.trim());
  144. const searchLink = searchUrl.includes('%s') ? searchUrl.replace('%s', gameTitle) : searchUrl + gameTitle;
  145. window.open(searchLink, '_blank');
  146. } else {
  147. console.error('Game title element not found');
  148. }
  149. }
  150. });
  151.  
  152. const journalButtonContainer = document.querySelector('.journal-button-container');
  153. if (journalButtonContainer) {
  154. const settingsButton = journalButtonContainer.querySelector('button[data-name="Settings"]');
  155. const existingButton = journalButtonContainer.querySelector(`.custom-button[data-search="${searchUrl}"]`);
  156.  
  157. if (!existingButton) {
  158. button.setAttribute('data-search', searchUrl);
  159. button.setAttribute('data-name', name);
  160. if (settingsButton) {
  161. journalButtonContainer.insertBefore(button, settingsButton); // Insert before Settings button
  162. } else {
  163. journalButtonContainer.appendChild(button); // Fallback in case settings button is not found
  164. }
  165. }
  166. } else {
  167. console.error('Journal button container not found');
  168. }
  169. }
  170.  
  171. function addSpacer() {
  172. const existingSpacer = document.querySelector('.journal-button-container > .spacer');
  173. if (!existingSpacer) {
  174. const spacer = document.createElement('div');
  175. spacer.className = 'spacer'; // Add a class to identify the spacer
  176. spacer.style.height = '10px'; // Adjust the height as needed
  177. const journalButtonContainer = document.querySelector('.journal-button-container');
  178. if (journalButtonContainer) {
  179. const firstButton = journalButtonContainer.querySelector('.custom-button');
  180. if (firstButton) {
  181. journalButtonContainer.insertBefore(spacer, firstButton);
  182. } else {
  183. journalButtonContainer.appendChild(spacer); // If no buttons exist yet, just append the spacer
  184. }
  185. } else {
  186. console.error('Journal button container not found');
  187. }
  188. }
  189. }
  190.  
  191.  
  192. function updateButtons() {
  193. buttons.forEach(button => {
  194. addButton(button.iconUrl, button.searchUrl, button.color, button.status, button.name);
  195. });
  196. }
  197.  
  198. function openSettingsModal() {
  199. const settingsModal = document.createElement('div');
  200. settingsModal.className = 'settings-modal';
  201. settingsModal.style.position = 'fixed';
  202. settingsModal.style.top = '50%';
  203. settingsModal.style.left = '50%';
  204. settingsModal.style.transform = 'translate(-50%, -50%)';
  205. settingsModal.style.backgroundColor = 'rgba(22, 24, 28)';
  206. settingsModal.style.borderRadius = '10px';
  207. settingsModal.style.padding = '20px';
  208. settingsModal.style.zIndex = '9999';
  209. settingsModal.innerHTML = `
  210. <h1>Backloggd Button Utility</h1>
  211. <p style="display: flex; align-items: center;">Script made by VERS.
  212. <a href="https://www.backloggd.com/u/VERS/" target="_blank">
  213. <img src="https://i.imgur.com/4FLnRC9.png" alt="Icon 1" style="margin-left: 10px;; width: 19px; height: 19px;">
  214. </a>
  215. <a href="https://twitter.com/versworks" target="_blank">
  216. <img src="https://i.imgur.com/yXPQwtv.png" alt="Icon 2" style="margin-left: 5px;; width: 19px; height: 19px;">
  217. </a>
  218. <a href="https://vers.works/" target="_blank">
  219. <img src="https://i.imgur.com/oePEdTt.png" alt="Icon 3" style="margin-left: 5px;; width: 19px; height: 19px;">
  220. </a>
  221. </p>
  222. <p style="font-size: 12px; font-style: italic; text-align: center;">
  223. If you have any issues or feedback you can contact me on discord (The_Vers) <br>or in this <a href="https://www.reddit.com/r/backloggd/comments/1bopg7t/introducing_button_utility_my_script_that_adds/" target="_blank">reddit post</a>.
  224. </p>
  225. <h2>Button Settings</h2>
  226. <ul>
  227. ${buttons
  228. .filter(button => !button.isSettings && button.name !== 'Remove Button')
  229. .map(
  230. button =>
  231. `<li>
  232. <label>
  233. <input type="checkbox" id="${button.name}-checkbox" ${
  234. button.status ? 'checked' : ''
  235. }>
  236. ${button.name}
  237. </label>
  238. </li>`
  239. )
  240. .join('')}
  241. </ul>
  242. <button id="add-button" style="background-color: #4a5e8d; border: none; color: #fff; padding: 8px 16px; cursor: pointer; border-radius: .25rem;">Add Custom Button</button>
  243. <button id="remove-button" style="background-color: #4a5e8d; border: none; color: #fff; padding: 8px 16px; cursor: pointer; border-radius: .25rem;">Remove Custom Button</button>
  244. <button id="save-settings-btn" style="background-color: #fc6399; border: none; color: #fff; padding: 8px 16px; cursor: pointer; border-radius: .25rem;">Save Settings</button>
  245. `;
  246.  
  247. document.body.appendChild(settingsModal);
  248.  
  249. const addButtonBtn = document.getElementById('add-button');
  250. addButtonBtn.addEventListener('click', () => {
  251. openAddButtonModal();
  252. });
  253.  
  254. const removeButtonBtn = document.getElementById('remove-button');
  255. removeButtonBtn.addEventListener('click', () => {
  256. openRemoveButtonModal();
  257. });
  258.  
  259. const saveSettingsBtn = document.getElementById('save-settings-btn');
  260. saveSettingsBtn.addEventListener('click', () => {
  261. buttons.forEach(button => {
  262. if (!button.isSettings) {
  263. const checkbox = document.getElementById(`${button.name}-checkbox`);
  264. button.status = checkbox.checked;
  265. }
  266. });
  267.  
  268. GM_setValue('buttons', buttons);
  269.  
  270. window.location.reload();
  271. });
  272. }
  273.  
  274.  
  275.  
  276. function openRemoveButtonModal() {
  277. const removeButtonModal = document.createElement('div');
  278. removeButtonModal.className = 'remove-button-modal';
  279. removeButtonModal.style.position = 'fixed';
  280. removeButtonModal.style.top = '50%';
  281. removeButtonModal.style.borderRadius = '10px';
  282. removeButtonModal.style.left = '50%';
  283. removeButtonModal.style.transform = 'translate(-50%, -50%)';
  284. removeButtonModal.style.backgroundColor = '#242832';
  285. removeButtonModal.style.padding = '20px';
  286. removeButtonModal.style.zIndex = '9999';
  287. removeButtonModal.innerHTML = `
  288. <h2>Remove Button</h2>
  289. <ul>
  290. ${buttons
  291. .filter(button => !button.isSettings && !whitelist.includes(button.name))
  292. .map(
  293. button =>
  294. `<li>
  295. <button id="remove-${button.name}" style="background-color: #fc6399; border: none; color: #ffffff; padding: 5px 5; cursor: pointer;">✖</button>
  296. <span style="margin-left: 10px;">${button.name}</span>
  297. </li>`
  298. )
  299. .join('')}
  300. </ul>
  301. <button id="close-remove-modal" style="background-color: #4a5e8d; border: none; color: #ffffff; padding: 8px 16px; cursor: pointer; margin-top: 10px;">Close</button>
  302. `;
  303.  
  304. document.body.appendChild(removeButtonModal);
  305.  
  306. buttons
  307. .filter(button => !button.isSettings && !whitelist.includes(button.name))
  308. .forEach(button => {
  309. const removeButton = document.getElementById(`remove-${button.name}`);
  310. removeButton.addEventListener('click', () => {
  311. removeButtonFromList(button.name);
  312. removeButton.parentElement.remove();
  313. });
  314. });
  315.  
  316. const closeModalBtn = document.getElementById('close-remove-modal');
  317. closeModalBtn.addEventListener('click', () => {
  318. removeButtonModal.remove();
  319. });
  320. }
  321.  
  322. function removeButtonFromList(buttonName) {
  323. buttons = buttons.filter(button => button.name !== buttonName);
  324. GM_setValue('buttons', buttons);
  325.  
  326. const existingButtons = document.querySelectorAll('.custom-button');
  327. existingButtons.forEach(button => {
  328. button.remove();
  329. });
  330.  
  331. addSpacer();
  332. updateButtons();
  333. }
  334.  
  335. function openAddButtonModal() {
  336. const addButtonModal = document.createElement('div');
  337. addButtonModal.className = 'add-button-modal';
  338. addButtonModal.style.position = 'fixed';
  339. addButtonModal.style.top = '50%';
  340. addButtonModal.style.left = '50%';
  341. addButtonModal.style.borderRadius = '10px';
  342. addButtonModal.style.transform = 'translate(-50%, -50%)';
  343. addButtonModal.style.backgroundColor = '#242832';
  344. addButtonModal.style.padding = '20px';
  345. addButtonModal.style.zIndex = '9999';
  346. addButtonModal.innerHTML = `
  347. <h2>Add Custom Button</h2>
  348. <div>
  349. <label for="website-name" style="width: 120px;">Website Name:</label>
  350. <button id="info-website-name" class="info-button" style="background-color: #384361; border: none; color: #fff; padding: 3px; cursor: pointer; border-radius: 3px;">
  351. <img src="https://i.imgur.com/jEbi3Oy.png" alt="Info" style="width: 15px; height: 15px;">
  352. </button>
  353. <input type="text" id="website-name" style="width: 200px;" required>
  354. </div>
  355. <div>
  356. <label for="icon-url" style="width: 120px;">Icon URL:</label>
  357. <button id="info-icon-url" class="info-button" style="background-color: #384361; border: none; color: #fff; padding: 3px; cursor: pointer; border-radius: 3px;">
  358. <img src="https://i.imgur.com/jEbi3Oy.png" alt="Info" style="width: 15px; height: 15px;">
  359. </button>
  360. <input type="text" id="icon-url" style="width: 200px;" required>
  361. </div>
  362. <div>
  363. <label for="search-url" style="width: 120px;">Search URL:</label>
  364. <button id="info-search-url" class="info-button" style="background-color: #384361; border: none; color: #fff; padding: 3px; cursor: pointer; border-radius: 3px;">
  365. <img src="https://i.imgur.com/jEbi3Oy.png" alt="Info" style="width: 15px; height: 15px;">
  366. </button>
  367. <input type="text" id="search-url" style="width: 200px;" required>
  368. </div>
  369. <div>
  370. <label for="color" style="width: 120px;">Button Color:</label>
  371. <button id="info-color" class="info-button" style="background-color: #384361; border: none; color: #fff; padding: 3px; cursor: pointer; border-radius: 3px;">
  372. <img src="https://i.imgur.com/jEbi3Oy.png" alt="Info" style="width: 15px; height: 15px;">
  373. </button>
  374. <input type="color" id="color" value="#fc6399">
  375. </div>
  376. <button id="save-new-button" style="background-color: #fc6399; border: none; color: #fff; padding: 8px 16px; cursor: pointer; margin-top: 10px; border-radius: 3px;">Add Button</button>
  377. <button id="close-button" style="background-color: #4a5e8d; border: none; color: #fff; padding: 8px 16px; cursor: pointer; margin-top: 10px; border-radius: 3px;">Close</button>
  378. `;
  379.  
  380. document.body.appendChild(addButtonModal);
  381.  
  382. const saveNewButtonBtn = document.getElementById('save-new-button');
  383. const closeButton = document.getElementById('close-button');
  384.  
  385. function openInfoPopup(inputId, infoText) {
  386. const inputElement = document.getElementById(inputId);
  387. const infoButton = document.getElementById(`info-${inputId}`);
  388.  
  389. infoButton.addEventListener('click', () => {
  390. alert(infoText);
  391. inputElement.focus();
  392. });
  393. }
  394.  
  395. const infoTexts = {
  396. 'website-name': 'Used for enabling/disabling in the settings (Doesnt have to be exact)',
  397. 'icon-url': `Full URL path to a (preferably transparent) PNG.
  398. Correct: https://i.imgur.com/n04Muj1.jpeg
  399. Wrong: https://cdn-icons-png.flaticon.com/512/2815/2815428`,
  400. 'search-url': `This is a URL when you search something without the actual thing you searched.
  401.  
  402. Simple:
  403. Add the full URL without the thing you searched (Good if the searched term is at the end of the URL)
  404.  
  405. Example (On google.com):
  406. If you google test you would get: https://www.google.com/search?q=test
  407. Then you remove 'test' and you get this, which you put into this box: https://www.google.com/search?q=
  408.  
  409. Advanced:
  410. In rare instances, when the searched term is not at the end of the URLm you can replace the searched term with %s, which is where the game name will be put into.
  411.  
  412. Example (Made-up case on Gamebanana):
  413. If the URL is:
  414. https://gamebanana.com/search?__sSearchString=TESTOrder=best_match&_idGameRow=18557
  415.  
  416. We replace the 'TEST' with %s, which we will get:
  417. https://gamebanana.com/search?__sSearchString=%sOrder=best_match&_idGameRow=18557`,
  418. 'color': 'Color for the button.'
  419. };
  420.  
  421. Object.keys(infoTexts).forEach(inputId => {
  422. openInfoPopup(inputId, infoTexts[inputId]);
  423. });
  424.  
  425. saveNewButtonBtn.addEventListener('click', () => {
  426. const websiteNameInput = document.getElementById('website-name').value;
  427. const iconUrlInput = document.getElementById('icon-url').value;
  428. const searchUrlInput = document.getElementById('search-url').value;
  429. const colorInput = document.getElementById('color').value;
  430.  
  431. if (buttons.some(button => button.name.toLowerCase() === websiteNameInput.toLowerCase())) {
  432. alert('A button with this name already exists. Please choose a different name.');
  433. return;
  434. }
  435.  
  436. const newButton = {
  437. name: websiteNameInput,
  438. iconUrl: iconUrlInput,
  439. searchUrl: searchUrlInput,
  440. color: colorInput,
  441. status: true
  442. };
  443.  
  444. buttons.push(newButton);
  445. GM_setValue('buttons', buttons);
  446.  
  447. addButton(newButton.iconUrl, newButton.searchUrl, newButton.color, newButton.status, newButton.name);
  448.  
  449. window.location.reload();
  450. });
  451.  
  452. closeButton.addEventListener('click', () => {
  453. addButtonModal.remove();
  454. });
  455. }
  456.  
  457.  
  458. function waitForElement(selector, callback) {
  459. const interval = setInterval(() => {
  460. const element = document.querySelector(selector);
  461. if (element) {
  462. clearInterval(interval);
  463. callback(element);
  464. }
  465. }, 100);
  466. }
  467.  
  468. function addAtButton() {
  469. const commentRows = document.querySelectorAll('.row.mb-2');
  470. commentRows.forEach(row => {
  471. if (!row.querySelector('.at-button')) {
  472. const atButton = document.createElement('div');
  473. atButton.className = 'col-auto mt-auto pl-0 pr-1 at-button';
  474. atButton.innerHTML = '<button class="button-link secondary-link" style="cursor: pointer;">@</button>';
  475.  
  476. const usernameDiv = row.querySelector('.col-auto.mt-auto.px-2');
  477. if (usernameDiv) {
  478. const usernameLink = usernameDiv.querySelector('a.secondary-link');
  479. if (usernameLink) {
  480. const username = usernameLink.textContent.trim();
  481. atButton.querySelector('button').addEventListener('click', () => {
  482. const commentBox = document.getElementById('comment_body');
  483. if (commentBox) {
  484. const currentText = commentBox.value;
  485. const cursorPosition = commentBox.selectionStart;
  486. const textBefore = currentText.substring(0, cursorPosition);
  487. const textAfter = currentText.substring(cursorPosition);
  488. const newText = `${textBefore}@${username} ${textAfter}`;
  489. commentBox.value = newText;
  490. commentBox.focus();
  491. const newCursorPosition = cursorPosition + username.length + 2;
  492. commentBox.setSelectionRange(newCursorPosition, newCursorPosition);
  493. }
  494. });
  495.  
  496. row.appendChild(atButton);
  497. }
  498. }
  499. }
  500. });
  501. }
  502.  
  503. function processElements() {
  504. addAtButton();
  505. // Add any other element processing functions here
  506. }
  507.  
  508. const observer = new MutationObserver(mutations => {
  509. mutations.forEach(mutation => {
  510. if (mutation.type === 'childList') {
  511. const addedNodes = Array.from(mutation.addedNodes);
  512. addedNodes.forEach(node => {
  513. if (node.nodeType === Node.ELEMENT_NODE) {
  514. // Check if the added node is part of the comment section
  515. if (node.classList && (node.classList.contains('comments-section') || node.closest('.comments-section'))) {
  516. processElements();
  517. } else {
  518. // Process any other elements
  519. processElements();
  520. }
  521. }
  522. });
  523. }
  524. });
  525. });
  526.  
  527. observer.observe(document.body, {
  528. childList: true,
  529. subtree: true
  530. });
  531.  
  532. // Initial setup
  533. addSpacer();
  534. updateButtons();
  535. processElements();
  536.  
  537. waitForElement('#title h1', (element) => {
  538. const titleObserver = new MutationObserver(function(mutationsList) {
  539. for (let mutation of mutationsList) {
  540. if (mutation.type === 'childList' && mutation.target.nodeName === 'HTML') {
  541. const existingButtons = document.querySelectorAll('.custom-button');
  542. existingButtons.forEach(button => {
  543. button.remove();
  544. });
  545. addSpacer();
  546. updateButtons();
  547. processElements();
  548. }
  549. }
  550. });
  551.  
  552. titleObserver.observe(document, { childList: true, subtree: true });
  553. });
  554.  
  555. window.onhashchange = function() {
  556. const existingButtons = document.querySelectorAll('.custom-button');
  557. existingButtons.forEach(button => {
  558. button.remove();
  559. });
  560. updateButtons();
  561. processElements();
  562. };
  563. })();