LetzAI Advanced Settings

A comprehensive suite of advanced features for Letz.ai: Advanced Settings, Gallery Navigation, and Multi-Select Helper.

// ==UserScript==
// @name         LetzAI Advanced Settings
// @namespace    https://x.com/SavitarStorm
// @version      2.0
// @description  A comprehensive suite of advanced features for Letz.ai: Advanced Settings, Gallery Navigation, and Multi-Select Helper.
// @author       @SavitarStorm @Tano
// @match        *://letz.ai/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    // --- SCRIPT-WIDE CONSTANTS ---
    const PROMPT_TEXT_AREA_SELECTOR = '#TextArea';
    const MY_MODELS_STORAGE_KEY = 'myCustomLetzAIModelsList';
    const SETTINGS_CONTAINER_SELECTOR = 'div.wrapgenerationsettings';
    const PROMPT_FORM_SELECTOR = '.outerwrapprompt form';
    const MODELS_BUTTONS_CONTAINER_ID = 'myCustomModelsButtonsContainer';

    // --- UTILITY FUNCTIONS ---
    function setNativeValue(element, value) {
        const valueSetter = Object.getOwnPropertyDescriptor(element, 'value').set;
        const prototype = Object.getPrototypeOf(element);
        const prototypeValueSetter = Object.getOwnPropertyDescriptor(prototype, 'value').set;

        if (valueSetter && valueSetter !== prototypeValueSetter) {
            prototypeValueSetter.call(element, value);
        } else {
            valueSetter.call(element, value);
        }
    }

    function dispatchEvents(element, events = ['input', 'change']) {
        events.forEach(eventType => {
            const event = new Event(eventType, { bubbles: true });
            element.dispatchEvent(event);
        });
    }


    // ===================================================================
    // FEATURE 1: ADVANCED SETTINGS & MODEL MANAGEMENT
    // ===================================================================

    let savedModels = [];

    function loadModels() {
        const modelsJson = GM_getValue(MY_MODELS_STORAGE_KEY, '[]');
        try { savedModels = JSON.parse(modelsJson); } catch (e) { savedModels = []; }
    }

    function saveModels() {
        GM_setValue(MY_MODELS_STORAGE_KEY, JSON.stringify(savedModels));
    }

    function addModelToList(modelString) {
        if (modelString && !savedModels.includes(modelString)) {
            savedModels.push(modelString);
            saveModels();
            renderModelButtons();
            return true;
        }
        return false;
    }

    function removeModelFromList(modelString) {
        savedModels = savedModels.filter(m => m !== modelString);
        saveModels();
        renderModelButtons();
    }

    function addDimensionButtons(settingsContainer) {
        if (document.getElementById('myCustomDimensionButtonsContainer')) return;
        const dimensionsHeader = Array.from(settingsContainer.querySelectorAll('h4')).find(h => h.textContent.trim() === 'Dimensions');
        if (!dimensionsHeader) return;

        const dimensionsBlock = dimensionsHeader.parentElement;
        const customDimensions = [
            { label: '3:4', width: 1440, height: 1920 },
            { label: '4:3', width: 1920, height: 1440 },
            { label: '9:16', width: 1080, height: 1920 },
            { label: '16:9', width: 1920, height: 1080 },
            { label: '1:1', width: 1920, height: 1920 },
            { label: '4:5', width: 1536, height: 1920 },
        ];

        const buttonsContainer = document.createElement('div');
        buttonsContainer.id = 'myCustomDimensionButtonsContainer';
        buttonsContainer.style.cssText = 'margin-top: 10px; display: flex; flex-wrap: wrap;';

        customDimensions.forEach(dim => {
            const button = document.createElement('div');
            button.className = 'stdbuttonsmall';
            button.textContent = dim.label;
            button.style.cssText = 'margin-right: 5px; margin-bottom: 5px; cursor: pointer;';
            button.addEventListener('click', () => updateDimensions(dim.width, dim.height));
            buttonsContainer.appendChild(button);
        });
        dimensionsBlock.appendChild(buttonsContainer);
    }

    function updateDimensions(width, height) {
        const inputs = {
            widthNumber: document.getElementById('width-number'),
            heightNumber: document.getElementById('height-number'),
            widthSlider: document.getElementById('width-slider'),
            heightSlider: document.getElementById('height-slider'),
        };
        if (Object.values(inputs).every(el => el)) {
            setNativeValue(inputs.widthNumber, width.toString());
            dispatchEvents(inputs.widthNumber);
            setNativeValue(inputs.heightNumber, height.toString());
            dispatchEvents(inputs.heightNumber);
            setNativeValue(inputs.widthSlider, width.toString());
            dispatchEvents(inputs.widthSlider);
            setNativeValue(inputs.heightSlider, height.toString());
            dispatchEvents(inputs.heightSlider, ['change', 'input']);
        }
    }

    function renderModelButtons() {
        const promptForm = document.querySelector(PROMPT_FORM_SELECTOR);
        if (!promptForm) return;

        let oldContainer = document.getElementById(MODELS_BUTTONS_CONTAINER_ID);
        if (oldContainer) oldContainer.remove();

        const container = document.createElement('div');
        container.id = MODELS_BUTTONS_CONTAINER_ID;
        container.style.cssText = 'margin-top: 10px; display: flex; flex-wrap: wrap;';

        const mentionInputMain = promptForm.querySelector('#mentionInput-main');
        promptForm.insertBefore(container, mentionInputMain.nextSibling || null);


        if (savedModels.length === 0) {
            container.innerHTML = '<p style="font-size: 12px; color: grey; width: 100%; margin: 0;">No saved models. Use the management section in settings.</p>';
            return;
        }

        savedModels.forEach(model => {
            const buttonWrapper = document.createElement('div');
            buttonWrapper.style.cssText = 'display: flex; align-items: center; margin: 0 5px 5px 0;';
            buttonWrapper.innerHTML = `
                <div class="stdbuttonsmall" style="cursor: pointer;" title="Insert model">${model}</div>
                <span class="remove-model" style="cursor: pointer; margin-left: 4px; font-size: 10px;" title="Remove model">❌</span>
            `;
            buttonWrapper.querySelector('.stdbuttonsmall').addEventListener('click', () => insertModelIntoPrompt(model));
            buttonWrapper.querySelector('.remove-model').addEventListener('click', (e) => {
                e.stopPropagation();
                if (confirm(`Delete model "${model}"?`)) removeModelFromList(model);
            });
            container.appendChild(buttonWrapper);
        });
    }

    function insertModelIntoPrompt(modelString) {
        const textArea = document.querySelector(PROMPT_TEXT_AREA_SELECTOR);
        if (!textArea) return;
        const currentText = textArea.value;
        const textToInsert = (currentText.length > 0 && !/\s$/.test(currentText)) ? ' ' + modelString : modelString;
        setNativeValue(textArea, currentText + textToInsert);
        dispatchEvents(textArea);
        textArea.focus();
    }

    function addModelManagementSection(settingsContainer) {
        if (document.getElementById('myModelManagementSectionAdded')) return;
        const modelSectionDiv = document.createElement('div');
        modelSectionDiv.id = 'myModelManagementSectionAdded';
        modelSectionDiv.style.cssText = 'margin-top: 20px; padding-top: 15px; border-top: 1px solid var(--bordercolor, #444);';
        modelSectionDiv.classList.add('left');

        modelSectionDiv.innerHTML = `
            <h4>Model Management</h4>
            <div id="addModelBtn" class="stdbuttonsmall" style="cursor: pointer; display: inline-block;">Add Model to List</div>
        `;
        modelSectionDiv.querySelector('#addModelBtn').addEventListener('click', () => {
            const modelStringInput = prompt('Enter model name (e.g., @fix):');
            if (modelStringInput?.trim()) {
                if (addModelToList(modelStringInput.trim())) alert(`Model "${modelStringInput.trim()}" added.`);
                else alert(`Model "${modelStringInput.trim()}" is already in the list.`);
            }
        });
        settingsContainer.appendChild(modelSectionDiv);
    }


    // ===================================================================
    // FEATURE 2: GALLERY NAVIGATION BUTTONS
    // ===================================================================

    GM_addStyle(`
        .gallery-nav-button {
            position: fixed; top: 50%; transform: translateY(-50%); width: 50px; height: 70px;
            background-color: rgba(20, 20, 20, 0.5); color: white; border: 1px solid rgba(255, 255, 255, 0.2);
            border-radius: 8px; display: flex; align-items: center; justify-content: center;
            font-size: 32px; font-weight: bold; cursor: pointer; z-index: 99999;
            transition: all 0.2s; user-select: none; font-family: monospace;
        }
        .gallery-nav-button:hover { background-color: rgba(0, 0, 0, 0.7); transform: translateY(-50%) scale(1.05); }
        #gallery-nav-prev { left: 20px; }
        #gallery-nav-next { right: 20px; }
    `);

    function simulateKeyPress(key) {
        document.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }));
    }

    function addGalleryNavButtons() {
        if (document.getElementById('gallery-nav-prev')) return;
        const prevButton = document.createElement('div');
        prevButton.id = 'gallery-nav-prev';
        prevButton.className = 'gallery-nav-button';
        prevButton.innerHTML = '‹';
        prevButton.onclick = (e) => { e.stopPropagation(); simulateKeyPress('ArrowLeft'); };

        const nextButton = document.createElement('div');
        nextButton.id = 'gallery-nav-next';
        nextButton.className = 'gallery-nav-button';
        nextButton.innerHTML = '›';
        nextButton.onclick = (e) => { e.stopPropagation(); simulateKeyPress('ArrowRight'); };

        document.body.appendChild(prevButton);
        document.body.appendChild(nextButton);
    }

    function removeGalleryNavButtons() {
        document.getElementById('gallery-nav-prev')?.remove();
        document.getElementById('gallery-nav-next')?.remove();
    }


    // ===================================================================
    // FEATURE 3: MULTI-SELECT HELPER (FIXED)
    // ===================================================================

    const MULTI_SELECT_ACTIVATION_KEY = "Shift";
    const IMAGE_CONTAINER_SELECTOR = ".imageonprofile__item";
    const CHECKBOX_SELECTOR = ".checkmarkcontainer input[type='checkbox']";

    let isShiftSelecting = false;
    let hoveredDuringDrag = new Set();

    function handleMultiSelectMouseOver(e) {
        if (!isShiftSelecting) return;
        const container = e.target.closest(IMAGE_CONTAINER_SELECTOR);
        if (container && !hoveredDuringDrag.has(container)) {
            hoveredDuringDrag.add(container);
            const checkbox = container.querySelector(CHECKBOX_SELECTOR);
            if (checkbox && !checkbox.checked) {
                checkbox.click();
            }
        }
    }

    function activateMultiSelect() {
        isShiftSelecting = true;
        document.body.style.cursor = 'crosshair';
    }

    function deactivateMultiSelect() {
        isShiftSelecting = false;
        document.body.style.cursor = 'default';
        hoveredDuringDrag.clear();
    }

    function setupMultiSelectListeners() {
        const onKeyDown = (e) => {
            if (e.key === MULTI_SELECT_ACTIVATION_KEY && !isShiftSelecting) activateMultiSelect();
        };
        const onKeyUp = (e) => {
            if (e.key === MULTI_SELECT_ACTIVATION_KEY) deactivateMultiSelect();
        };

        // We add listeners to window to capture events globally
        window.addEventListener('keydown', onKeyDown);
        window.addEventListener('keyup', onKeyUp);
        window.addEventListener('blur', deactivateMultiSelect); // Deactivate on window blur
        document.body.addEventListener('mouseover', handleMultiSelectMouseOver);

        console.log("LetzAI Multi-Select Helper: Ready and listening.");
    }


    // ===================================================================
    // INITIALIZATION & OBSERVER (UNIFIED)
    // ===================================================================

    function runAllFeatures() {
        // Feature 1: Advanced Settings
        const settingsContainer = document.querySelector(SETTINGS_CONTAINER_SELECTOR);
        const promptForm = document.querySelector(PROMPT_FORM_SELECTOR);
        if (settingsContainer) {
            addDimensionButtons(settingsContainer);
            addModelManagementSection(settingsContainer);
        }
        if (promptForm && !document.getElementById(MODELS_BUTTONS_CONTAINER_ID)) {
            renderModelButtons();
        }

        // Feature 2: Gallery Navigation
        const modalExists = !!document.querySelector('.modal');
        const navButtonsExist = !!document.getElementById('gallery-nav-prev');
        if (modalExists && !navButtonsExist) {
            addGalleryNavButtons();
        } else if (!modalExists && navButtonsExist) {
            removeGalleryNavButtons();
        }
    }

    // This observer handles SPA navigation and dynamic content loading
    function observeDOM() {
        let lastUrl = location.href;
        let multiSelectInitialized = false;

        const observer = new MutationObserver(() => {
            // Re-run features that depend on specific elements appearing
            runAllFeatures();

            // Handle Multi-Select initialization based on URL
            const isProfilePage = location.pathname.startsWith('/profile/');
            if (isProfilePage && !multiSelectInitialized) {
                setupMultiSelectListeners();
                multiSelectInitialized = true;
            } else if (!isProfilePage && multiSelectInitialized) {
                // Clean up when leaving profile page (optional but good practice)
                deactivateMultiSelect();
                multiSelectInitialized = false;
                // You could also remove the listeners here if needed, but it's generally safe to leave them.
            }

            // Handle full page transitions in SPA
            if (location.href !== lastUrl) {
                lastUrl = location.href;
                // A small delay allows the new page content to render
                setTimeout(runAllFeatures, 250);
            }
        });

        observer.observe(document.body, { childList: true, subtree: true });
    }

    // Initial load
    loadModels();
    setTimeout(runAllFeatures, 1000); // Initial delay to ensure the site is ready
    observeDOM();

})();