`;
document.body.insertAdjacentHTML('beforeend', formHtml);
// Focus on the label input when form opens
document.getElementById('newButtonLabel').focus();
document.getElementById('saveNewButton').addEventListener('click', () => {
const label = document.getElementById('newButtonLabel').value.trim();
const text = document.getElementById('newButtonText').value.trim();
const color = document.getElementById('newButtonColor').value.trim();
if (label && text) {
config.buttons.push({
id: 'custom_' + Date.now(),
label,
text,
bgColor: color
});
renderCustomButtonsList(config);
document.getElementById('newButtonForm').remove();
document.getElementById('newButtonFormOverlay').remove();
scrollToManagerBottom(); // Scroll to bottom after adding
} else {
alert('Both label and text are required');
}
});
document.getElementById('cancelNewButton').addEventListener('click', () => {
document.getElementById('newButtonForm').remove();
document.getElementById('newButtonFormOverlay').remove();
});
document.getElementById('closeNewButtonForm').addEventListener('click', () => {
document.getElementById('newButtonForm').remove();
document.getElementById('newButtonFormOverlay').remove();
});
document.getElementById('newButtonFormOverlay').addEventListener('click', () => {
document.getElementById('newButtonForm').remove();
document.getElementById('newButtonFormOverlay').remove();
});
}
function editButtonForm(config, index) {
const button = config.buttons[index];
const formHtml = `
Edit Button
`;
document.body.insertAdjacentHTML('beforeend', formHtml);
// Focus on the label input when form opens
document.getElementById('editButtonLabel').focus();
document.getElementById('saveEditedButton').addEventListener('click', () => {
const label = document.getElementById('editButtonLabel').value.trim();
const text = document.getElementById('editButtonText').value.trim();
const color = document.getElementById('editButtonColor').value.trim();
if (label && text) {
button.label = label;
button.text = text;
button.bgColor = color;
renderCustomButtonsList(config);
document.getElementById('editButtonForm').remove();
document.getElementById('editButtonFormOverlay').remove();
scrollToManagerBottom(); // Scroll to bottom after editing
} else {
alert('Both label and text are required');
}
});
document.getElementById('cancelEditButton').addEventListener('click', () => {
document.getElementById('editButtonForm').remove();
document.getElementById('editButtonFormOverlay').remove();
});
document.getElementById('closeEditButtonForm').addEventListener('click', () => {
document.getElementById('editButtonForm').remove();
document.getElementById('editButtonFormOverlay').remove();
});
document.getElementById('editButtonFormOverlay').addEventListener('click', () => {
document.getElementById('editButtonForm').remove();
document.getElementById('editButtonFormOverlay').remove();
});
}
function simulateTyping(el, value) {
if (!el) return;
el.focus();
const prototype = Object.getPrototypeOf(el);
const descriptor = Object.getOwnPropertyDescriptor(prototype, 'value');
descriptor.set.call(el, value);
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: value.slice(-1) || 'a' }));
el.focus();
}
// Comprehensive selector for the suggestion textarea
function getSuggestionTextarea() {
return document.querySelector(
'textarea[aria-label="Suggestion textbox"], ' +
'textarea[placeholder="suggestion textbox"], ' +
'textarea[placeholder="Your suggestion text..."], ' +
'textarea[aria-label="Suggestion contents textbox"], ' + // Keep this, as HTML confirms it exists
'textarea[placeholder*="(Optional suggestion)"]' // Add a more robust placeholder match
);
}
function clearTextarea() {
const textarea = getSuggestionTextarea();
if (textarea) {
simulateTyping(textarea, '');
}
}
function appendSuggestion(text) {
const textarea = getSuggestionTextarea();
if (!textarea) return;
const currentValue = textarea.value.trim();
let newValue = '';
if (currentValue) {
const cleanedValue = currentValue.replace(/[.,;!?]+$/, '');
const potentialNewValue = `${cleanedValue}, ${text}`;
if (potentialNewValue.length <= MAX_CHARS) {
newValue = potentialNewValue;
} else {
const availableSpace = MAX_CHARS - cleanedValue.length - 2;
if (availableSpace > 10) {
newValue = `${cleanedValue}, ${text.substring(0, availableSpace)}`;
} else {
newValue = cleanedValue;
}
}
} else {
newValue = text.length > MAX_CHARS ? text.substring(0, MAX_CHARS) : text;
}
simulateTyping(textarea, newValue);
}
function saveCurrentText() {
const textarea = getSuggestionTextarea();
if (textarea) {
const text = textarea.value.trim();
if (text) {
GM_setValue(STORAGE_KEY, text);
updateLastButton();
}
}
}
function restoreLastText() {
const textarea = getSuggestionTextarea();
if (textarea) {
const lastSavedText = GM_getValue(STORAGE_KEY, '');
if (lastSavedText) {
const restored = lastSavedText.length > MAX_CHARS ? lastSavedText.substring(0, MAX_CHARS) : lastSavedText;
simulateTyping(textarea, restored);
}
}
}
function updateLastButton() {
const lastBtn = document.getElementById('lastSuggestionBtn');
if (lastBtn) {
const lastSavedText = GM_getValue(STORAGE_KEY, '');
lastBtn.style.display = 'inline-flex';
lastBtn.title = lastSavedText ? `${lastSavedText.length}/${MAX_CHARS} chars` : 'No saved text';
}
}
// Helper to find the currently visible button row
function getActiveButtonRow() {
const allButtonRows = document.querySelectorAll('div.css-l3vyye');
for (const row of allButtonRows) {
// offsetParent is null for elements that are not rendered (hidden by display:none, or not in DOM)
if (row.offsetParent !== null) {
return row;
}
}
return null; // No visible button row found
}
// Modified to accept the specific buttonRow to work on
function createCustomButtons(buttonRowToModify) {
if (!buttonRowToModify) {
console.warn("Kindroid Suggestion Buttons: createCustomButtons called without a valid buttonRow to modify.");
return false;
}
// Hide the original button row
buttonRowToModify.style.display = 'none';
// Remove existing custom buttons IF they are directly associated with THIS buttonRowToModify
const existingContainer = buttonRowToModify.previousElementSibling;
if (existingContainer && existingContainer.classList.contains('custom-button-container')) {
existingContainer.remove();
}
// Create a new container for our custom buttons
const customButtonContainer = document.createElement('div');
customButtonContainer.className = 'custom-button-container'; // Add a class for easy identification
customButtonContainer.style.display = 'flex';
customButtonContainer.style.flexDirection = 'column';
customButtonContainer.style.gap = '6px';
customButtonContainer.style.margin = '6px 0';
customButtonContainer.style.maxWidth = MAX_CONTAINER_WIDTH + 'px';
// Find the parent container where the original buttons were
const parentContainer = buttonRowToModify.parentElement;
if (!parentContainer) return false;
// Insert our custom container where the original buttons were
parentContainer.insertBefore(customButtonContainer, buttonRowToModify);
// Get current config
const config = loadButtonConfig();
// Create buttons from config
let currentLine = document.createElement('div');
currentLine.style.display = 'flex';
currentLine.style.flexWrap = 'wrap';
currentLine.style.gap = '6px';
currentLine.style.alignItems = 'center';
customButtonContainer.appendChild(currentLine);
let currentLineWidth = 0;
config.buttons.forEach(btn => {
const button = document.createElement('button');
button.className = 'chakra-button css-yhzben custom-suggestion-btn';
button.id = btn.id === 'last' ? 'lastSuggestionBtn' : btn.id === 'clear' ? 'clearSuggestionBtn' : '';
button.textContent = btn.label;
const tempButton = document.createElement('span'); // Use span for inline measurement
tempButton.style.visibility = 'hidden';
tempButton.style.whiteSpace = 'nowrap';
tempButton.style.font = getComputedStyle(button).font; // Match font for accurate width
document.body.appendChild(tempButton);
tempButton.textContent = btn.label; // Set text content after appending to get computed font
const estimatedButtonWidth = tempButton.offsetWidth + 20; // Estimate with some padding/margin
document.body.removeChild(tempButton); // Remove the temporary element after measurement
if (btn.bgColor) {
button.style.backgroundColor = btn.bgColor;
}
if (btn.id === 'clear') {
button.addEventListener('click', clearTextarea);
} else if (btn.id === 'last') {
button.addEventListener('click', restoreLastText);
} else {
button.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
appendSuggestion(btn.text);
});
}
// Check if we need to start a new line
if (currentLineWidth + estimatedButtonWidth > MAX_CONTAINER_WIDTH && currentLineWidth !== 0) {
currentLine = document.createElement('div');
currentLine.style.display = 'flex';
currentLine.style.flexWrap = 'wrap';
currentLine.style.gap = '6px';
currentLine.style.alignItems = 'center';
currentLine.style.marginTop = '6px';
customButtonContainer.appendChild(currentLine);
currentLineWidth = 0;
}
currentLine.appendChild(button);
currentLineWidth += estimatedButtonWidth + 6; // Add button width and gap
});
updateLastButton();
return true;
}
function setupRegenerateListener() {
const regenerateBtn = document.querySelector('button[aria-label="Regenerate"]');
if (regenerateBtn && !regenerateBtn.hasAttribute('data-listener-added')) {
regenerateBtn.setAttribute('data-listener-added', 'true');
regenerateBtn.addEventListener('click', saveCurrentText);
}
}
// Main setup function, now with retry logic
function checkAndSetupButtons() {
const suggestionBox = getSuggestionTextarea(); // Attempt to find suggestion box
const activeKindroidButtonRow = getActiveButtonRow(); // Attempt to find active button row
if (!suggestionBox) {
// If suggestionBox is null, log and retry if max retries not reached
if (setupRetries < MAX_SETUP_RETRIES) {
setupRetries++;
//console.log(`Kindroid Buttons: Suggestion box not found. Retrying in ${RETRY_DELAY_MS}ms... (${setupRetries}/${MAX_SETUP_RETRIES})`);
setTimeout(checkAndSetupButtons, RETRY_DELAY_MS);
} else {
//console.warn("Kindroid Buttons: Max retries reached. Suggestion box still not found. Skipping setup for this cycle.");
setupRetries = 0; // Reset retries so it can try again later
}
return; // Exit if suggestionBox is null (and retries are handled)
}
// If suggestionBox is found, reset retries for the next potential challenge
setupRetries = 0;
if (activeKindroidButtonRow) {
const customButtonsExistAndCorrectlyPlaced = activeKindroidButtonRow.previousElementSibling?.classList.contains('custom-button-container');
if (!customButtonsExistAndCorrectlyPlaced) {
//console.log("Kindroid Buttons: Setting up custom buttons for a new or uninitialized button row.");
if (createCustomButtons(activeKindroidButtonRow)) { // Pass the identified active row
GM_setValue(BUTTONS_ADDED_KEY, 'true'); // Flag that buttons were added
}
} else {
// console.log("Kindroid Buttons: Custom buttons already set up for this visible row.");
}
setupRegenerateListener();
} else {
// console.log("Kindroid Buttons: No active Kindroid button row found, skipping button creation.");
}
}
// Register menu command to open manager
GM_registerMenuCommand('Manage Suggestion Buttons', showButtonManager);
// Watch for major DOM changes that might indicate modals or new content
const boxObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.addedNodes.length > 0) {
// Trigger checkAndSetupButtons on any addition, it will handle finding the right elements
checkAndSetupButtons();
return; // Only need to trigger once per mutation record that adds nodes
}
}
});
// Also watch for attribute changes, specifically style or class changes that might affect visibility
const attributeObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'attributes' && (mutation.attributeName === 'style' || mutation.attributeName === 'class')) {
// Trigger checkAndSetupButtons if a style or class attribute changed on a relevant element
// Or simply for any attribute change on body, as many elements are affected by parent visibility
checkAndSetupButtons();
return; // Trigger once per relevant attribute mutation
}
}
});
// Start observing the body for changes
boxObserver.observe(document.body, { childList: true, subtree: true });
attributeObserver.observe(document.body, { attributes: true, subtree: true });
// Initial check on script load, with a small delay to ensure DOM is ready
setTimeout(checkAndSetupButtons, 1500); // Increased initial delay slightly for good measure
// Listen for storage changes (though checkAndSetupButtons should handle most re-init needs)
GM_addValueChangeListener(BUTTONS_ADDED_KEY, (key, oldValue, newValue) => {
if (newValue === 'true') {
checkAndSetupButtons(); // Re-check if flag indicates buttons might need re-adding
}
});
})();