Suno Control Panel

Integrated workflow tools, instrument library, Khmer UI translation, Play Button fix, and dynamic grid sizing.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

You will need to install an extension such as Tampermonkey to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==UserScript==
// @name         Suno Control Panel
// @namespace    http://tampermonkey.net/
// @version      1.0.7
// @author       LCR
// @icon         https://www.google.com/s2/favicons?sz=64&domain=suno.com
// @description  Integrated workflow tools, instrument library, Khmer UI translation, Play Button fix, and dynamic grid sizing.
// @match        https://suno.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @grant        GM_notification
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';
    console.log('Suno Control Panel is running...');

    // ==========================================
    // 1. TRANSLATION & UI CONFIGURATION
    // ==========================================
    const TARGET_LANG = 'km';
    const CACHE_KEY = `tm_translation_cache_suno`;
    const AUTO_TRANSLATE = false;
    const BATCH_DELAY = 500;
    const MAX_STRING_LENGTH = 35;
    let isAudioUploading = false;
    let isCopyrighted = false;

    // Google Sheet Keywords list
    const TRANSLATION_SHEET_URL = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vTUFLqfu5wNtggyqcG4fNhjulyUtiyVl3O3WqA4pAQVBD9b3O7cKSGrRXQzHEUtXz63Jn_i2CdUx5ah/pubhtml?gid=2086043567&single=true';

    const APPLY_CUSTOM_FONT = true;
    const FONT_FAMILY = "'Battambang', sans-serif";
    const FONT_SIZE = "14px";
    const LINE_HEIGHT = "1.6";

    if (APPLY_CUSTOM_FONT) {

        const fontLink = document.createElement('link');
        fontLink.href = 'https://fonts.googleapis.com/css2?family=Battambang:wght@400;700&display=swap';
        fontLink.rel = 'stylesheet';
        document.head.appendChild(fontLink);

        const style = document.createElement('style');
        style.textContent = `
            .khmer-translated-ui {
                font-family: ${FONT_FAMILY} !important;
                font-size: ${FONT_SIZE} !important;
                line-height: ${LINE_HEIGHT} !important;
            }
            a.text-sm.leading-5.font-medium.whitespace-nowrap.text-foreground-primary {
        font-family: ${FONT_FAMILY} !important;
        font-size: 20px !important;
        line-height: normal !important;
    }
        `;
        document.head.appendChild(style);
    }

    let MANUAL_OVERRIDES = {};
    let translationCache = {};
    const pendingStrings = new Set();
    const nodeMap = new Map();
    let batchTimeout = null;
    let isFetchingTranslations = false;


    try {
        const stored = localStorage.getItem(CACHE_KEY);
        if (stored) {

            translationCache = JSON.parse(stored);
        }
    } catch (e) {
        console.error("Failed to load translation cache:", e);
    }

    function saveCache() {
        try {
            localStorage.setItem(CACHE_KEY, JSON.stringify(translationCache));
        } catch (e) {}
    }

    GM_registerMenuCommand("Export Captured Strings JSON", () => {
        console.log("=== SUNO CAPTURED STRINGS ===");
        console.log(JSON.stringify(translationCache, null, 2));
    });

    // ==========================================
    // 2. WORKFLOW & INSTRUMENT PANEL VARIABLES
    // ==========================================
    let isAutoMode = true;
    let removeStringValue = "";
    let stepValue = 1;
    let recentRemovals = JSON.parse(localStorage.getItem('suno_recent_removals') || '[]');
    let isHidePromoMode = JSON.parse(localStorage.getItem('suno_hide_promo') || 'false');
    let isLocalSearchMode = JSON.parse(localStorage.getItem('suno_search_fixed') || 'true');
    let isWorkflowCollapsed = JSON.parse(localStorage.getItem('suno_workflow_collapsed') || 'false');

    let instrumentData = [];
    let savedFavs = localStorage.getItem('suno_instrument_favs');
    let favoriteInstruments = savedFavs ? JSON.parse(savedFavs) : ["Symphonic Orchestra", "Foxtrot"];
    let instrumentLimit = 10;
    let instrumentSearchQuery = "";
    let isFetchingInstruments = false;
    let gridObserver = null;

    let instrumentCardMinWidth = parseInt(localStorage.getItem('suno_instrument_card_width') || '80', 10);

    const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
    let cacheDate = parseInt(localStorage.getItem('suno_instrument_cache_date') || '0', 10);

    if (Date.now() - cacheDate > THIRTY_DAYS_MS) {
        localStorage.removeItem('suno_instrument_cache');
        localStorage.removeItem('suno_instrument_cache_date');
    } else {
        instrumentData = JSON.parse(localStorage.getItem('suno_instrument_cache') || '[]');
    }

    // ==========================================
    // 3. TRANSLATION ENGINE FUNCTIONS
    // ==========================================
    // Add 'msg' inside the parentheses here
    function sendUploadNotification(msg = "✅ ចប់ហើយ!") {
        const title = "Suno Control Panel";
        const iconUrl = "https://www.google.com/s2/favicons?sz=64&domain=suno.com";

        if (typeof GM_notification === "function") {
            GM_notification({
                title: title,
                text: msg, // This now uses whatever text you pass in
                image: iconUrl,
                //timeout: 5000,
                onclick: function() {
                    window.focus();
                }
            });
        } else {
            // Fallback to standard Web API
            if (Notification.permission === "granted") {
                const notif = new Notification(title, { body: msg, icon: iconUrl });
                notif.onclick = () => { window.focus(); notif.close(); };
                setTimeout(() => notif.close(), 3000);
            } else if (Notification.permission !== "denied") {
                Notification.requestPermission().then(permission => {
                    if (permission === "granted") sendUploadNotification(msg); // Pass it here too
                });
            }
        }
    }

    // --- NEW: Fetch Google Sheet Translations (UTF-8 CSV Method) ---
    function fetchTranslationsFromSheet() {
        if (isFetchingTranslations) return;
        isFetchingTranslations = true;

        // Use the CSV export URL
        const CSV_URL = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vTUFLqfu5wNtggyqcG4fNhjulyUtiyVl3O3WqA4pAQVBD9b3O7cKSGrRXQzHEUtXz63Jn_i2CdUx5ah/pub?gid=2086043567&single=true&output=csv';

        GM_xmlhttpRequest({
            method: "GET",
            url: CSV_URL,
            responseType: "arraybuffer", // Magic bullet to prevent encoding issues
            onload: function(response) {
                if (response.status === 200) {
                    // Manually decode the raw binary data as strict UTF-8
                    const decoder = new TextDecoder('utf-8');
                    const csvText = decoder.decode(response.response);

                    const lines = csvText.split('\n');
                    let newOverrides = {};

                    lines.forEach(line => {
                        const parts = line.split(',');
                        if (parts.length >= 2) {
                            const enText = parts[0].trim().toLowerCase();
                            const kmText = parts[1].trim();

                            // Skip the header row and ensure the text isn't blank
                            if (enText && kmText && enText !== 'English' && !enText.includes('SUNO Instrument List')) {
                                newOverrides[enText] = kmText;
                            }
                        }
                    });
                   // console.log("Khmer Translations Loaded:", newOverrides);
                    MANUAL_OVERRIDES = newOverrides;
                    translationCache = { ...translationCache, ...MANUAL_OVERRIDES };
                    saveCache();

                    // Instantly apply new translations to the visible page
                    walkDOM(document.body);
                }
                isFetchingTranslations = false;
            },
            onerror: function(err) {
                console.error("Failed to fetch Google Sheet translations:", err);
                isFetchingTranslations = false;
            }
        });
    }

    // Start fetching dictionary updates in the background immediately
    fetchTranslationsFromSheet();

    function translateBatch(stringsArray) {
        if (!stringsArray.length) return;

        const queries = stringsArray.map(s => `q=${encodeURIComponent(s)}`).join('&');
        const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${TARGET_LANG}&dt=t&${queries}`;

        GM_xmlhttpRequest({
            method: "GET",
            url: url,
            onload: function(response) {
                if (response.status === 200) {
                    const data = JSON.parse(response.responseText);
                    const translations = stringsArray.length === 1 ? [data[0][0][0]] : data[0].map(x => x[0]);

                    stringsArray.forEach((original, index) => {
                        const translatedText = translations[index] || original;
                        translationCache[original] = translatedText;
                        applyTranslationToDOM(original, translatedText);
                    });
                    saveCache();
                }
            }
        });
    }

    function applyTranslationToDOM(original, translated) {
        if (nodeMap.has(original)) {
            nodeMap.get(original).forEach(node => {
                if (node.textContent !== translated) {
                    node.textContent = translated;
                }
                if (APPLY_CUSTOM_FONT && node.parentElement) {
                    node.parentElement.classList.add('khmer-translated-ui');
                }
            });
        }
    }

    function processTextNode(node) {
        const text = node.textContent.trim();
        const searchKey = text.toLowerCase();

        if (!text || text.length < 2 || text.length > MAX_STRING_LENGTH) return;
        if (/^\d+$/.test(text) || /^[\s\W]+$/.test(text) || text.includes('http')) return;

        const parent = node.parentElement;
        if (!parent) return;
        const parentTag = parent.tagName;
        if (/^(SCRIPT|STYLE|CODE|PRE|NOSCRIPT|TEXTAREA|INPUT|OPTION)$/i.test(parentTag)) return;

        if (!nodeMap.has(text)) nodeMap.set(text, new Set());
        nodeMap.get(text).add(node);

        if (translationCache[searchKey] || translationCache[text]) {
            // Use the lowercase match if it exists, otherwise use the exact match
            node.textContent = translationCache[searchKey] || translationCache[text];

            if (APPLY_CUSTOM_FONT) {
                parent.classList.add('khmer-translated-ui');
            }
            return;
        }

        if (AUTO_TRANSLATE) {
            pendingStrings.add(text);
            clearTimeout(batchTimeout);
            batchTimeout = setTimeout(() => {
                const batch = Array.from(pendingStrings);
                pendingStrings.clear();
                const chunk = 15;
                for (let i = 0; i < batch.length; i += chunk) {
                    translateBatch(batch.slice(i, i + chunk));
                }
            }, BATCH_DELAY);
        } else {
            translationCache[text] = text;
        }
    }

    function walkDOM(node) {
        if (node.nodeType === Node.TEXT_NODE) {
            processTextNode(node);
        } else {
            for (let child of node.childNodes) {
                walkDOM(child);
            }
        }
    }

    // ==========================================
    // 4. WORKFLOW & UI FUNCTIONS
    // ==========================================
    function escapeRegExp(string) {
        return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }

    function saveRecentRemoval(val) {
        const trimmed = val.trim();
        if (!trimmed) return;
        recentRemovals = recentRemovals.filter(item => item !== trimmed);
        recentRemovals.unshift(trimmed);
        if (recentRemovals.length > 5) recentRemovals.pop();
        localStorage.setItem('suno_recent_removals', JSON.stringify(recentRemovals));
        updateDatalist();
    }

    function updateDatalist() {
        const dl = document.getElementById('suno-recent-removals');
        if (!dl) return;
        dl.innerHTML = '';
        recentRemovals.forEach(val => {
            const opt = document.createElement('option');
            opt.value = val;
            dl.appendChild(opt);
        });
    }

    function getExpectedTitle(baseTitle) {
        if (!baseTitle) return "";
        let processedTitle = baseTitle;

        if (removeStringValue) {
            const escapedStr = escapeRegExp(removeStringValue);
            const regex = new RegExp(escapedStr, 'gi');
            processedTitle = processedTitle.replace(regex, '');
            processedTitle = processedTitle.replace(/\s{2,}/g, ' ').trim();
        }

        const step = parseInt(stepValue, 10);
        if (isNaN(step) || step <= 0) return processedTitle;

        const match = processedTitle.match(/(^|.*?\D)(\d{1,3})$/);
        if (match) {
            const prefix = match[1];
            const numStr = match[2];
            const newNum = parseInt(numStr, 10) + step;
            let newNumStr = newNum.toString();

            if (numStr.startsWith('0') && newNumStr.length < numStr.length) {
                newNumStr = newNumStr.padStart(numStr.length, '0');
            }
            return prefix + newNumStr;
        }
        const spacer = processedTitle.endsWith(' ') ? '' : ' ';
        return processedTitle + spacer + step;
    }

    function setReactValue(inputElement, newValue) {
        let nativeInputValueSetter;
        if (inputElement.tagName === 'TEXTAREA') {
            nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
        } else {
            nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
        }
        if (nativeInputValueSetter) {
            nativeInputValueSetter.call(inputElement, newValue);
        }
        const tracker = inputElement._valueTracker;
        if (tracker) tracker.setValue("");
        inputElement.dispatchEvent(new Event('input', { bubbles: true }));
        inputElement.dispatchEvent(new Event('change', { bubbles: true }));
    }

    async function fetchInstruments() {
        if (isFetchingInstruments || instrumentData.length > 0) return;
        isFetchingInstruments = true;
        const url = 'https://docs.google.com/spreadsheets/d/e/2PACX-1vTUFLqfu5wNtggyqcG4fNhjulyUtiyVl3O3WqA4pAQVBD9b3O7cKSGrRXQzHEUtXz63Jn_i2CdUx5ah/pub?output=csv';
        try {
            const res = await fetch(url);
            const text = await res.text();
            const lines = text.split('\n').slice(1);
            instrumentData = lines.map(line => {
                const parts = line.split(',');
                return {
                    name: parts[0]?.trim(),
                    isBeta: parts[1]?.trim().toLowerCase() === 'beta',
                    img: parts[2]?.trim()
                };
            }).filter(item => item.name);
            localStorage.setItem('suno_instrument_cache', JSON.stringify(instrumentData));
            localStorage.setItem('suno_instrument_cache_date', Date.now().toString());
            renderInstrumentGrid();
        } catch (err) {
            console.error("Failed to fetch instruments:", err);
            isFetchingInstruments = false;
        }
    }

    function toggleFavorite(name, e) {
        e.stopPropagation();
        if (favoriteInstruments.includes(name)) {
            favoriteInstruments = favoriteInstruments.filter(n => n !== name);
        } else {
            favoriteInstruments.push(name);
        }
        localStorage.setItem('suno_instrument_favs', JSON.stringify(favoriteInstruments));
        renderInstrumentGrid();
    }

    function injectInstrumentToTextarea(name) {
        const textarea = document.querySelector('div.mb-0.pb-0.css-1a9916v.e1duxo6f0 textarea');
        if (!textarea) return;
        const currentVal = textarea.value.trim();
        const newVal = currentVal ? `${currentVal}, ${name}` : name;
        setReactValue(textarea, newVal);
    }

    function renderInstrumentGrid() {
        const grid = document.getElementById('suno-instrument-grid');
        if (!grid) return;
        grid.innerHTML = '';

        let filtered = instrumentData.filter(item =>
            item.name.toLowerCase().includes(instrumentSearchQuery)
        );

        filtered.sort((a, b) => {
            const aFav = favoriteInstruments.includes(a.name);
            const bFav = favoriteInstruments.includes(b.name);
            if (aFav && !bFav) return -1;
            if (!aFav && bFav) return 1;
            return a.name.localeCompare(b.name);
        });

        const visibleItems = filtered.slice(0, instrumentLimit);

        visibleItems.forEach(item => {
            const isFav = favoriteInstruments.includes(item.name);
            const card = document.createElement('div');
            card.style.cssText = `
                position: relative; height: ${instrumentCardMinWidth}px; border-radius: 6px; overflow: hidden;
                background-image: url('${item.img || ''}'); background-size: contain;
                background-repeat: no-repeat; background-position: center; background-color: #222;
                cursor: pointer; border: 1px solid #8b8fb5; transition: transform 0.1s, border-color 0.2s;
            `;
            card.onmouseover = () => card.style.borderColor = '#888';
            card.onmouseout = () => card.style.borderColor = '#8b8fb5';
            card.onclick = () => injectInstrumentToTextarea(item.name);

            const starBtn = document.createElement('div');
            starBtn.innerHTML = isFav ? '⭐' : '☆';
            starBtn.style.cssText = `
                position: absolute; top: 4px; right: 4px; width: 24px; height: 24px;
                border-radius: 50%; background: rgba(0,0,0,0.6); display: flex;
                align-items: center; justify-content: center; font-size: 14px;
                color: ${isFav ? '#fbbf24' : '#fff'}; transition: background 0.2s;
                opacity: ${isFav ? '1' : '0'};
            `;

            card.addEventListener('mouseenter', () => starBtn.style.opacity = '1');
            card.addEventListener('mouseleave', () => { if (!isFav) starBtn.style.opacity = '0'; });
            starBtn.onmouseover = () => starBtn.style.background = 'rgba(0,0,0,0.9)';
            starBtn.onmouseout = () => starBtn.style.background = 'rgba(0,0,0,0.6)';
            starBtn.onclick = (e) => toggleFavorite(item.name, e);

            const label = document.createElement('div');
            label.innerText = item.name;
            label.style.cssText = `
                position: absolute; bottom: 0; width: 100%; background: rgba(0,0,0,0.7);
                color: #fff; font-size: 11px; padding: 4px; text-align: center;
                box-sizing: border-box; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
            `;

            card.appendChild(starBtn);
            card.appendChild(label);
            grid.appendChild(card);
        });

        if (visibleItems.length < filtered.length) {
            const sentinel = document.createElement('div');
            sentinel.style.height = '10px';
            grid.appendChild(sentinel);

            if (gridObserver) gridObserver.disconnect();
            gridObserver = new IntersectionObserver((entries) => {
                if (entries[0].isIntersecting) {
                    instrumentLimit += 10;
                    renderInstrumentGrid();
                }
            });
            gridObserver.observe(sentinel);
        }
    }

    function injectInstrumentPanel() {
        if (document.getElementById('suno-instrument-panel')) return;
        const workflowPanel = document.getElementById('suno-rename-panel');
        if (!workflowPanel) return;

        const panel = document.createElement('div');
        panel.id = 'suno-instrument-panel';
        panel.style.cssText = `
            display: flex; flex-direction: column; gap: 8px; padding: 12px;
            margin: 0 12px 12px 12px; background-color: transparent; border: 1px solid #333;
            border-radius: 8px; font-family: sans-serif; font-size: 13px;
            flex: 1; min-height: 0;
        `;
		panel.classList.add('khmer-translated-ui');

        // --- NEW: Title Wrapper and Size Controls ---
        const titleWrapper = document.createElement('div');
        titleWrapper.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;';

        const title = document.createElement('div');
        title.innerHTML = '🎸 ឧបករណ៍ភ្លេង';
        title.style.fontWeight = '600';

        titleWrapper.appendChild(title);
        // ---------------------------------------------#############################

        // --- NEW: Horizontal Wrapper for Search and Size Controls ---
		const searchAndSizeWrapper = document.createElement('div');
		searchAndSizeWrapper.style.cssText = 'display: flex; flex-direction: row; justify-content: space-between; align-items: stretch; gap: 8px; width: 100%; margin-bottom: 4px;';

		const searchInput = document.createElement('input');
		searchInput.type = 'text';
		searchInput.placeholder = 'ស្វែងរក...';
		// Using flex: 6.5 takes up roughly 65% of the space
		searchInput.style.cssText = `
			padding: 6px; background: #c4c9ff; border: 1px solid #8b8fb5;
			border-radius: 4px; color: black; font-size: 12px; flex: 6.5; box-sizing: border-box; min-width: 0 !important;
		`;
		searchInput.oninput = (e) => {
			instrumentSearchQuery = e.target.value.toLowerCase();
			instrumentLimit = 10;
			renderInstrumentGrid();
		};

		const sizeControls = document.createElement('div');
		sizeControls.style.cssText = 'display: flex; gap: 4px; align-items: center; flex: 3.5;'; // Using flex: 3.5 takes up the remaining 35% of the space
		// Adjusted height to 28px to prevent Khmer font clipping, and used flex: 1 so both buttons share the 35% width evenly
		const btnStyle = 'background: #c4c9ff; border: 1px solid #8b8fb5; color: #000; flex: 1; height: 33px; border-radius: 4px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s;';

		const btnMinus = document.createElement('button');
		btnMinus.innerText = 'តូច';
		btnMinus.style.cssText = btnStyle;
		btnMinus.onmouseover = () => btnMinus.style.background = '#8b8fb5';
		btnMinus.onmouseout = () => btnMinus.style.background = '#c4c9ff';

		const btnPlus = document.createElement('button');
		btnPlus.innerText = 'ធំ';
		btnPlus.style.cssText = btnStyle;
		btnPlus.onmouseover = () => btnPlus.style.background = '#8b8fb5';
		btnPlus.onmouseout = () => btnPlus.style.background = '#c4c9ff';

		sizeControls.appendChild(btnMinus);
		sizeControls.appendChild(btnPlus);

		searchAndSizeWrapper.appendChild(searchInput);
		searchAndSizeWrapper.appendChild(sizeControls);

        const grid = document.createElement('div');
        grid.id = 'suno-instrument-grid';
        grid.style.cssText = `
            display: grid; grid-template-columns: repeat(auto-fit, minmax(${instrumentCardMinWidth}px, 1fr));
            gap: 8px; flex: 1; min-height: 0; overflow-y: auto; padding-right: 4px;
            margin-top: 4px; align-content: start;
        `;

        btnPlus.onclick = () => {
            instrumentCardMinWidth += 20;
            if (instrumentCardMinWidth > 600) instrumentCardMinWidth = 600; // safety max
            localStorage.setItem('suno_instrument_card_width', instrumentCardMinWidth.toString());
            grid.style.gridTemplateColumns = `repeat(auto-fit, minmax(${instrumentCardMinWidth}px, 1fr))`;
            renderInstrumentGrid(); // Re-render to update the height of the cards to match the new width
        };

        btnMinus.onclick = () => {
            instrumentCardMinWidth -= 20;
            if (instrumentCardMinWidth < 40) instrumentCardMinWidth = 40; // minimum readable size
            localStorage.setItem('suno_instrument_card_width', instrumentCardMinWidth.toString());
            grid.style.gridTemplateColumns = `repeat(auto-fit, minmax(${instrumentCardMinWidth}px, 1fr))`;
            renderInstrumentGrid(); // Re-render to update the height
        };
        // -------------------------------

        const footer = document.createElement('div');
        footer.style.cssText = 'display: flex; justify-content: flex-start; margin-top: 6px;';

        const clearCacheBtn = document.createElement('button');
        clearCacheBtn.innerHTML = '🔄 លុបទិន្នន័យ';
        clearCacheBtn.style.cssText = `
            padding: 4px 10px; background: transparent; border: 1px solid #8b8fb5; border-radius: 6px;
             font-size: 11px; cursor: pointer; transition: all 0.2s;
        `;
        clearCacheBtn.onmouseover = () => { clearCacheBtn.style.background = '#ff6179'; };
        clearCacheBtn.onmouseout = () => { clearCacheBtn.style.background = 'transparent'; };
        clearCacheBtn.onclick = () => {
            localStorage.removeItem('suno_instrument_cache');
            localStorage.removeItem('suno_instrument_cache_date');
            instrumentData = [];
            isFetchingInstruments = false;

            grid.innerHTML = '<div style="text-align:center; padding: 20px; color:#aaa;">Reloading...</div>';
            fetchInstruments();
        };
        footer.appendChild(clearCacheBtn);


        panel.appendChild(titleWrapper);
		panel.appendChild(searchAndSizeWrapper);
        panel.appendChild(grid);
        panel.appendChild(footer);

        workflowPanel.parentNode.insertBefore(panel, workflowPanel.nextSibling);

        fetchInstruments();
        if (instrumentData.length > 0) {
            renderInstrumentGrid();
        }
    }

    function triggerAutoRename() {
        const sourceDiv = document.querySelector('div.css-14p9rp6.e5u4t8c8');
        const inputWrapper = document.querySelector('div.css-2nnn4s.ee0qkbv1');
        if (!sourceDiv || !inputWrapper) return;
        const titleInput = inputWrapper.querySelector('input');
        if (!titleInput) return;

        if (removeStringValue) saveRecentRemoval(removeStringValue);
        const expected = getExpectedTitle(sourceDiv.innerText.trim());

        navigator.clipboard.writeText(expected).catch(err => {
            console.error("Suno Auto-Rename: Failed to copy to clipboard", err);
        });

        setReactValue(titleInput, expected);
    }

    function injectPanel() {
        if (document.getElementById('suno-rename-panel')) return;
        const targetSidebar = document.querySelector('div.min-h-2.flex-1');
        if (!targetSidebar) return;

        targetSidebar.style.display = 'flex';
        targetSidebar.style.flexDirection = 'column';

        const panel = document.createElement('div');
        panel.id = 'suno-rename-panel';
        panel.style.cssText = `
            display: flex; flex-direction: column; padding: 12px; margin: 0 12px 12px 12px;
            background-color: transparent; border: 1px solid #333; border-radius: 8px;
            font-size: 13px; flex-shrink: 0;
        `;
		panel.classList.add('khmer-translated-ui');

        const titleBar = document.createElement('div');
        titleBar.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;';

        const titleText = document.createElement('div');
        titleText.innerHTML = '⚙️ ផ្ទាំងកែឈ្មោះ';
        titleText.style.fontWeight = '600';

        const toggleBtn = document.createElement('button');
        toggleBtn.innerHTML = isWorkflowCollapsed ? '▼' : '▲';
        toggleBtn.style.cssText = 'background: none; border: none; color: #aaa; cursor: pointer; font-size: 14px; padding: 0 4px;';

        titleBar.appendChild(titleText);
        titleBar.appendChild(toggleBtn);
        panel.appendChild(titleBar);

        const contentWrapper = document.createElement('div');
        contentWrapper.style.cssText = `display: ${isWorkflowCollapsed ? 'none' : 'flex'}; flex-direction: column; gap: 12px;`;

        toggleBtn.onclick = () => {
            isWorkflowCollapsed = !isWorkflowCollapsed;
            localStorage.setItem('suno_workflow_collapsed', JSON.stringify(isWorkflowCollapsed));
            contentWrapper.style.display = isWorkflowCollapsed ? 'none' : 'flex';
            toggleBtn.innerHTML = isWorkflowCollapsed ? '▼' : '▲';
        };

        const datalist = document.createElement('datalist');
        datalist.id = 'suno-recent-removals';
        panel.appendChild(datalist);

        const label = document.createElement('div');
        label.id = 'suno-comparison-label';
        label.style.fontWeight = '600';
        label.style.textAlign = 'center';

        const removeWrapper = document.createElement('div');
        removeWrapper.style.cssText = 'display: flex; flex-direction: row; align-items: center; gap: 8px; width: 100% !important;';

        const removeLabel = document.createElement('label');
        removeLabel.innerText = '🗑️ លុបពាក្យ';
        removeLabel.style.cssText = 'font-size: 13px; white-space: nowrap;';// Added white-space: nowrap to prevent the text from breaking into two lines

        const removeInput = document.createElement('input');
        removeInput.type = 'text';
        removeInput.setAttribute('list', 'suno-recent-removals');
        removeInput.placeholder = 'ឧ. "FINAL MASTERED"';
        removeInput.value = removeStringValue;
        // Added flex: 1 to fill remaining space, box-sizing, and height: 35px for UI consistency
        removeInput.style.cssText = `
    padding: 6px; background: #c4c9ff; border: 1px solid #8b8fb5;
    border-radius: 4px; color: black; font-size: 12px;
    flex: 1; height: 35px; box-sizing: border-box; text-align: center; min-width: 0 !important;
`;
        removeInput.oninput = (e) => { removeStringValue = e.target.value; };
        removeInput.onchange = (e) => { saveRecentRemoval(e.target.value); };

        removeWrapper.appendChild(removeLabel);
        removeWrapper.appendChild(removeInput);

        // --- NEW: Horizontal Wrapper for Input and Button ---
		const inlineWrapper = document.createElement('div');
        // Changed align-items to 'center' so the row is perfectly vertically aligned
        inlineWrapper.style.cssText = 'display: flex; flex-direction: row; align-items: center; gap: 8px; width: 100%; margin-top: 4px;';

        const stepWrapper = document.createElement('div');
        // Changed flex-direction to 'row', added 'align-items: center', and gave it slightly more flex space (4)
        stepWrapper.style.cssText = 'display: flex; flex-direction: row; align-items: center; gap: 6px; flex: 4;';

        const stepLabel = document.createElement('label');
        stepLabel.innerText = '🔢 បូក';
        stepLabel.style.cssText = 'font-size: 13px; white-space: nowrap;';

        const stepInput = document.createElement('input');
        stepInput.type = 'number';
        stepInput.value = stepValue;
        stepInput.style.cssText = `
    padding: 6px; background: #c4c9ff; border: 1px solid #8b8fb5;
    border-radius: 4px; color: black; font-size: 12px; width: 100%;
    box-sizing: border-box; flex: 1; height: 35px; text-align: center; /* Matched height to the copy button */
`;
        stepInput.oninput = (e) => { stepValue = e.target.value; };

        stepWrapper.appendChild(stepLabel);
        stepWrapper.appendChild(stepInput);

		const btnCopy = document.createElement('button');
        btnCopy.id = 'goBtn';
        btnCopy.innerText = '📝 ដាក់ឈ្មោះ';
        btnCopy.style.cssText = `
    padding: 8px; background: #8b5cf6; border: 1px solid #a78bfa;
    border-radius: 6px; cursor: pointer; transition: all 0.2s;
    font-weight: 600; flex: 7; height: 35px; display: flex; align-items: center; justify-content: center;
    box-shadow: 0 0 10px rgba(139, 92, 246, 0.5);
`;

        // Updated hover effects to make the violet glow brighter when hovered
        btnCopy.onmouseover = () => {
            btnCopy.style.background = '#7c3aed';
            btnCopy.style.boxShadow = '0 0 15px rgba(139, 92, 246, 0.8)';
        };
        btnCopy.onmouseout = () => {
            btnCopy.style.background = '#8b5cf6';
            btnCopy.style.boxShadow = '0 0 10px rgba(139, 92, 246, 0.5)';
        };
        btnCopy.onclick = (e) => { e.preventDefault(); triggerAutoRename(); };

		// Append both to the horizontal wrapper
		inlineWrapper.appendChild(stepWrapper);
		inlineWrapper.appendChild(btnCopy);

        const autoToggleWrapper = document.createElement('label');
        autoToggleWrapper.style.cssText = `display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: 13px;`;
        const autoToggle = document.createElement('input');
        autoToggle.type = 'checkbox';
        autoToggle.checked = isAutoMode;
        autoToggle.onchange = (e) => { isAutoMode = e.target.checked; };
        autoToggleWrapper.appendChild(autoToggle);
        autoToggleWrapper.appendChild(document.createTextNode('🤖 អូតូដាក់ឈ្មោះ'));

        const promoToggleWrapper = document.createElement('label');
        promoToggleWrapper.style.cssText = `display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: 13px; margin-top: 4px;`;
        const promoToggle = document.createElement('input');
        promoToggle.type = 'checkbox';
        promoToggle.checked = isHidePromoMode;
        promoToggle.onchange = (e) => {
            isHidePromoMode = e.target.checked;
            localStorage.setItem('suno_hide_promo', JSON.stringify(isHidePromoMode));
        };
        promoToggleWrapper.appendChild(promoToggle);
        promoToggleWrapper.appendChild(document.createTextNode('🚫 លាក់ផ្ទាំង Ads'));

        const searchToggleWrapper = document.createElement('label');
        searchToggleWrapper.style.cssText = `display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: 13px; margin-top: 4px;`;
        const searchToggle = document.createElement('input');
        searchToggle.type = 'checkbox';
        searchToggle.checked = isLocalSearchMode;
        searchToggle.onchange = (e) => {
            isLocalSearchMode = e.target.checked;
            if (!isLocalSearchMode) {
                document.querySelectorAll('div[data-testid="clip-row"]').forEach(r => r.style.display = 'flex');
                const box = document.querySelector('input[aria-label="ស្វែងរកចម្រៀង"]');
                if (box) box.style.boxShadow = '';
            }
			localStorage.setItem('suno_search_fixed', JSON.stringify(isLocalSearchMode));
        };
        searchToggleWrapper.appendChild(searchToggle);
        searchToggleWrapper.appendChild(document.createTextNode('🔍 ជួលជុស Search'));

        contentWrapper.appendChild(label);
        contentWrapper.appendChild(removeWrapper);
		contentWrapper.appendChild(inlineWrapper);
        contentWrapper.appendChild(autoToggleWrapper);
        contentWrapper.appendChild(promoToggleWrapper);
        contentWrapper.appendChild(searchToggleWrapper);

        panel.appendChild(contentWrapper);
        targetSidebar.appendChild(panel);

        updateDatalist();
    }

    function enhanceListUI() {
        //const draggables = document.querySelectorAll('div[draggable="true"].css-u0rgu7.e1vhawg90, div[role="rowgroup"] div[draggable="true"]');
        //draggables.forEach(el => el.setAttribute('draggable', 'false'));

        const rows = document.querySelectorAll('div[data-testid="clip-row"]');
        rows.forEach(row => {
            if (!row.dataset.uiFixed) {
                const multiSelect = row.querySelector('.multi-select-button');
                if (multiSelect) {
                    multiSelect.style.height = '100%';
                    multiSelect.style.display = 'flex';
                    multiSelect.style.alignItems = 'center';
                    const btn = multiSelect.querySelector('button');
                    if (btn) {
                        btn.style.height = '100%';
                        btn.style.minWidth = '40px';
                    }
                }
                row.dataset.uiFixed = "true";
            }

            if (!row.dataset.clickBound) {
                row.addEventListener('click', (e) => {
                    const isInteractive = e.target.closest('button, a, input, [role="button"], [aria-label="Play" i], [aria-label="Pause" i], svg');
                    if (isInteractive) return;

                    e.stopPropagation();
                    e.preventDefault();

                    const checkboxBtn = row.querySelector('.multi-select-button button');
                    if (checkboxBtn) checkboxBtn.click();
                }, true);
                row.dataset.clickBound = "true";
            }

            const actionHoverDiv = row.querySelector('.hover-fade-in.css-1ufejka');
            if (actionHoverDiv && !row.dataset.customBtnsInjected) {

                const btnStyle = `
                    padding: 4px 8px; margin-right: 6px; background: rgba(0,0,0,0.1);
                    border: 1px solid #8b8fb5; border-radius: 12px;
                    font-size: 11px; cursor: pointer; display: flex;
                    align-items: center; justify-content: center; height: 24px;
                    transition: all 0.2s;
                    `;

                const btnCopyName = document.createElement('button');
                btnCopyName.innerText = "📋 COPYឈ្មោះ";
                btnCopyName.style.cssText = btnStyle;
                btnCopyName.classList.add('khmer-translated-ui');
                btnCopyName.onmouseover = () => btnCopyName.style.borderColor = "#8B5CF6";
                btnCopyName.onmouseout = () => btnCopyName.style.borderColor = "#8b8fb5";

                btnCopyName.onclick = (e) => {
                    e.stopPropagation();
                    e.preventDefault();
                    const titleWrapper = row.querySelector('.clip-title-wrapper');
                    if (titleWrapper) {
                        navigator.clipboard.writeText(titleWrapper.innerText.trim());
                        btnCopyName.innerText = "✓ Copied";
                        btnCopyName.style.color = "#4ade80";
                        setTimeout(() => {
                            btnCopyName.innerText = "📋 COPYឈ្មោះ";
                            btnCopyName.style.color = "#ccc";
                        }, 1500);
                    }
                };

                const btnAutoRename = document.createElement('button');
                btnAutoRename.innerText = "✏️ កែឈ្មោះ";
                btnAutoRename.style.cssText = btnStyle;
                btnAutoRename.classList.add('khmer-translated-ui');
                btnAutoRename.onmouseover = () => btnAutoRename.style.borderColor = "#8B5CF6";
                btnAutoRename.onmouseout = () => btnAutoRename.style.borderColor = "#8b8fb5";

                btnAutoRename.onclick = async (e) => {
                    e.stopPropagation();
                    e.preventDefault();

                    try {
                        let clipboardText = await navigator.clipboard.readText();

                        if (!clipboardText || clipboardText.trim() === "") {
                            console.warn('Clipboard is empty.');
                            //return;
                        }

                        if (clipboardText.length > 50) {
                            clipboardText = clipboardText.substring(0, 50) + "...";
                        }

                        const nativeRenameBtn = row.querySelector('.edit-icon-wrapper');
                        if (nativeRenameBtn) {
                            nativeRenameBtn.click();

                            let attempts = 0;
                            const findInputInterval = setInterval(() => {
                                attempts++;
                                const editInput = row.querySelector('input[type="text"]');

                                if (editInput) {
                                    clearInterval(findInputInterval);
                                    editInput.focus();
                                    setReactValue(editInput, clipboardText.trim());
                                } else if (attempts > 10) {
                                    clearInterval(findInputInterval);
                                    console.warn("Suno Auto-Rename: React input never spawned.");
                                }
                            }, 50);
                        }
                    } catch (err) {
                        console.error('Failed to read clipboard: ', err);
                        alert('Could not read from clipboard. Please ensure clipboard permissions are allowed.');
                    }
                };

                actionHoverDiv.prepend(btnAutoRename);
                actionHoverDiv.prepend(btnCopyName);

                row.dataset.customBtnsInjected = "true";
            }
        });
    }

    function handlePromoVisibility() {
        const promoPanel = document.querySelector('div.flex.h-full.flex-col.overflow-y-hidden > div.bg-background-primary');
        if (promoPanel) {
            promoPanel.style.display = isHidePromoMode ? 'none' : '';
        }
        const sidebarBottomLinks = document.querySelector('div.min-h-2.flex-1 + div.flex.flex-col.gap-px.px-3');
        if (sidebarBottomLinks) {
            sidebarBottomLinks.style.display = isHidePromoMode ? 'none' : '';
        }
    }

    function hijackSearchBox() {
        const searchInput = document.querySelector('input[aria-label="Search clips"], input[aria-label="ស្វែងរកចម្រៀង"]');
        if (!searchInput) return;

        if (!searchInput.dataset.hijackBound) {
            searchInput._localSearchHandler = function(e) {
                if (!isLocalSearchMode) return;

                // Block React from seeing the typing
                e.stopPropagation();

                // Prevent the Enter key from submitting Suno's native backend search
                if (e.key === 'Enter') {
                    e.preventDefault();
                }
            };

            // Bind to all keystroke events in the Capture phase
            ['input', 'change', 'keydown', 'keyup', 'keypress'].forEach(evt => {
                searchInput.addEventListener(evt, searchInput._localSearchHandler, true);
            });
            searchInput.dataset.hijackBound = "true";
        }

        if (isLocalSearchMode) {
            searchInput.style.boxShadow = "0 0 5px #4ade80";
        } else {
            searchInput.style.boxShadow = "";
        }
    }

    function enforceLocalSearchFilter() {
        if (!isLocalSearchMode) return;

        const searchInput = document.querySelector('input[aria-label="Search clips"], input[aria-label="ស្វែងរកចម្រៀង"]');
        if (!searchInput) return;

        const searchTerm = searchInput.value.toLowerCase().trim();
        const rows = document.querySelectorAll('div[data-testid="clip-row"]');

        rows.forEach(row => {
            // If the search box is empty, show everything
            if (searchTerm === "") {
                row.style.display = 'flex';
                return;
            }

            // Grab the standard text from the row
            let text = row.innerText.toLowerCase();

            // If the row is in "edit mode", grab the text from the input field too
            const editInput = row.querySelector('input[type="text"]');
            if (editInput && editInput.value) {
                text += " " + editInput.value.toLowerCase();
            }

            // Enforce visibility
            row.style.display = text.includes(searchTerm) ? 'flex' : 'none';
        });
    }

    function makeSidebarResizable() {
        // Target the sidebar container (the element with the width style)
        const sidebar = document.querySelector('div.relative.h-full.transition-\\[width\\]');
        if (!sidebar || document.getElementById('suno-custom-splitter')) return;

        // Prevent native CSS resize since we are building a custom handle
        sidebar.style.resize = 'none';

        // Create the draggable splitter handle
        const splitter = document.createElement('div');
        splitter.id = 'suno-custom-splitter';
        splitter.style.cssText = `
        width: 5px;
        height: 100%;
        background-color: transparent;
        position: absolute;
        right: -3px;
        top: 0;
        cursor: col-resize;
        z-index: 9999;
        transition: background-color 0.2s;
    `;

        // Visual hover effect so the user knows they can grab it
        splitter.onmouseover = () => splitter.style.backgroundColor = '#8b5cf6';
        splitter.onmouseout = () => { if (!isDragging) splitter.style.backgroundColor = 'transparent'; };

        let isDragging = false;

        splitter.addEventListener('mousedown', (e) => {
            isDragging = true;
            splitter.style.backgroundColor = '#8b5cf6';
            document.body.style.cursor = 'col-resize';
            e.preventDefault();
        });

        document.addEventListener('mousemove', (e) => {
            if (!isDragging) return;

            // Calculate new width based on mouse X position relative to viewport
            const newWidth = e.clientX;

            // Set min (200px) and max (600px) boundaries so it doesn't break the layout
            if (newWidth >= 200 && newWidth <= 600) {
                sidebar.style.width = `${newWidth}px`;
                localStorage.setItem('suno_sidebar_width', `${newWidth}px`);
            }
        });

        document.addEventListener('mouseup', () => {
            if (isDragging) {
                isDragging = false;
                splitter.style.backgroundColor = 'transparent';
                document.body.style.cursor = 'default';
            }
        });

        // Append the splitter to the sidebar element
        sidebar.style.position = 'relative'; // Ensures absolute positioning works for the splitter
        sidebar.appendChild(splitter);

        // Restore saved width from localStorage if it exists
        const savedWidth = localStorage.getItem('suno_sidebar_width');
        if (savedWidth) {
            sidebar.style.width = savedWidth;
        }
    }

    function addGlobalStyle(css) {
        var head, style;
        head = document.getElementsByTagName('head')[0];
        if (!head) { return; }
        style = document.createElement('style');
        style.type = 'text/css';
        style.innerHTML = css;
        head.appendChild(style);
    }
    addGlobalStyle(`
    #goBtn {
        padding: 8px;
        background: #8b5cf6;
        border: 1px solid #a78bfa;
        border-radius: 6px;
        cursor: pointer;
        color: white;
        transition: all 0.2s;
        font-weight: 600;
        flex: 7;
        height: 35px;
        display: flex;
        align-items: center;
        justify-content: center;
        box-shadow: 0 0 10px rgba(139, 92, 246, 0.5);
    }
    #goBtn:hover {
        background: #7c3aed;
        box-shadow: 0 0 15px rgba(139, 92, 246, 0.8);
    }
