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