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.

// ==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 });

})();