Section Marker UI has been updated

This commit is contained in:
Ashim Kumar
2026-01-16 18:56:45 +06:00
parent 93119fcee6
commit f1748d2c8e
2 changed files with 651 additions and 71 deletions

View File

@@ -134,7 +134,6 @@ window.insertSectionMarker = insertSectionMarker;
function createMarkerHTML(type, num, voice = 'af_alloy', markerId = null) {
const id = markerId || (Date.now() + '_' + Math.random().toString(36).substr(2, 9));
const title = type.toUpperCase();
const btnClass = type === 'chapter' ? 'btn-danger' : 'btn-primary';
const voiceOpts = VOICES.map(v =>
`<option value="${v.val}" ${v.val === voice ? 'selected' : ''}>${v.label}</option>`
@@ -146,26 +145,71 @@ function createMarkerHTML(type, num, voice = 'af_alloy', markerId = null) {
if (type === 'section') {
extraControls = `
<div class="vr bg-secondary mx-2"></div>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-dark dropdown-toggle" type="button" data-bs-toggle="dropdown" onmousedown="event.preventDefault()">Style</button>
<ul class="dropdown-menu">
<li><button class="dropdown-item" type="button" onmousedown="event.preventDefault(); formatBlock('p')">Normal</button></li>
<li><button class="dropdown-item" type="button" onmousedown="event.preventDefault(); formatBlock('h1')">Heading 1</button></li>
<li><button class="dropdown-item" type="button" onmousedown="event.preventDefault(); formatBlock('h2')">Heading 2</button></li>
<li><button class="dropdown-item" type="button" onmousedown="event.preventDefault(); formatBlock('h3')">Heading 3</button></li>
</ul>
<button class="btn btn-outline-dark" title="Bold" onmousedown="event.preventDefault(); applyFormat('bold')"><i class="bi bi-type-bold"></i></button>
<button class="btn btn-outline-dark" title="Italic" onmousedown="event.preventDefault(); applyFormat('italic')"><i class="bi bi-type-italic"></i></button>
<button class="btn btn-outline-dark" title="Normalize (select text first, or normalizes entire section)" onmousedown="event.preventDefault(); normalizeSection('${id}')"><i class="bi bi-eraser"></i></button>
<div class="vr bg-secondary mx-2"></div>
<button class="btn btn-outline-success" onclick="triggerImageUpload('${id}')" title="Add Image">
<div class="toolbar-divider"></div>
<!-- Formatting Group -->
<div class="toolbar-group">
<span class="toolbar-group-label">Format</span>
<div class="dropdown">
<button class="toolbar-btn dropdown-toggle" type="button" data-bs-toggle="dropdown" onmousedown="event.preventDefault()" title="Text Style">
Style
</button>
<ul class="dropdown-menu">
<li><button class="dropdown-item" type="button" onmousedown="event.preventDefault(); formatBlock('p')"><i class="bi bi-text-paragraph me-2"></i>Normal</button></li>
<li><button class="dropdown-item" type="button" onmousedown="event.preventDefault(); formatBlock('h1')"><i class="bi bi-type-h1 me-2"></i>Heading 1</button></li>
<li><button class="dropdown-item" type="button" onmousedown="event.preventDefault(); formatBlock('h2')"><i class="bi bi-type-h2 me-2"></i>Heading 2</button></li>
<li><button class="dropdown-item" type="button" onmousedown="event.preventDefault(); formatBlock('h3')"><i class="bi bi-type-h3 me-2"></i>Heading 3</button></li>
</ul>
</div>
<button class="toolbar-btn" title="Bold (Ctrl+B)" onmousedown="event.preventDefault(); applyFormat('bold')">
<i class="bi bi-type-bold"></i>
</button>
<button class="toolbar-btn" title="Italic (Ctrl+I)" onmousedown="event.preventDefault(); applyFormat('italic')">
<i class="bi bi-type-italic"></i>
</button>
</div>
<!-- Text Tools Group -->
<div class="toolbar-group">
<span class="toolbar-group-label">Tools</span>
<button class="toolbar-btn" title="Normalize - Clear formatting" onmousedown="event.preventDefault(); normalizeSection('${id}')">
<i class="bi bi-eraser"></i>
</button>
<div class="dropdown">
<button class="toolbar-btn dropdown-toggle" type="button" data-bs-toggle="dropdown" onmousedown="event.preventDefault()" title="Change Case">
Aa
</button>
<ul class="dropdown-menu">
<li><button class="dropdown-item" type="button" onmousedown="event.preventDefault(); changeCase('sentence')"><i class="bi bi-type me-2"></i>Sentence case</button></li>
<li><button class="dropdown-item" type="button" onmousedown="event.preventDefault(); changeCase('lower')"><i class="bi bi-alphabet me-2"></i>lower case</button></li>
<li><button class="dropdown-item" type="button" onmousedown="event.preventDefault(); changeCase('upper')"><i class="bi bi-alphabet-uppercase me-2"></i>UPPER CASE</button></li>
<li><button class="dropdown-item" type="button" onmousedown="event.preventDefault(); changeCase('capitalized')"><i class="bi bi-card-text me-2"></i>Capitalized Case</button></li>
<li><button class="dropdown-item" type="button" onmousedown="event.preventDefault(); changeCase('title')"><i class="bi bi-blockquote-left me-2"></i>Title Case</button></li>
</ul>
</div>
</div>
<!-- Media Group -->
<div class="toolbar-group">
<span class="toolbar-group-label">Media</span>
<button class="toolbar-btn image-btn" onclick="triggerImageUpload('${id}')" title="Add Image">
<i class="bi bi-image"></i>
</button>
<button class="btn btn-outline-primary fw-bold" onclick="openTTSEditor('${id}')" title="Edit TTS Text">
<button class="toolbar-btn tts-btn" onclick="openTTSEditor('${id}')" title="Edit TTS Text">
<i class="bi bi-pencil-square"></i> TTS
</button>
</div>
<div class="toolbar-divider"></div>
<!-- Action Buttons -->
<button class="toolbar-action-btn btn-primary" onclick="generateMarkerAudio('${id}')" title="Generate Audio">
<i class="bi bi-play-circle-fill"></i>
<span>Generate</span>
</button>
<button class="toolbar-action-btn btn-outline-danger" title="Remove Section" onclick="removeMarker('${id}')">
<i class="bi bi-trash3"></i>
</button>
`;
imageSection = `
@@ -186,6 +230,18 @@ function createMarkerHTML(type, num, voice = 'af_alloy', markerId = null) {
</div>
</div>
`;
} else {
// Chapter marker - simpler controls
extraControls = `
<div class="toolbar-divider"></div>
<button class="toolbar-action-btn btn-danger" onclick="generateMarkerAudio('${id}')" title="Generate All Sections">
<i class="bi bi-play-circle-fill"></i>
<span>Generate All</span>
</button>
<button class="toolbar-action-btn btn-outline-danger" title="Remove Chapter" onclick="removeMarker('${id}')">
<i class="bi bi-trash3"></i>
</button>
`;
}
return `
@@ -193,21 +249,15 @@ function createMarkerHTML(type, num, voice = 'af_alloy', markerId = null) {
<div class="marker-header">
<span class="marker-title">${title}</span>
<div class="marker-controls">
<div class="input-group input-group-sm">
<!-- Number & Voice -->
<div class="marker-number-group input-group input-group-sm">
<span class="input-group-text">#</span>
<input type="number" class="form-control" value="${num}" style="width: 60px;" onchange="updateMarkerData('${id}', 'num', this.value)">
<input type="number" class="form-control marker-number-input" value="${num}" onchange="updateMarkerData('${id}', 'num', this.value)">
</div>
<select class="form-select form-select-sm" style="width: 140px;" onchange="updateMarkerData('${id}', 'voice', this.value)">
<select class="form-select voice-select" onchange="updateMarkerData('${id}', 'voice', this.value)">
${voiceOpts}
</select>
${extraControls}
<div class="vr bg-secondary mx-2"></div>
<button class="btn btn-sm ${btnClass} fw-bold" onclick="generateMarkerAudio('${id}')">
<i class="bi bi-play-circle me-1"></i> Generate ${type === 'chapter' ? 'All' : 'Audio'}
</button>
<button class="btn btn-sm btn-outline-danger ms-2" title="Remove Marker" onclick="removeMarker('${id}')">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
${imageSection}
@@ -573,6 +623,170 @@ function formatBlock(tag) {
// Make it globally available
window.formatBlock = formatBlock;
// ==========================================
// CASE CHANGE FUNCTIONS
// ==========================================
/**
* Change the case of selected text
* @param {string} caseType - Type of case: 'sentence', 'lower', 'upper', 'capitalized', 'title'
*/
function changeCase(caseType) {
const selection = window.getSelection();
// Check if there's a valid selection with actual content
if (!selection || selection.isCollapsed || selection.toString().trim().length === 0) {
alert('Please select some text first to change its case.');
return;
}
const selectedText = selection.toString();
let transformedText = '';
switch (caseType) {
case 'sentence':
transformedText = toSentenceCase(selectedText);
break;
case 'lower':
transformedText = selectedText.toLowerCase();
break;
case 'upper':
transformedText = selectedText.toUpperCase();
break;
case 'capitalized':
transformedText = toCapitalizedCase(selectedText);
break;
case 'title':
transformedText = toTitleCase(selectedText);
break;
default:
transformedText = selectedText;
}
// Replace the selected text with transformed text
replaceSelectedText(selection, transformedText);
}
// Make it globally available
window.changeCase = changeCase;
/**
* Convert text to Sentence case
* First letter of each sentence is capitalized, rest is lowercase
* @param {string} text - Input text
* @returns {string} Transformed text
*/
function toSentenceCase(text) {
// First convert everything to lowercase
let result = text.toLowerCase();
// Capitalize the first letter
result = result.charAt(0).toUpperCase() + result.slice(1);
// Capitalize letter after sentence-ending punctuation (. ! ?)
result = result.replace(/([.!?]\s*)([a-z])/g, (match, punctuation, letter) => {
return punctuation + letter.toUpperCase();
});
// Also handle newlines as sentence breaks
result = result.replace(/(\n\s*)([a-z])/g, (match, newline, letter) => {
return newline + letter.toUpperCase();
});
return result;
}
/**
* Convert text to Capitalized Case
* First letter of every word is capitalized
* @param {string} text - Input text
* @returns {string} Transformed text
*/
function toCapitalizedCase(text) {
return text.toLowerCase().replace(/\b\w/g, (letter) => letter.toUpperCase());
}
/**
* Convert text to Title Case
* Like Capitalized Case but keeps small words (articles, prepositions, conjunctions) lowercase
* unless they're the first or last word
* @param {string} text - Input text
* @returns {string} Transformed text
*/
function toTitleCase(text) {
// Words that should remain lowercase (unless first or last)
const smallWords = [
'a', 'an', 'and', 'as', 'at', 'but', 'by', 'for', 'in', 'nor',
'of', 'on', 'or', 'so', 'the', 'to', 'up', 'yet', 'is', 'be',
'with', 'from', 'into', 'over', 'after', 'under', 'above'
];
const words = text.toLowerCase().split(/(\s+)/); // Split but keep whitespace
let isFirstWord = true;
let result = [];
for (let i = 0; i < words.length; i++) {
const word = words[i];
// If it's whitespace, just add it
if (/^\s+$/.test(word)) {
result.push(word);
continue;
}
// Check if this is the last actual word
let isLastWord = true;
for (let j = i + 1; j < words.length; j++) {
if (!/^\s+$/.test(words[j])) {
isLastWord = false;
break;
}
}
// Capitalize if it's first word, last word, or not a small word
if (isFirstWord || isLastWord || !smallWords.includes(word.toLowerCase())) {
result.push(word.charAt(0).toUpperCase() + word.slice(1));
} else {
result.push(word);
}
isFirstWord = false;
}
return result.join('');
}
/**
* Replace selected text with new text while preserving cursor position
* @param {Selection} selection - Current selection
* @param {string} newText - Text to insert
*/
function replaceSelectedText(selection, newText) {
if (!selection.rangeCount) return;
const range = selection.getRangeAt(0);
// Delete the selected content
range.deleteContents();
// Create a text node with the new text
const textNode = document.createTextNode(newText);
// Insert the new text
range.insertNode(textNode);
// Move cursor to end of inserted text
range.setStartAfter(textNode);
range.setEndAfter(textNode);
selection.removeAllRanges();
selection.addRange(range);
}
// ==========================================
// NORMALIZE FUNCTIONS
// ==========================================
/**
* Normalize section text (clean up formatting)
* If text is selected, only normalize the selection