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