// ==UserScript==
// @name Sniffies Save Profiles
// @namespace LiveCamShow.scripts
// @version 1.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.
// @author LiveCamShow
// @match *://sniffies.com/*
// @license mit
// ==/UserScript==
(function () {
'use strict';
function getSavedProfiles() {
return JSON.parse(localStorage.getItem('savedProfiles')) || [];
}
function setSavedProfiles(profiles) {
localStorage.setItem('savedProfiles', JSON.stringify(profiles));
}
// Save profile data
const saveProfile = (profileUrl, profileHeader, profileImage) => {
let savedProfiles = getSavedProfiles();
const profileExists = savedProfiles.some((profile) => profile.url === profileUrl);
if (!profileExists) {
savedProfiles.push({ url: profileUrl, header: profileHeader, image: profileImage });
setSavedProfiles(savedProfiles);
alert(`Profile saved: ${profileHeader}`);
} else {
alert('Profile already saved!');
}
};
// Display save popup
const showSavePopup = (profileUrl, profileHeader, profileImage) => {
// Create the backdrop for click detection
const backdrop = document.createElement('div');
Object.assign(backdrop.style, {
position: 'fixed',
top: '0',
left: '0',
width: '100vw',
height: '100vh',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
zIndex: '9999',
});
// Popup Container
const popupContainer = document.createElement('div');
Object.assign(popupContainer.style, {
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
zIndex: '10000',
backgroundColor: '#0f1e35',
color: '#d4d9e4',
borderTop: '4px solid #4b84e6',
borderBottom: '4px solid #4b84e6',
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(14, 22, 33, .25)',
padding: '20px',
width: '300px',
});
const titleLabel = document.createElement('label');
titleLabel.textContent = 'Edit Profile Title:';
titleLabel.style.display = 'block';
titleLabel.style.marginBottom = '10px';
const titleInput = document.createElement('input');
Object.assign(titleInput, {
type: 'text',
value: profileHeader
});
Object.assign(titleInput.style, {
width: '100%',
marginBottom: '20px',
padding: '5px',
border: '1px solid #ccc',
borderRadius: '4px'
});
const saveButton = document.createElement('button');
saveButton.textContent = 'Save';
saveButton.style.marginRight = '10px';
saveButton.addEventListener('click', () => {
saveProfile(profileUrl, titleInput.value, profileImage);
backdrop.remove();
});
const cancelButton = document.createElement('button');
cancelButton.textContent = 'Cancel';
cancelButton.addEventListener('click', () => backdrop.remove());
popupContainer.append(titleLabel, titleInput, saveButton, cancelButton);
backdrop.appendChild(popupContainer);
document.body.appendChild(backdrop);
// Close popup on clicking outside
backdrop.addEventListener('click', (e) => {
if (e.target === backdrop) {
backdrop.remove();
}
});
};
// Open Saved Profiles GUI
const openSavedProfilesGUI = () => {
const savedProfiles = getSavedProfiles();
// Create a backdrop to capture outside clicks
const backdrop = document.createElement('div');
Object.assign(backdrop.style, {
position: 'fixed',
top: '0',
left: '0',
width: '100%',
height: '100%',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
zIndex: '9999',
});
backdrop.addEventListener('click', (e) => {
if (e.target === backdrop) {
backdrop.remove();
}
});
const guiContainer = document.createElement('div');
Object.assign(guiContainer.style, {
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
zIndex: '10000',
padding: '20px',
width: '600px',
height: '400px',
overflowY: 'auto',
backgroundColor: '#0f1e35',
color: '#d4d9e4',
borderTop: '4px solid #4b84e6',
borderBottom: '4px solid #4b84e6',
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(14, 22, 33, .25)',
});
backdrop.appendChild(guiContainer);
document.body.appendChild(backdrop);
const header = document.createElement('div');
Object.assign(header.style, {
display: 'flex',
justifyContent: 'space-between', // Aligns elements to opposite sides
alignItems: 'center', // Vertically center the text and button
marginBottom: '10px',
width: '100%' // Ensure the container takes up the full width
});
const headerText = document.createElement('span');
headerText.textContent = 'Saved Profiles';
headerText.style.fontWeight = 'bold';
headerText.style.fontSize = '1.2em';
// Create the close button
const closeButton = document.createElement('button');
closeButton.innerHTML = '<i class="fa fa-times"></i>'; // Using Font Awesome icon
Object.assign(closeButton.style, {
backgroundColor: 'transparent',
border: 'none',
fontSize: '1.2em',
cursor: 'pointer',
});
// Close button functionality
closeButton.addEventListener('click', () => guiContainer.remove());
// Append both elements to the header
header.appendChild(headerText);
header.appendChild(closeButton);
const table = document.createElement('table');
table.style.width = '100%';
table.style.tableLayout = 'fixed'; // Prevents columns from resizing dynamically
table.style.borderCollapse = 'collapse';
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
['Image', 'Title', 'Actions'].forEach((text, index) => {
const th = document.createElement('th');
th.textContent = text;
Object.assign(th.style, {
borderBottom: '4px solid #4b84e6',
borderRadius: '2px',
boxShadow: '0 4px 12px rgba(14, 22, 33, .25)',
padding: '10px',
paddingRight: index === 0 ? '30px' : '10px',
paddingLeft: index === 2 ? '0px' : '10px',
textAlign: index === 1 ? 'left' : 'center', // Center headers horizontally
fontWeight: 'bold',
width: index === 1 ? '355px' : 'auto'
});
headerRow.appendChild(th);
});
thead.appendChild(headerRow);
table.appendChild(thead);
const tbody = document.createElement('tbody');
savedProfiles.forEach((profile) => {
const row = document.createElement('tr');
Object.assign(row.style, {
backgroundColor: '#000000',
borderBottom: '4px solid #07101e',
color: '#d4d9e4',
});
const imgCell = document.createElement('td');
imgCell.style.padding = '10px';
imgCell.style.paddingRight = '30px';
imgCell.style.textAlign = 'center'; // Centers the image horizontally
const img = document.createElement('img');
Object.assign(img, {
src: profile.image,
alt: 'Profile Picture',
});
Object.assign(img.style, {
width: '60px',
height: '60px',
borderRadius: '50%',
cursor: 'pointer',
});
// Click event to open overlay
img.addEventListener('click', () => {
const overlay = document.createElement('div');
Object.assign(overlay.style, {
position: 'fixed',
top: '0',
left: '0',
width: '100%',
height: '100%',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: '10001',
});
const fullImg = document.createElement('img');
fullImg.src = profile.image.replace('-thumb', ''); // Removes "-thumb" from image URL
Object.assign(fullImg.style, {
maxWidth: '90%',
maxHeight: '90%',
borderRadius: '8px',
});
overlay.appendChild(fullImg);
// Close overlay on click
overlay.addEventListener('click', () => overlay.remove());
document.body.appendChild(overlay);
});
imgCell.appendChild(img);
const titleCell = document.createElement('td');
titleCell.textContent = profile.header;
titleCell.style.padding = '10px';
titleCell.style.width = '355px'; // Title column width
const actionsCell = document.createElement('td');
actionsCell.style.padding = '10px';
actionsCell.style.paddingLeft = '0px';
actionsCell.style.textAlign = 'left'; // Align actions (buttons) to the left
const actionsContainer = document.createElement('div'); // Action buttons container
actionsContainer.style.display = 'flex';
actionsContainer.style.gap = '1px';
// View icon button
const viewIcon = document.createElement('button');
viewIcon.innerHTML = '<i class="fa fa-eye"></i>';
viewIcon.title = 'View Profile';
viewIcon.addEventListener('click', () => window.open(profile.url, '_blank'));
actionsContainer.appendChild(viewIcon);
// Delete icon button
const deleteIcon = document.createElement('button');
deleteIcon.innerHTML = '<i class="fa fa-trash"></i>';
deleteIcon.title = 'Delete Profile';
deleteIcon.addEventListener('click', () => {
if (confirm('Are you sure you want to delete this profile?')) {
const updatedProfiles = savedProfiles.filter((p) => p.url !== profile.url);
setSavedProfiles(savedProfiles);
guiContainer.remove();
openSavedProfilesGUI();
}
});
actionsContainer.appendChild(deleteIcon);
// Edit icon button for changing profile title
const editIcon = document.createElement('button');
editIcon.innerHTML = '<i class="fa fa-edit"></i>';
editIcon.title = 'Edit Title';
editIcon.addEventListener('click', () => {
const newTitle = prompt('Edit Profile Title:', profile.header);
if (newTitle) {
profile.header = newTitle;
setSavedProfiles(savedProfiles);
guiContainer.remove();
openSavedProfilesGUI();
}
});
actionsContainer.appendChild(editIcon);
// Notes icon button for sticky note
const notesIcon = document.createElement('button');
notesIcon.innerHTML = '<i class="fa fa-sticky-note"></i>';
notesIcon.title = 'Add Notes';
notesIcon.addEventListener('click', () => {
const notePopup = document.createElement('div');
Object.assign(notePopup.style, {
position: 'fixed',
top: '20%',
left: '50%',
transform: 'translateX(-50%)',
zIndex: '10001',
backgroundColor: '#fffb8f',
color: '#333',
padding: '15px',
border: '1px solid #ccc',
borderRadius: '8px',
boxShadow: '0 4px 8px rgba(0,0,0,0.2)',
width: '300px',
maxHeight: '200px',
overflowY: 'auto',
fontFamily: 'Arial, sans-serif',
});
const noteText = document.createElement('textarea');
noteText.value = profile.notes || '';
Object.assign(noteText.style, {
width: '100%',
height: '100px',
border: 'none',
backgroundColor: 'transparent',
outline: 'none',
resize: 'none',
fontFamily: 'inherit',
});
const saveNoteButton = document.createElement('button');
saveNoteButton.textContent = 'Save';
Object.assign(saveNoteButton.style, {
marginTop: '10px',
marginRight: '10px',
padding: '5px 10px',
backgroundColor: '#007bff',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
});
const closeNoteButton = document.createElement('button');
closeNoteButton.textContent = 'Close';
Object.assign(closeNoteButton.style, {
padding: '5px 10px',
backgroundColor: '#6c757d',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
});
saveNoteButton.addEventListener('click', () => {
profile.notes = noteText.value;
setSavedProfiles(savedProfiles);
alert('Notes saved!');
notePopup.remove();
});
closeNoteButton.addEventListener('click', () => notePopup.remove());
notePopup.append(noteText, saveNoteButton, closeNoteButton);
document.body.appendChild(notePopup);
});
actionsContainer.appendChild(notesIcon);
actionsCell.appendChild(actionsContainer);
row.append(imgCell, titleCell, actionsCell);
tbody.appendChild(row);
});
table.appendChild(tbody);
guiContainer.append(header, table);
};
// Add "Save Profile" button
const addSaveButtonToProfiles = () => {
const profileButtons = document.querySelectorAll('[data-testid="pinUserButton"]');
profileButtons.forEach((button) => {
if (button.parentNode.querySelector('#saveProfileButton')) return;
const saveButton = document.createElement('button');
Object.assign(saveButton, {
id: 'saveProfileButton',
type: 'button',
'aria-label': 'Save Profile',
innerHTML: '<i class="fa fa-save controls-icon text-size: 25px;"></i>'
});
Object.assign(saveButton, {
width: '34xp',
height: '40px',
})
saveButton.addEventListener('click', (event) => {
const profileUrl = window.location.href;
const profileHeader = document.querySelector('[data-testid="cruiserProfileLabel"].header-text')?.textContent.trim() || 'Unknown Profile';
const profileImageContainer = document.querySelector('.profile-image-container');
const profileImageUrl = profileImageContainer?.style.backgroundImage.match(/url\("(.*)"\)/)?.[1];
const profileImageThumbnail = profileImageUrl?.replace('.jpeg', '-thumb.jpeg') || '';
showSavePopup(profileUrl, profileHeader, profileImageThumbnail);
});
button.parentNode.insertBefore(saveButton, button);
});
};
// Add GUI button to map controls
const addGUIButtonToMap = () => {
const travelModeIcon = document.querySelector('[data-testid="travelModeIcon"]');
if (!travelModeIcon || document.getElementById('openGUIButton')) return;
const guiButton = document.createElement('button');
Object.assign(guiButton, {
id: 'openGUIButton',
type: 'button',
'aria-label': 'Open Saved Profiles GUI',
innerHTML: '<i class="fa fa-list controls-icon" style="font-size: 25px;"></i>'
});
Object.assign(guiButton.style, {
width: '50px',
height: '50px',
color: '#fff',
cursor: 'pointer',
});
guiButton.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
openSavedProfilesGUI();
});
travelModeIcon.parentNode.insertBefore(guiButton, travelModeIcon);
}
// Observe DOM changes
function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
const debouncedObserverCallback = debounce(() => {
addSaveButtonToProfiles();
addGUIButtonToMap();
}, 100);
const observer = new MutationObserver(debouncedObserverCallback);
observer.observe(document.body, { childList: true, subtree: true });
})();