Section Marker UI has been updated
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user