`);

    // ==========================================
    // 5. INITIALIZATION & OBSERVERS
    // ==========================================

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => walkDOM(document.body));
    } else {
        walkDOM(document.body);
    }

    const translationObserver = new MutationObserver((mutations) => {
        for (let mutation of mutations) {
            mutation.addedNodes.forEach(node => walkDOM(node));
        }
    });
    translationObserver.observe(document, { childList: true, subtree: true });

    setInterval(() => {
        // --- NEW: Audio Upload Monitor ---
        const uploadSpan = document.querySelector('div[role="dialog"][data-open] span.text-sm.text-foreground-secondary');
        const copyrightBox = document.getElementById("_r_0_"); // Could change

        if (uploadSpan) {
            //console.log('UploadSpan Found...');

            const statusText = uploadSpan.textContent.trim().toLowerCase();

            if (statusText.includes('uploading') || statusText.includes('កំពុង')) {
                isAudioUploading = true; //Start the session.
                //console.log('Detect an Upload on going...');
            } else if ((statusText.includes('uploaded') || statusText.includes('រួចរាល់')) && isAudioUploading) {
                isAudioUploading = false;
                sendUploadNotification("✅ Upload ចប់ហើយ!");
            }
        } else if (isAudioUploading) {
            //issue
            if (copyrightBox) {
                sendUploadNotification("❌ ជាប់ Copyright...");
            }
            isAudioUploading = false; //Close session.
        } else {
            //console.log('UploadSpan NOT found');
            isAudioUploading = false; // If the panel is closed, reset the state
        }

        injectPanel();
        injectInstrumentPanel();
        enhanceListUI();
        handlePromoVisibility();
        hijackSearchBox();
        enforceLocalSearchFilter();
        makeSidebarResizable();

        const label = document.getElementById('suno-comparison-label');
        if (!label) return;

        const sourceDiv = document.querySelector('div.css-14p9rp6.e5u4t8c8');
        const inputWrapper = document.querySelector('div.css-2nnn4s.ee0qkbv1');

        const sourceText = sourceDiv ? sourceDiv.innerText.trim() : "";
        const titleInput = inputWrapper ? inputWrapper.querySelector('input') : null;
        const currentInputText = titleInput ? titleInput.value.trim() : "";

        if (!sourceText) {
            label.innerText = "🎵 មិនមានសំឡេង";
            label.style.color = "#bf4141";
            return;
        }

        const expectedText = getExpectedTitle(sourceText);

        if (currentInputText.toLowerCase() === expectedText.toLowerCase()) {
            label.innerText = "✓ ដាក់ឈ្មោះរួច";
            label.style.color = "#4ade80";
        } else {
            label.innerText = "⚠️ ឈ្មោះមិនត្រូវគ្នា";
            label.style.color = "#cf6700";

            if (isAutoMode && currentInputText.toLowerCase() === sourceText.toLowerCase() && titleInput) {
                navigator.clipboard.writeText(expectedText).catch(err => console.error(err));
                setReactValue(titleInput, expectedText);
            }
        }



    }, 500);

})();