您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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.
// ==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 }); })();