Sniffies Save Profiles

Adds a "Save Profile" button to profiles and a professional GUI to manage saved profiles in a table format. Allows editing the profile title before saving.

  1. // ==UserScript==
  2. // @name Sniffies Save Profiles
  3. // @namespace LiveCamShow.scripts
  4. // @version 1.5
  5. // @description Adds a "Save Profile" button to profiles and a professional GUI to manage saved profiles in a table format. Allows editing the profile title before saving.
  6. // @author LiveCamShow
  7. // @match *://sniffies.com/*
  8. // @license mit
  9. // ==/UserScript==
  10.  
  11. (function () {
  12. 'use strict';
  13.  
  14. function getSavedProfiles() {
  15. return JSON.parse(localStorage.getItem('savedProfiles')) || [];
  16. }
  17.  
  18. function setSavedProfiles(profiles) {
  19. localStorage.setItem('savedProfiles', JSON.stringify(profiles));
  20. }
  21. // Save profile data
  22. const saveProfile = (profileUrl, profileHeader, profileImage) => {
  23. let savedProfiles = getSavedProfiles();
  24. const profileExists = savedProfiles.some((profile) => profile.url === profileUrl);
  25.  
  26. if (!profileExists) {
  27. savedProfiles.push({ url: profileUrl, header: profileHeader, image: profileImage });
  28. setSavedProfiles(savedProfiles);
  29. alert(`Profile saved: ${profileHeader}`);
  30. } else {
  31. alert('Profile already saved!');
  32. }
  33. };
  34.  
  35. // Display save popup
  36. const showSavePopup = (profileUrl, profileHeader, profileImage) => {
  37. // Create the backdrop for click detection
  38. const backdrop = document.createElement('div');
  39. Object.assign(backdrop.style, {
  40. position: 'fixed',
  41. top: '0',
  42. left: '0',
  43. width: '100vw',
  44. height: '100vh',
  45. backgroundColor: 'rgba(0, 0, 0, 0.5)',
  46. zIndex: '9999',
  47. });
  48.  
  49.  
  50. // Popup Container
  51. const popupContainer = document.createElement('div');
  52. Object.assign(popupContainer.style, {
  53. position: 'fixed',
  54. top: '50%',
  55. left: '50%',
  56. transform: 'translate(-50%, -50%)',
  57. zIndex: '10000',
  58. backgroundColor: '#0f1e35',
  59. color: '#d4d9e4',
  60. borderTop: '4px solid #4b84e6',
  61. borderBottom: '4px solid #4b84e6',
  62. borderRadius: '8px',
  63. boxShadow: '0 4px 12px rgba(14, 22, 33, .25)',
  64. padding: '20px',
  65. width: '300px',
  66. });
  67.  
  68. const titleLabel = document.createElement('label');
  69. titleLabel.textContent = 'Edit Profile Title:';
  70. titleLabel.style.display = 'block';
  71. titleLabel.style.marginBottom = '10px';
  72.  
  73. const titleInput = document.createElement('input');
  74. Object.assign(titleInput, {
  75. type: 'text',
  76. value: profileHeader
  77. });
  78. Object.assign(titleInput.style, {
  79. width: '100%',
  80. marginBottom: '20px',
  81. padding: '5px',
  82. border: '1px solid #ccc',
  83. borderRadius: '4px'
  84. });
  85.  
  86. const saveButton = document.createElement('button');
  87. saveButton.textContent = 'Save';
  88. saveButton.style.marginRight = '10px';
  89. saveButton.addEventListener('click', () => {
  90. saveProfile(profileUrl, titleInput.value, profileImage);
  91. backdrop.remove();
  92. });
  93.  
  94. const cancelButton = document.createElement('button');
  95. cancelButton.textContent = 'Cancel';
  96. cancelButton.addEventListener('click', () => backdrop.remove());
  97.  
  98. popupContainer.append(titleLabel, titleInput, saveButton, cancelButton);
  99. backdrop.appendChild(popupContainer);
  100. document.body.appendChild(backdrop);
  101.  
  102. // Close popup on clicking outside
  103. backdrop.addEventListener('click', (e) => {
  104. if (e.target === backdrop) {
  105. backdrop.remove();
  106. }
  107. });
  108. };
  109.  
  110.  
  111. // Open Saved Profiles GUI
  112. const openSavedProfilesGUI = () => {
  113. const savedProfiles = getSavedProfiles();
  114.  
  115. // Create a backdrop to capture outside clicks
  116. const backdrop = document.createElement('div');
  117. Object.assign(backdrop.style, {
  118. position: 'fixed',
  119. top: '0',
  120. left: '0',
  121. width: '100%',
  122. height: '100%',
  123. backgroundColor: 'rgba(0, 0, 0, 0.5)',
  124. zIndex: '9999',
  125. });
  126.  
  127. backdrop.addEventListener('click', (e) => {
  128. if (e.target === backdrop) {
  129. backdrop.remove();
  130. }
  131. });
  132.  
  133.  
  134.  
  135. const guiContainer = document.createElement('div');
  136. Object.assign(guiContainer.style, {
  137. position: 'fixed',
  138. top: '50%',
  139. left: '50%',
  140. transform: 'translate(-50%, -50%)',
  141. zIndex: '10000',
  142. padding: '20px',
  143. width: '600px',
  144. height: '400px',
  145. overflowY: 'auto',
  146. backgroundColor: '#0f1e35',
  147. color: '#d4d9e4',
  148. borderTop: '4px solid #4b84e6',
  149. borderBottom: '4px solid #4b84e6',
  150. borderRadius: '8px',
  151. boxShadow: '0 4px 12px rgba(14, 22, 33, .25)',
  152. });
  153.  
  154. backdrop.appendChild(guiContainer);
  155. document.body.appendChild(backdrop);
  156. const header = document.createElement('div');
  157. Object.assign(header.style, {
  158. display: 'flex',
  159. justifyContent: 'space-between', // Aligns elements to opposite sides
  160. alignItems: 'center', // Vertically center the text and button
  161. marginBottom: '10px',
  162. width: '100%' // Ensure the container takes up the full width
  163. });
  164.  
  165. const headerText = document.createElement('span');
  166. headerText.textContent = 'Saved Profiles';
  167. headerText.style.fontWeight = 'bold';
  168. headerText.style.fontSize = '1.2em';
  169.  
  170. // Create the close button
  171. const closeButton = document.createElement('button');
  172. closeButton.innerHTML = '<i class="fa fa-times"></i>'; // Using Font Awesome icon
  173. Object.assign(closeButton.style, {
  174. backgroundColor: 'transparent',
  175. border: 'none',
  176. fontSize: '1.2em',
  177. cursor: 'pointer',
  178. });
  179.  
  180. // Close button functionality
  181. closeButton.addEventListener('click', () => guiContainer.remove());
  182.  
  183. // Append both elements to the header
  184. header.appendChild(headerText);
  185. header.appendChild(closeButton);
  186.  
  187.  
  188. const table = document.createElement('table');
  189. table.style.width = '100%';
  190. table.style.tableLayout = 'fixed'; // Prevents columns from resizing dynamically
  191. table.style.borderCollapse = 'collapse';
  192.  
  193. const thead = document.createElement('thead');
  194. const headerRow = document.createElement('tr');
  195. ['Image', 'Title', 'Actions'].forEach((text, index) => {
  196. const th = document.createElement('th');
  197. th.textContent = text;
  198. Object.assign(th.style, {
  199. borderBottom: '4px solid #4b84e6',
  200. borderRadius: '2px',
  201. boxShadow: '0 4px 12px rgba(14, 22, 33, .25)',
  202. padding: '10px',
  203. paddingRight: index === 0 ? '30px' : '10px',
  204. paddingLeft: index === 2 ? '0px' : '10px',
  205. textAlign: index === 1 ? 'left' : 'center', // Center headers horizontally
  206. fontWeight: 'bold',
  207. width: index === 1 ? '355px' : 'auto'
  208. });
  209. headerRow.appendChild(th);
  210. });
  211.  
  212. thead.appendChild(headerRow);
  213. table.appendChild(thead);
  214.  
  215. const tbody = document.createElement('tbody');
  216.  
  217. savedProfiles.forEach((profile) => {
  218. const row = document.createElement('tr');
  219. Object.assign(row.style, {
  220. backgroundColor: '#000000',
  221. borderBottom: '4px solid #07101e',
  222. color: '#d4d9e4',
  223. });
  224.  
  225. const imgCell = document.createElement('td');
  226. imgCell.style.padding = '10px';
  227. imgCell.style.paddingRight = '30px';
  228. imgCell.style.textAlign = 'center'; // Centers the image horizontally
  229. const img = document.createElement('img');
  230. Object.assign(img, {
  231. src: profile.image,
  232. alt: 'Profile Picture',
  233. });
  234. Object.assign(img.style, {
  235. width: '60px',
  236. height: '60px',
  237. borderRadius: '50%',
  238. cursor: 'pointer',
  239. });
  240.  
  241.  
  242. // Click event to open overlay
  243. img.addEventListener('click', () => {
  244. const overlay = document.createElement('div');
  245. Object.assign(overlay.style, {
  246. position: 'fixed',
  247. top: '0',
  248. left: '0',
  249. width: '100%',
  250. height: '100%',
  251. backgroundColor: 'rgba(0, 0, 0, 0.8)',
  252. display: 'flex',
  253. alignItems: 'center',
  254. justifyContent: 'center',
  255. zIndex: '10001',
  256. });
  257.  
  258. const fullImg = document.createElement('img');
  259. fullImg.src = profile.image.replace('-thumb', ''); // Removes "-thumb" from image URL
  260. Object.assign(fullImg.style, {
  261. maxWidth: '90%',
  262. maxHeight: '90%',
  263. borderRadius: '8px',
  264. });
  265.  
  266. overlay.appendChild(fullImg);
  267.  
  268. // Close overlay on click
  269. overlay.addEventListener('click', () => overlay.remove());
  270. document.body.appendChild(overlay);
  271. });
  272.  
  273. imgCell.appendChild(img);
  274.  
  275. const titleCell = document.createElement('td');
  276. titleCell.textContent = profile.header;
  277. titleCell.style.padding = '10px';
  278. titleCell.style.width = '355px'; // Title column width
  279.  
  280. const actionsCell = document.createElement('td');
  281. actionsCell.style.padding = '10px';
  282. actionsCell.style.paddingLeft = '0px';
  283. actionsCell.style.textAlign = 'left'; // Align actions (buttons) to the left
  284.  
  285. const actionsContainer = document.createElement('div'); // Action buttons container
  286. actionsContainer.style.display = 'flex';
  287. actionsContainer.style.gap = '1px';
  288.  
  289. // View icon button
  290. const viewIcon = document.createElement('button');
  291. viewIcon.innerHTML = '<i class="fa fa-eye"></i>';
  292. viewIcon.title = 'View Profile';
  293. viewIcon.addEventListener('click', () => window.open(profile.url, '_blank'));
  294. actionsContainer.appendChild(viewIcon);
  295.  
  296. // Delete icon button
  297. const deleteIcon = document.createElement('button');
  298. deleteIcon.innerHTML = '<i class="fa fa-trash"></i>';
  299. deleteIcon.title = 'Delete Profile';
  300. deleteIcon.addEventListener('click', () => {
  301. if (confirm('Are you sure you want to delete this profile?')) {
  302. const updatedProfiles = savedProfiles.filter((p) => p.url !== profile.url);
  303. setSavedProfiles(savedProfiles);
  304. guiContainer.remove();
  305. openSavedProfilesGUI();
  306. }
  307. });
  308. actionsContainer.appendChild(deleteIcon);
  309.  
  310. // Edit icon button for changing profile title
  311. const editIcon = document.createElement('button');
  312. editIcon.innerHTML = '<i class="fa fa-edit"></i>';
  313. editIcon.title = 'Edit Title';
  314. editIcon.addEventListener('click', () => {
  315. const newTitle = prompt('Edit Profile Title:', profile.header);
  316. if (newTitle) {
  317. profile.header = newTitle;
  318. setSavedProfiles(savedProfiles);
  319. guiContainer.remove();
  320. openSavedProfilesGUI();
  321. }
  322. });
  323. actionsContainer.appendChild(editIcon);
  324.  
  325. // Notes icon button for sticky note
  326. const notesIcon = document.createElement('button');
  327. notesIcon.innerHTML = '<i class="fa fa-sticky-note"></i>';
  328. notesIcon.title = 'Add Notes';
  329. notesIcon.addEventListener('click', () => {
  330. const notePopup = document.createElement('div');
  331. Object.assign(notePopup.style, {
  332. position: 'fixed',
  333. top: '20%',
  334. left: '50%',
  335. transform: 'translateX(-50%)',
  336. zIndex: '10001',
  337. backgroundColor: '#fffb8f',
  338. color: '#333',
  339. padding: '15px',
  340. border: '1px solid #ccc',
  341. borderRadius: '8px',
  342. boxShadow: '0 4px 8px rgba(0,0,0,0.2)',
  343. width: '300px',
  344. maxHeight: '200px',
  345. overflowY: 'auto',
  346. fontFamily: 'Arial, sans-serif',
  347. });
  348.  
  349. const noteText = document.createElement('textarea');
  350. noteText.value = profile.notes || '';
  351. Object.assign(noteText.style, {
  352. width: '100%',
  353. height: '100px',
  354. border: 'none',
  355. backgroundColor: 'transparent',
  356. outline: 'none',
  357. resize: 'none',
  358. fontFamily: 'inherit',
  359. });
  360.  
  361. const saveNoteButton = document.createElement('button');
  362. saveNoteButton.textContent = 'Save';
  363. Object.assign(saveNoteButton.style, {
  364. marginTop: '10px',
  365. marginRight: '10px',
  366. padding: '5px 10px',
  367. backgroundColor: '#007bff',
  368. color: '#fff',
  369. border: 'none',
  370. borderRadius: '4px',
  371. cursor: 'pointer',
  372. });
  373.  
  374. const closeNoteButton = document.createElement('button');
  375. closeNoteButton.textContent = 'Close';
  376. Object.assign(closeNoteButton.style, {
  377. padding: '5px 10px',
  378. backgroundColor: '#6c757d',
  379. color: '#fff',
  380. border: 'none',
  381. borderRadius: '4px',
  382. cursor: 'pointer',
  383. });
  384.  
  385. saveNoteButton.addEventListener('click', () => {
  386. profile.notes = noteText.value;
  387. setSavedProfiles(savedProfiles);
  388. alert('Notes saved!');
  389. notePopup.remove();
  390. });
  391.  
  392. closeNoteButton.addEventListener('click', () => notePopup.remove());
  393.  
  394. notePopup.append(noteText, saveNoteButton, closeNoteButton);
  395. document.body.appendChild(notePopup);
  396. });
  397. actionsContainer.appendChild(notesIcon);
  398.  
  399.  
  400. actionsCell.appendChild(actionsContainer);
  401.  
  402. row.append(imgCell, titleCell, actionsCell);
  403. tbody.appendChild(row);
  404. });
  405.  
  406. table.appendChild(tbody);
  407. guiContainer.append(header, table);
  408. };
  409.  
  410. // Add "Save Profile" button
  411. const addSaveButtonToProfiles = () => {
  412. const profileButtons = document.querySelectorAll('[data-testid="pinUserButton"]');
  413. profileButtons.forEach((button) => {
  414. if (button.parentNode.querySelector('#saveProfileButton')) return;
  415.  
  416. const saveButton = document.createElement('button');
  417. Object.assign(saveButton, {
  418. id: 'saveProfileButton',
  419. type: 'button',
  420. 'aria-label': 'Save Profile',
  421. innerHTML: '<i class="fa fa-save controls-icon text-size: 25px;"></i>'
  422. });
  423. Object.assign(saveButton, {
  424. width: '34xp',
  425. height: '40px',
  426. })
  427.  
  428. saveButton.addEventListener('click', (event) => {
  429. const profileUrl = window.location.href;
  430. const profileHeader = document.querySelector('[data-testid="cruiserProfileLabel"].header-text')?.textContent.trim() || 'Unknown Profile';
  431. const profileImageContainer = document.querySelector('.profile-image-container');
  432. const profileImageUrl = profileImageContainer?.style.backgroundImage.match(/url\("(.*)"\)/)?.[1];
  433. const profileImageThumbnail = profileImageUrl?.replace('.jpeg', '-thumb.jpeg') || '';
  434.  
  435. showSavePopup(profileUrl, profileHeader, profileImageThumbnail);
  436. });
  437.  
  438. button.parentNode.insertBefore(saveButton, button);
  439. });
  440. };
  441.  
  442. // Add GUI button to map controls
  443. const addGUIButtonToMap = () => {
  444. const travelModeIcon = document.querySelector('[data-testid="travelModeIcon"]');
  445. if (!travelModeIcon || document.getElementById('openGUIButton')) return;
  446.  
  447. const guiButton = document.createElement('button');
  448. Object.assign(guiButton, {
  449. id: 'openGUIButton',
  450. type: 'button',
  451. 'aria-label': 'Open Saved Profiles GUI',
  452. innerHTML: '<i class="fa fa-list controls-icon" style="font-size: 25px;"></i>'
  453. });
  454.  
  455. Object.assign(guiButton.style, {
  456. width: '50px',
  457. height: '50px',
  458. color: '#fff',
  459. cursor: 'pointer',
  460. });
  461.  
  462. guiButton.addEventListener('click', (event) => {
  463.  
  464. event.preventDefault();
  465. event.stopPropagation();
  466. openSavedProfilesGUI();
  467. });
  468.  
  469. travelModeIcon.parentNode.insertBefore(guiButton, travelModeIcon);
  470. }
  471.  
  472. // Observe DOM changes
  473. function debounce(func, wait) {
  474. let timeout;
  475. return function (...args) {
  476. clearTimeout(timeout);
  477. timeout = setTimeout(() => func.apply(this, args), wait);
  478. };
  479. }
  480.  
  481. const debouncedObserverCallback = debounce(() => {
  482. addSaveButtonToProfiles();
  483. addGUIButtonToMap();
  484. }, 100);
  485. const observer = new MutationObserver(debouncedObserverCallback);
  486. observer.observe(document.body, { childList: true, subtree: true });
  487.  
  488. })();