Section Marker UI has been updated
This commit is contained in:
@@ -212,51 +212,308 @@ body {
|
||||
.floating-btn:hover .tooltip-text { opacity: 1; }
|
||||
|
||||
/* ============================================
|
||||
Chapter/Section Markers
|
||||
Chapter/Section Markers - Base Styles
|
||||
============================================= */
|
||||
.editor-marker {
|
||||
padding: 15px;
|
||||
padding: 12px 16px;
|
||||
margin: 20px 0;
|
||||
border-radius: 10px;
|
||||
border-radius: 12px;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
position: relative;
|
||||
border: 1px solid rgba(0,0,0,0.1);
|
||||
border: 1px solid rgba(0,0,0,0.08);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.chapter-marker {
|
||||
background: #fff0f0;
|
||||
border-left: 5px solid #FF6B6B;
|
||||
background: linear-gradient(135deg, #fff5f5 0%, #ffe3e3 100%);
|
||||
border-left: 4px solid #FF6B6B;
|
||||
}
|
||||
|
||||
.section-marker {
|
||||
background: #f0f0ff;
|
||||
border-left: 5px solid #4834d4;
|
||||
background: linear-gradient(135deg, #f5f3ff 0%, #ede9fe 100%);
|
||||
border-left: 4px solid #4834d4;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.marker-header {
|
||||
/* ============================================
|
||||
Section Marker Toolbar Styles
|
||||
============================================= */
|
||||
.editor-marker .marker-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.marker-title {
|
||||
.editor-marker .marker-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.editor-marker .marker-title {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
font-size: 12px;
|
||||
min-width: 65px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.chapter-marker .marker-title {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.section-marker .marker-title {
|
||||
color: #5b21b6;
|
||||
}
|
||||
|
||||
/* Toolbar Groups */
|
||||
.toolbar-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.toolbar-group-label {
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
color: #9ca3af;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-right: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Toolbar Buttons */
|
||||
.toolbar-btn {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #e5e7eb;
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
color: #4b5563;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toolbar-btn:hover {
|
||||
background: #f9fafb;
|
||||
border-color: #d1d5db;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.toolbar-btn:active {
|
||||
background: #f3f4f6;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.toolbar-btn.dropdown-toggle {
|
||||
width: auto;
|
||||
padding: 0 10px;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.toolbar-btn.dropdown-toggle::after {
|
||||
font-size: 8px;
|
||||
margin-left: 2px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Dropdown Menus in Toolbar */
|
||||
.editor-marker .dropdown-menu {
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
padding: 6px;
|
||||
min-width: 170px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.editor-marker .dropdown-item {
|
||||
border-radius: 6px;
|
||||
padding: 9px 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: all 0.15s ease;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.editor-marker .dropdown-item:hover {
|
||||
background: linear-gradient(135deg, var(--accent-primary) 0%, var(--pill-bg-gradient-end) 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.editor-marker .dropdown-item i {
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.chapter-marker .marker-title { color: #d63031; }
|
||||
.section-marker .marker-title { color: #4834d4; }
|
||||
/* Voice Select */
|
||||
.voice-select {
|
||||
min-width: 140px;
|
||||
max-width: 160px;
|
||||
height: 32px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 0 8px;
|
||||
background: white;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.marker-controls {
|
||||
.voice-select:focus {
|
||||
border-color: var(--accent-primary);
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Number Input */
|
||||
.marker-number-input {
|
||||
width: 50px !important;
|
||||
height: 32px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
border-radius: 0 6px 6px 0 !important;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.marker-number-input:focus {
|
||||
border-color: var(--accent-primary);
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.marker-number-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.marker-number-group .input-group-text {
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-right: none;
|
||||
border-radius: 6px 0 0 6px;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
/* Action Buttons */
|
||||
.toolbar-action-btn {
|
||||
height: 32px;
|
||||
padding: 0 14px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.toolbar-action-btn i {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.toolbar-action-btn.btn-primary {
|
||||
background: linear-gradient(135deg, var(--accent-primary) 0%, var(--pill-bg-gradient-end) 100%);
|
||||
border: none;
|
||||
color: white;
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.toolbar-action-btn.btn-primary:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.toolbar-action-btn.btn-danger {
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
border: none;
|
||||
color: white;
|
||||
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.toolbar-action-btn.btn-danger:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4);
|
||||
}
|
||||
|
||||
.toolbar-action-btn.btn-outline-danger {
|
||||
border: 1px solid #fca5a5;
|
||||
color: #dc2626;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.toolbar-action-btn.btn-outline-danger:hover {
|
||||
background: #fef2f2;
|
||||
border-color: #f87171;
|
||||
}
|
||||
|
||||
/* Image Button */
|
||||
.toolbar-btn.image-btn {
|
||||
color: #059669;
|
||||
border-color: #a7f3d0;
|
||||
background: #ecfdf5;
|
||||
}
|
||||
|
||||
.toolbar-btn.image-btn:hover {
|
||||
background: #d1fae5;
|
||||
border-color: #6ee7b7;
|
||||
color: #047857;
|
||||
}
|
||||
|
||||
/* TTS Button */
|
||||
.toolbar-btn.tts-btn {
|
||||
width: auto;
|
||||
padding: 0 10px;
|
||||
gap: 4px;
|
||||
color: #7c3aed;
|
||||
border-color: #ddd6fe;
|
||||
background: #f5f3ff;
|
||||
font-weight: 700;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.toolbar-btn.tts-btn:hover {
|
||||
background: #ede9fe;
|
||||
border-color: #c4b5fd;
|
||||
color: #6d28d9;
|
||||
}
|
||||
|
||||
/* Divider */
|
||||
.toolbar-divider {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background: #e5e7eb;
|
||||
margin: 0 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
@@ -589,23 +846,23 @@ body {
|
||||
Image Upload Styles for Section Markers
|
||||
============================================= */
|
||||
.section-image-container {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
border: 2px dashed #dee2e6;
|
||||
margin: 12px 0 0 0;
|
||||
padding: 12px;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border-radius: 10px;
|
||||
border: 2px dashed #d1d5db;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.section-image-container.drag-over {
|
||||
border-color: #667eea;
|
||||
background: #f0f4ff;
|
||||
border-color: var(--accent-primary);
|
||||
background: rgba(102, 126, 234, 0.05);
|
||||
}
|
||||
|
||||
.section-image-container.has-image {
|
||||
border-style: solid;
|
||||
border-color: #28a745;
|
||||
background: #f8fff8;
|
||||
border-color: #10b981;
|
||||
background: rgba(16, 185, 129, 0.05);
|
||||
}
|
||||
|
||||
.image-drop-zone {
|
||||
@@ -613,24 +870,25 @@ body {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 80px;
|
||||
min-height: 70px;
|
||||
cursor: pointer;
|
||||
color: #6c757d;
|
||||
font-size: 14px;
|
||||
color: #9ca3af;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.image-drop-zone i {
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
color: #adb5bd;
|
||||
font-size: 22px;
|
||||
margin-bottom: 6px;
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.image-drop-zone:hover {
|
||||
color: #495057;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.image-drop-zone:hover i {
|
||||
color: #667eea;
|
||||
color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.image-preview-wrapper {
|
||||
@@ -681,8 +939,8 @@ body {
|
||||
|
||||
.image-info {
|
||||
font-size: 11px;
|
||||
color: #6c757d;
|
||||
margin-top: 5px;
|
||||
color: #6b7280;
|
||||
margin-top: 6px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -693,7 +951,7 @@ body {
|
||||
|
||||
/* Save Button Styles */
|
||||
.save-project-btn {
|
||||
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
border: none;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
@@ -704,7 +962,7 @@ body {
|
||||
|
||||
.save-project-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.4);
|
||||
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -758,3 +1016,111 @@ body {
|
||||
.dropdown-divider {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Responsive Adjustments for Toolbar
|
||||
============================================= */
|
||||
@media (max-width: 1400px) {
|
||||
.editor-marker .marker-controls {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.toolbar-group {
|
||||
padding: 3px 8px;
|
||||
}
|
||||
|
||||
.toolbar-group-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.voice-select {
|
||||
min-width: 120px;
|
||||
max-width: 140px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.toolbar-btn {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.toolbar-action-btn {
|
||||
height: 30px;
|
||||
padding: 0 12px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.voice-select {
|
||||
height: 30px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.marker-number-input {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.marker-number-group .input-group-text {
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.editor-marker .marker-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.editor-marker .marker-controls {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.section-marker {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.toolbar-group {
|
||||
padding: 3px 6px;
|
||||
}
|
||||
|
||||
.toolbar-divider {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.editor-marker .marker-controls {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.toolbar-action-btn span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toolbar-action-btn {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.voice-select {
|
||||
min-width: 100px;
|
||||
max-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.toolbar-group {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.editor-marker {
|
||||
padding: 10px 12px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.section-marker {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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