Integrated workflow tools, instrument library, Khmer UI translation, Play Button fix, and dynamic grid sizing.
// ==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);
})();