Update index.html
Browse files- index.html +137 -187
index.html
CHANGED
@@ -464,25 +464,22 @@
|
|
464 |
align-items: center;
|
465 |
justify-content: center;
|
466 |
flex-direction: column;
|
467 |
-
|
468 |
-
min-height: 200px; /* Default height for empty/loading state */
|
469 |
position: relative;
|
470 |
box-sizing: border-box;
|
471 |
-
padding: 2rem;
|
472 |
-
background-color: var(--input-bg);
|
473 |
border-radius: var(--radius-card);
|
474 |
border: 2px dashed var(--panel-border);
|
475 |
box-shadow: var(--shadow-subtle) inset;
|
476 |
-
transition: var(--transition-smooth)
|
477 |
}
|
478 |
-
/* When output section has content (the player), remove its own styling and apply new */
|
479 |
#output-section.has-content {
|
480 |
-
background-color: var(--panel-bg);
|
481 |
-
border
|
482 |
-
|
483 |
-
|
484 |
-
|
485 |
-
min-height: auto; /* Shrink to fit player content */
|
486 |
}
|
487 |
#output-section.has-content #status-message,
|
488 |
#output-section.has-content #loading-animation-wrapper {
|
@@ -500,7 +497,6 @@
|
|
500 |
justify-content: center;
|
501 |
gap: 1.8rem;
|
502 |
width: 100%;
|
503 |
-
min-height: 150px;
|
504 |
}
|
505 |
.orbital-loader {
|
506 |
width: 110px;
|
@@ -556,50 +552,25 @@
|
|
556 |
|
557 |
/* --- Custom Audio Player Styles --- */
|
558 |
#audio-player-content {
|
559 |
-
display: none;
|
560 |
width: 100%;
|
561 |
-
padding: 1.5rem;
|
562 |
box-sizing: border-box;
|
563 |
flex-direction: column;
|
564 |
gap: 1.2rem;
|
565 |
}
|
566 |
|
567 |
-
.audio-player-header-simple {
|
568 |
-
display: flex;
|
569 |
-
align-items: center;
|
570 |
-
justify-content: flex-end; /* Align to right for RTL */
|
571 |
-
gap: 0.5rem;
|
572 |
-
padding-bottom: 0.8rem; /* No border-bottom as in image */
|
573 |
-
margin-right: auto; /* Push content to left */
|
574 |
-
width: 100%; /* Take full width */
|
575 |
-
color: var(--text-secondary); /* Grey color for header text */
|
576 |
-
font-size: 0.95em; /* Slightly smaller text */
|
577 |
-
font-weight: 500;
|
578 |
-
}
|
579 |
-
.audio-player-header-simple span {
|
580 |
-
order: -1; /* Puts text first in RTL order */
|
581 |
-
margin-left: 0.5rem; /* Space between text and icon */
|
582 |
-
}
|
583 |
-
.audio-player-header-simple .icon {
|
584 |
-
width: 18px; /* Smaller icons */
|
585 |
-
height: 18px;
|
586 |
-
fill: currentColor;
|
587 |
-
stroke: currentColor;
|
588 |
-
stroke-width: 2;
|
589 |
-
vertical-align: middle;
|
590 |
-
}
|
591 |
-
|
592 |
.audio-waveform-container {
|
593 |
display: flex;
|
594 |
align-items: center;
|
595 |
-
gap:
|
596 |
width: 100%;
|
597 |
-
margin-bottom: 1rem;
|
598 |
}
|
599 |
.audio-time {
|
600 |
font-size: 0.9em;
|
601 |
color: var(--text-secondary);
|
602 |
-
min-width: 40px;
|
603 |
text-align: center;
|
604 |
font-variant-numeric: tabular-nums;
|
605 |
}
|
@@ -607,13 +578,10 @@
|
|
607 |
/* Waveform Display */
|
608 |
.audio-waveform {
|
609 |
flex-grow: 1;
|
610 |
-
height: 60px;
|
611 |
position: relative;
|
612 |
-
overflow: hidden;
|
613 |
-
border-radius: 4px; /* Slight radius on the waveform area */
|
614 |
-
background-color: transparent; /* No background color here */
|
615 |
}
|
616 |
-
|
617 |
.audio-waveform-bars-wrapper {
|
618 |
position: absolute;
|
619 |
top: 0;
|
@@ -621,140 +589,130 @@
|
|
621 |
width: 100%;
|
622 |
height: 100%;
|
623 |
display: flex;
|
624 |
-
justify-content: space-between;
|
625 |
-
align-items: center;
|
626 |
-
z-index: 0;
|
627 |
}
|
628 |
-
|
629 |
.waveform-bar {
|
630 |
-
width:
|
631 |
-
|
632 |
-
|
633 |
-
|
634 |
-
|
635 |
-
flex-shrink: 0; /* Don't shrink bars */
|
636 |
-
margin: 0 1px; /* Small gap between bars */
|
637 |
-
transform-origin: center; /* For scaling/positioning */
|
638 |
}
|
639 |
-
|
640 |
.audio-waveform-dashed-line {
|
641 |
position: absolute;
|
642 |
top: 50%;
|
643 |
left: 0;
|
644 |
width: 100%;
|
645 |
-
height: 1px;
|
646 |
background-image: linear-gradient(to right, var(--waveform-dashed-line-color) 33%, transparent 0%);
|
647 |
background-position: center;
|
648 |
-
background-size: 10px 1px;
|
649 |
transform: translateY(-50%);
|
650 |
-
z-index: 1;
|
651 |
}
|
652 |
|
653 |
/* Audio Controls */
|
654 |
.audio-controls-group {
|
655 |
display: flex;
|
656 |
-
justify-content: center;
|
657 |
align-items: center;
|
658 |
-
gap: 1.5rem;
|
659 |
-
margin-bottom: 1rem;
|
660 |
}
|
661 |
-
|
662 |
-
.audio-skip-btn {
|
663 |
background: none;
|
664 |
border: none;
|
665 |
cursor: pointer;
|
666 |
padding: 8px;
|
667 |
-
|
668 |
-
transition: transform 0.2s;
|
669 |
}
|
670 |
-
.audio-skip-btn
|
671 |
-
|
672 |
-
height: 24px;
|
673 |
-
fill: currentColor;
|
674 |
}
|
675 |
-
.audio-skip-btn:hover {
|
676 |
-
|
677 |
}
|
678 |
-
.audio-skip-btn:active {
|
679 |
transform: scale(0.9);
|
680 |
}
|
681 |
-
|
682 |
-
|
683 |
-
|
684 |
-
|
685 |
-
cursor: pointer;
|
686 |
-
padding: 0; /* No padding inside for icon sizing */
|
687 |
-
width: 60px; /* Larger circular area */
|
688 |
-
height: 60px;
|
689 |
-
border-radius: 50%;
|
690 |
-
display: flex;
|
691 |
-
align-items: center;
|
692 |
-
justify-content: center;
|
693 |
-
color: var(--text-secondary); /* Grey color */
|
694 |
-
transition: transform 0.2s;
|
695 |
-
/* No background effect on hover as per image */
|
696 |
-
}
|
697 |
-
.audio-play-pause-btn-large:hover {
|
698 |
-
transform: scale(1.05);
|
699 |
}
|
700 |
-
.audio-play-pause-btn-large
|
701 |
-
|
|
|
|
|
702 |
}
|
703 |
.audio-play-pause-btn-large svg {
|
704 |
-
width:
|
705 |
-
height:
|
706 |
fill: currentColor;
|
707 |
}
|
708 |
|
709 |
-
.audio-
|
710 |
display: flex;
|
711 |
align-items: center;
|
712 |
-
justify-content:
|
713 |
width: 100%;
|
714 |
-
gap: 1rem; /* Space between volume and speed button */
|
715 |
-
}
|
716 |
-
|
717 |
-
.audio-volume-btn {
|
718 |
-
background: none;
|
719 |
-
border: none;
|
720 |
-
cursor: pointer;
|
721 |
-
padding: 8px;
|
722 |
-
color: var(--text-secondary);
|
723 |
-
transition: transform 0.2s;
|
724 |
}
|
725 |
.audio-volume-btn svg {
|
726 |
width: 24px;
|
727 |
height: 24px;
|
728 |
fill: currentColor;
|
729 |
}
|
730 |
-
.audio-volume-btn:hover {
|
731 |
-
transform: scale(1.1);
|
732 |
-
}
|
733 |
-
.audio-volume-btn:active {
|
734 |
-
transform: scale(0.9);
|
735 |
-
}
|
736 |
-
|
737 |
.audio-speed-btn {
|
738 |
font-family: var(--app-font);
|
739 |
font-size: 0.9em;
|
740 |
font-weight: 600;
|
741 |
-
background-color: var(--panel-bg);
|
742 |
-
border: 1px solid var(--panel-border);
|
743 |
-
padding: 6px 12px;
|
744 |
border-radius: 8px;
|
745 |
min-width: 40px;
|
746 |
text-align: center;
|
747 |
color: var(--text-primary);
|
748 |
-
|
749 |
-
transition: background-color 0.2s, border-color 0.2s;
|
750 |
-
box-shadow: var(--shadow-subtle); /* Add subtle shadow */
|
751 |
}
|
752 |
.audio-speed-btn:hover {
|
753 |
-
background-color: var(--input-bg);
|
754 |
}
|
755 |
-
|
756 |
/* Hide the default audio player */
|
757 |
#hidden-audio-player { display: none !important; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
758 |
</style>
|
759 |
</head>
|
760 |
<body>
|
@@ -825,12 +783,6 @@
|
|
825 |
|
826 |
<!-- Custom Audio Player Structure -->
|
827 |
<div id="audio-player-content">
|
828 |
-
<div class="audio-player-header-simple">
|
829 |
-
<svg class="icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"></path><circle cx="6" cy="18" r="3"></circle><circle cx="18" cy="16" r="3"></circle></svg>
|
830 |
-
<svg class="icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 18v-6a9 9 0 0 1 18 0v6"></path><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"></path></svg>
|
831 |
-
<span>فایل صوتی</span>
|
832 |
-
</div>
|
833 |
-
|
834 |
<div class="audio-waveform-container">
|
835 |
<span class="audio-time audio-current-time">0:00</span>
|
836 |
<div class="audio-waveform">
|
@@ -853,7 +805,7 @@
|
|
853 |
</button>
|
854 |
</div>
|
855 |
|
856 |
-
<div class="audio-
|
857 |
<button class="audio-volume-btn">
|
858 |
<svg viewBox="0 0 24 24" class="volume-high-icon"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"></path></svg>
|
859 |
<svg viewBox="0 0 24 24" class="volume-mute-icon" style="display:none;"><path d="M7 9v6h4l5 5V4L11 9H7zM16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zM19 12c0 .94-.23 1.82-.68 2.6L19 14.88c.45-.88.7-1.88.7-2.88 0-4.01-2.99-7.14-7-8.05v2.06c2.89.86 5 3.54 5 6.71zM4.55 4L2 6.55 9.45 14H7v6h4l5 5V14.55l4.05 4.05L22 18 12 8 4.55 4z"></path></svg>
|
@@ -965,7 +917,7 @@
|
|
965 |
|
966 |
// --- Custom Audio Player Elements ---
|
967 |
const hiddenAudioPlayer = document.getElementById('hidden-audio-player');
|
968 |
-
const audioPlayerContent = document.getElementById('audio-player-content');
|
969 |
const audioCurrentTimeSpan = audioPlayerContent.querySelector('.audio-current-time');
|
970 |
const audioTotalTimeSpan = audioPlayerContent.querySelector('.audio-total-time');
|
971 |
const audioWaveformBarsWrapper = audioPlayerContent.querySelector('.audio-waveform-bars-wrapper');
|
@@ -980,8 +932,7 @@
|
|
980 |
const speedBtn = audioPlayerContent.querySelector('.audio-speed-btn');
|
981 |
|
982 |
let currentPlaybackSpeedIndex = 0;
|
983 |
-
const playbackSpeeds = [1.0, 1.25, 1.5,
|
984 |
-
const NUM_WAVEFORM_BARS = 60; // Number of bars to display for the waveform
|
985 |
|
986 |
// Character Counter
|
987 |
const charCountSpan = document.getElementById('char-count');
|
@@ -1006,7 +957,6 @@
|
|
1006 |
|
1007 |
function getImageUrl(speaker, index, size = 'thumb') {
|
1008 |
const gender = speaker.name.includes('(مرد)') ? 'men' : 'women';
|
1009 |
-
// A more deterministic way to get images while still being "random-like" for examples
|
1010 |
const imageSeed = speaker.id.charCodeAt(0) + speaker.id.length + index;
|
1011 |
const imageIndex = (imageSeed * 7 + 5) % 100;
|
1012 |
let portraitSizePath = 'thumb/';
|
@@ -1114,22 +1064,27 @@
|
|
1114 |
|
1115 |
// --- Custom Audio Player Functionality ---
|
1116 |
|
1117 |
-
// Function to generate waveform bars
|
1118 |
function generateWaveformBars() {
|
1119 |
-
audioWaveformBarsWrapper.innerHTML = '';
|
1120 |
-
const
|
1121 |
-
|
1122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
1123 |
const bar = document.createElement('div');
|
1124 |
bar.classList.add('waveform-bar');
|
1125 |
// Simple randomized heights for visual variety
|
1126 |
-
const height = Math.random() *
|
1127 |
bar.style.height = `${height}%`;
|
1128 |
audioWaveformBarsWrapper.appendChild(bar);
|
1129 |
}
|
1130 |
}
|
1131 |
|
1132 |
-
// Helper function to format time (e.g., 0:06)
|
1133 |
function formatTime(seconds) {
|
1134 |
if (isNaN(seconds) || seconds < 0) return '0:00';
|
1135 |
const minutes = Math.floor(seconds / 60);
|
@@ -1137,7 +1092,7 @@
|
|
1137 |
return `${minutes}:${remainingSeconds < 10 ? '0' : ''}${remainingSeconds}`;
|
1138 |
}
|
1139 |
|
1140 |
-
// Update custom player UI
|
1141 |
function updatePlayerUI() {
|
1142 |
if (hiddenAudioPlayer.paused || hiddenAudioPlayer.ended) {
|
1143 |
playIcon.style.display = 'block';
|
@@ -1149,16 +1104,16 @@
|
|
1149 |
|
1150 |
const currentTime = hiddenAudioPlayer.currentTime;
|
1151 |
const duration = hiddenAudioPlayer.duration;
|
1152 |
-
|
1153 |
audioCurrentTimeSpan.textContent = formatTime(currentTime);
|
1154 |
|
1155 |
if (isFinite(duration) && duration > 0) {
|
1156 |
audioTotalTimeSpan.textContent = formatTime(duration);
|
1157 |
const progressRatio = currentTime / duration;
|
1158 |
-
const barsToColor = Math.floor(progressRatio * NUM_WAVEFORM_BARS);
|
1159 |
-
|
1160 |
const bars = audioWaveformBarsWrapper.children;
|
1161 |
-
|
|
|
|
|
|
|
1162 |
if (i < barsToColor) {
|
1163 |
bars[i].style.backgroundColor = 'var(--waveform-color-active)';
|
1164 |
} else {
|
@@ -1167,7 +1122,6 @@
|
|
1167 |
}
|
1168 |
} else {
|
1169 |
audioTotalTimeSpan.textContent = '0:00';
|
1170 |
-
// Reset all bars to inactive color
|
1171 |
const bars = audioWaveformBarsWrapper.children;
|
1172 |
for (let i = 0; i < bars.length; i++) {
|
1173 |
bars[i].style.backgroundColor = 'var(--waveform-color-inactive)';
|
@@ -1175,7 +1129,6 @@
|
|
1175 |
}
|
1176 |
}
|
1177 |
|
1178 |
-
// Event Listeners for Custom Player Controls
|
1179 |
playPauseBtn.addEventListener('click', () => {
|
1180 |
if (hiddenAudioPlayer.paused) {
|
1181 |
hiddenAudioPlayer.play();
|
@@ -1183,27 +1136,17 @@
|
|
1183 |
hiddenAudioPlayer.pause();
|
1184 |
}
|
1185 |
});
|
1186 |
-
|
1187 |
skipBackwardBtn.addEventListener('click', () => {
|
1188 |
-
hiddenAudioPlayer.currentTime = Math.max(0, hiddenAudioPlayer.currentTime -
|
1189 |
});
|
1190 |
-
|
1191 |
skipForwardBtn.addEventListener('click', () => {
|
1192 |
-
hiddenAudioPlayer.currentTime = Math.min(hiddenAudioPlayer.duration, hiddenAudioPlayer.currentTime +
|
1193 |
});
|
1194 |
-
|
1195 |
volumeBtn.addEventListener('click', () => {
|
1196 |
-
|
1197 |
-
|
1198 |
-
|
1199 |
-
volumeMuteIcon.style.display = 'none';
|
1200 |
-
} else {
|
1201 |
-
hiddenAudioPlayer.muted = true;
|
1202 |
-
volumeHighIcon.style.display = 'none';
|
1203 |
-
volumeMuteIcon.style.display = 'block';
|
1204 |
-
}
|
1205 |
});
|
1206 |
-
|
1207 |
speedBtn.addEventListener('click', () => {
|
1208 |
currentPlaybackSpeedIndex = (currentPlaybackSpeedIndex + 1) % playbackSpeeds.length;
|
1209 |
const newSpeed = playbackSpeeds[currentPlaybackSpeedIndex];
|
@@ -1211,24 +1154,35 @@
|
|
1211 |
speedBtn.textContent = `${newSpeed}x`;
|
1212 |
});
|
1213 |
|
1214 |
-
// Listen to hidden audio player events to update UI
|
1215 |
hiddenAudioPlayer.addEventListener('timeupdate', updatePlayerUI);
|
1216 |
hiddenAudioPlayer.addEventListener('loadedmetadata', () => {
|
1217 |
-
generateWaveformBars();
|
1218 |
-
updatePlayerUI();
|
1219 |
});
|
1220 |
hiddenAudioPlayer.addEventListener('play', updatePlayerUI);
|
1221 |
hiddenAudioPlayer.addEventListener('pause', updatePlayerUI);
|
1222 |
hiddenAudioPlayer.addEventListener('ended', () => {
|
1223 |
-
hiddenAudioPlayer.currentTime = 0;
|
1224 |
updatePlayerUI();
|
1225 |
});
|
1226 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1227 |
// --- State Management Functions ---
|
1228 |
function showLoadingState() {
|
1229 |
-
audioPlayerContent.style.display = 'none';
|
1230 |
-
outputSection.classList.remove('has-content');
|
1231 |
-
statusMessage.style.display = '
|
1232 |
loadingAnimationWrapper.style.display = 'flex';
|
1233 |
generateBtn.disabled = true;
|
1234 |
generateBtn.innerHTML = `
|
@@ -1244,18 +1198,16 @@
|
|
1244 |
loadingAnimationWrapper.style.display = 'none';
|
1245 |
if (isSuccess) {
|
1246 |
statusMessage.style.display = 'none';
|
1247 |
-
audioPlayerContent.style.display = 'flex';
|
1248 |
-
outputSection.classList.add('has-content');
|
1249 |
-
|
1250 |
-
updatePlayerUI(); // Update UI with initial playback state (paused, time 0)
|
1251 |
-
hiddenAudioPlayer.play(); // Auto-play the audio
|
1252 |
} else {
|
1253 |
statusMessage.textContent = message || 'خطایی رخ داد. لطفاً دوباره تلاش کنید.';
|
1254 |
statusMessage.style.display = 'block';
|
1255 |
-
audioPlayerContent.style.display = 'none';
|
1256 |
-
outputSection.classList.remove('has-content');
|
1257 |
-
hiddenAudioPlayer.pause();
|
1258 |
-
hiddenAudioPlayer.src = '';
|
1259 |
}
|
1260 |
generateBtn.disabled = false;
|
1261 |
generateBtn.innerHTML = '✨ تولید صدا با آلفا';
|
@@ -1359,12 +1311,10 @@
|
|
1359 |
// Initial setup
|
1360 |
updateSelectedSpeakerDisplay(selectedSpeakerIdStorage.value || speakers[0].id);
|
1361 |
form.addEventListener('submit', generateAudio);
|
1362 |
-
|
1363 |
-
// Hide custom audio player and show status message on load
|
1364 |
-
audioPlayerContent.style.display = 'none';
|
1365 |
statusMessage.style.display = 'block';
|
1366 |
loadingAnimationWrapper.style.display = 'none';
|
1367 |
-
|
|
|
1368 |
});
|
1369 |
</script>
|
1370 |
</body>
|
|
|
464 |
align-items: center;
|
465 |
justify-content: center;
|
466 |
flex-direction: column;
|
467 |
+
min-height: 200px;
|
|
|
468 |
position: relative;
|
469 |
box-sizing: border-box;
|
470 |
+
padding: 2rem;
|
471 |
+
background-color: var(--input-bg);
|
472 |
border-radius: var(--radius-card);
|
473 |
border: 2px dashed var(--panel-border);
|
474 |
box-shadow: var(--shadow-subtle) inset;
|
475 |
+
transition: var(--transition-smooth);
|
476 |
}
|
|
|
477 |
#output-section.has-content {
|
478 |
+
background-color: var(--panel-bg);
|
479 |
+
border: 1px solid var(--panel-border);
|
480 |
+
box-shadow: var(--shadow-medium);
|
481 |
+
padding: 0;
|
482 |
+
min-height: auto;
|
|
|
483 |
}
|
484 |
#output-section.has-content #status-message,
|
485 |
#output-section.has-content #loading-animation-wrapper {
|
|
|
497 |
justify-content: center;
|
498 |
gap: 1.8rem;
|
499 |
width: 100%;
|
|
|
500 |
}
|
501 |
.orbital-loader {
|
502 |
width: 110px;
|
|
|
552 |
|
553 |
/* --- Custom Audio Player Styles --- */
|
554 |
#audio-player-content {
|
555 |
+
display: none;
|
556 |
width: 100%;
|
557 |
+
padding: 1.5rem;
|
558 |
box-sizing: border-box;
|
559 |
flex-direction: column;
|
560 |
gap: 1.2rem;
|
561 |
}
|
562 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
563 |
.audio-waveform-container {
|
564 |
display: flex;
|
565 |
align-items: center;
|
566 |
+
gap: 1rem;
|
567 |
width: 100%;
|
568 |
+
margin-bottom: 1rem;
|
569 |
}
|
570 |
.audio-time {
|
571 |
font-size: 0.9em;
|
572 |
color: var(--text-secondary);
|
573 |
+
min-width: 40px;
|
574 |
text-align: center;
|
575 |
font-variant-numeric: tabular-nums;
|
576 |
}
|
|
|
578 |
/* Waveform Display */
|
579 |
.audio-waveform {
|
580 |
flex-grow: 1;
|
581 |
+
height: 60px;
|
582 |
position: relative;
|
583 |
+
overflow: hidden;
|
|
|
|
|
584 |
}
|
|
|
585 |
.audio-waveform-bars-wrapper {
|
586 |
position: absolute;
|
587 |
top: 0;
|
|
|
589 |
width: 100%;
|
590 |
height: 100%;
|
591 |
display: flex;
|
592 |
+
justify-content: space-between;
|
593 |
+
align-items: center;
|
594 |
+
z-index: 0;
|
595 |
}
|
|
|
596 |
.waveform-bar {
|
597 |
+
width: 3px;
|
598 |
+
background-color: var(--waveform-color-inactive);
|
599 |
+
border-radius: 2px;
|
600 |
+
flex-shrink: 0;
|
601 |
+
margin: 0 1px;
|
|
|
|
|
|
|
602 |
}
|
|
|
603 |
.audio-waveform-dashed-line {
|
604 |
position: absolute;
|
605 |
top: 50%;
|
606 |
left: 0;
|
607 |
width: 100%;
|
608 |
+
height: 1px;
|
609 |
background-image: linear-gradient(to right, var(--waveform-dashed-line-color) 33%, transparent 0%);
|
610 |
background-position: center;
|
611 |
+
background-size: 10px 1px;
|
612 |
transform: translateY(-50%);
|
613 |
+
z-index: 1;
|
614 |
}
|
615 |
|
616 |
/* Audio Controls */
|
617 |
.audio-controls-group {
|
618 |
display: flex;
|
619 |
+
justify-content: center;
|
620 |
align-items: center;
|
621 |
+
gap: 1.5rem;
|
622 |
+
margin-bottom: 1rem;
|
623 |
}
|
624 |
+
.audio-skip-btn, .audio-play-pause-btn-large, .audio-volume-btn, .audio-speed-btn {
|
|
|
625 |
background: none;
|
626 |
border: none;
|
627 |
cursor: pointer;
|
628 |
padding: 8px;
|
629 |
+
transition: transform 0.2s, opacity 0.2s;
|
|
|
630 |
}
|
631 |
+
.audio-skip-btn, .audio-play-pause-btn-large, .audio-volume-btn {
|
632 |
+
color: var(--text-secondary);
|
|
|
|
|
633 |
}
|
634 |
+
.audio-skip-btn:hover, .audio-play-pause-btn-large:hover, .audio-volume-btn:hover {
|
635 |
+
opacity: 0.8;
|
636 |
}
|
637 |
+
.audio-skip-btn:active, .audio-play-pause-btn-large:active, .audio-volume-btn:active {
|
638 |
transform: scale(0.9);
|
639 |
}
|
640 |
+
.audio-skip-btn svg {
|
641 |
+
width: 28px;
|
642 |
+
height: 28px;
|
643 |
+
fill: currentColor;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
644 |
}
|
645 |
+
.audio-play-pause-btn-large {
|
646 |
+
padding: 0;
|
647 |
+
width: 50px;
|
648 |
+
height: 50px;
|
649 |
}
|
650 |
.audio-play-pause-btn-large svg {
|
651 |
+
width: 38px;
|
652 |
+
height: 38px;
|
653 |
fill: currentColor;
|
654 |
}
|
655 |
|
656 |
+
.audio-utility-controls {
|
657 |
display: flex;
|
658 |
align-items: center;
|
659 |
+
justify-content: space-between;
|
660 |
width: 100%;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
661 |
}
|
662 |
.audio-volume-btn svg {
|
663 |
width: 24px;
|
664 |
height: 24px;
|
665 |
fill: currentColor;
|
666 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
667 |
.audio-speed-btn {
|
668 |
font-family: var(--app-font);
|
669 |
font-size: 0.9em;
|
670 |
font-weight: 600;
|
671 |
+
background-color: var(--panel-bg);
|
672 |
+
border: 1px solid var(--panel-border);
|
|
|
673 |
border-radius: 8px;
|
674 |
min-width: 40px;
|
675 |
text-align: center;
|
676 |
color: var(--text-primary);
|
677 |
+
box-shadow: var(--shadow-subtle);
|
|
|
|
|
678 |
}
|
679 |
.audio-speed-btn:hover {
|
680 |
+
background-color: var(--input-bg);
|
681 |
}
|
682 |
+
|
683 |
/* Hide the default audio player */
|
684 |
#hidden-audio-player { display: none !important; }
|
685 |
+
|
686 |
+
/* Responsive adjustments for mobile */
|
687 |
+
@media (max-width: 600px) {
|
688 |
+
.container {
|
689 |
+
width: 95%;
|
690 |
+
}
|
691 |
+
.main-content {
|
692 |
+
padding: 1.5rem;
|
693 |
+
}
|
694 |
+
.app-header h1 {
|
695 |
+
font-size: 1.8em;
|
696 |
+
}
|
697 |
+
#audio-player-content {
|
698 |
+
padding: 1rem;
|
699 |
+
}
|
700 |
+
.audio-controls-group {
|
701 |
+
gap: 1rem; /* Reduce gap between play/skip */
|
702 |
+
}
|
703 |
+
.audio-skip-btn svg {
|
704 |
+
width: 24px;
|
705 |
+
height: 24px;
|
706 |
+
}
|
707 |
+
.audio-play-pause-btn-large {
|
708 |
+
width: 44px;
|
709 |
+
height: 44px;
|
710 |
+
}
|
711 |
+
.audio-play-pause-btn-large svg {
|
712 |
+
width: 32px;
|
713 |
+
height: 32px;
|
714 |
+
}
|
715 |
+
}
|
716 |
</style>
|
717 |
</head>
|
718 |
<body>
|
|
|
783 |
|
784 |
<!-- Custom Audio Player Structure -->
|
785 |
<div id="audio-player-content">
|
|
|
|
|
|
|
|
|
|
|
|
|
786 |
<div class="audio-waveform-container">
|
787 |
<span class="audio-time audio-current-time">0:00</span>
|
788 |
<div class="audio-waveform">
|
|
|
805 |
</button>
|
806 |
</div>
|
807 |
|
808 |
+
<div class="audio-utility-controls">
|
809 |
<button class="audio-volume-btn">
|
810 |
<svg viewBox="0 0 24 24" class="volume-high-icon"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"></path></svg>
|
811 |
<svg viewBox="0 0 24 24" class="volume-mute-icon" style="display:none;"><path d="M7 9v6h4l5 5V4L11 9H7zM16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zM19 12c0 .94-.23 1.82-.68 2.6L19 14.88c.45-.88.7-1.88.7-2.88 0-4.01-2.99-7.14-7-8.05v2.06c2.89.86 5 3.54 5 6.71zM4.55 4L2 6.55 9.45 14H7v6h4l5 5V14.55l4.05 4.05L22 18 12 8 4.55 4z"></path></svg>
|
|
|
917 |
|
918 |
// --- Custom Audio Player Elements ---
|
919 |
const hiddenAudioPlayer = document.getElementById('hidden-audio-player');
|
920 |
+
const audioPlayerContent = document.getElementById('audio-player-content');
|
921 |
const audioCurrentTimeSpan = audioPlayerContent.querySelector('.audio-current-time');
|
922 |
const audioTotalTimeSpan = audioPlayerContent.querySelector('.audio-total-time');
|
923 |
const audioWaveformBarsWrapper = audioPlayerContent.querySelector('.audio-waveform-bars-wrapper');
|
|
|
932 |
const speedBtn = audioPlayerContent.querySelector('.audio-speed-btn');
|
933 |
|
934 |
let currentPlaybackSpeedIndex = 0;
|
935 |
+
const playbackSpeeds = [1.0, 1.25, 1.5, 0.75];
|
|
|
936 |
|
937 |
// Character Counter
|
938 |
const charCountSpan = document.getElementById('char-count');
|
|
|
957 |
|
958 |
function getImageUrl(speaker, index, size = 'thumb') {
|
959 |
const gender = speaker.name.includes('(مرد)') ? 'men' : 'women';
|
|
|
960 |
const imageSeed = speaker.id.charCodeAt(0) + speaker.id.length + index;
|
961 |
const imageIndex = (imageSeed * 7 + 5) % 100;
|
962 |
let portraitSizePath = 'thumb/';
|
|
|
1064 |
|
1065 |
// --- Custom Audio Player Functionality ---
|
1066 |
|
1067 |
+
// Function to generate waveform bars dynamically based on container width
|
1068 |
function generateWaveformBars() {
|
1069 |
+
audioWaveformBarsWrapper.innerHTML = '';
|
1070 |
+
const containerWidth = audioWaveformBarsWrapper.offsetWidth;
|
1071 |
+
if(containerWidth === 0) return; // Don't generate if not visible
|
1072 |
+
|
1073 |
+
const barWidth = 3; // From CSS
|
1074 |
+
const barMargin = 1; // From CSS
|
1075 |
+
const totalBarWidth = barWidth + (barMargin * 2);
|
1076 |
+
const numBars = Math.floor(containerWidth / totalBarWidth);
|
1077 |
+
|
1078 |
+
for (let i = 0; i < numBars; i++) {
|
1079 |
const bar = document.createElement('div');
|
1080 |
bar.classList.add('waveform-bar');
|
1081 |
// Simple randomized heights for visual variety
|
1082 |
+
const height = Math.random() * 90 + 10; // Height between 10% and 100%
|
1083 |
bar.style.height = `${height}%`;
|
1084 |
audioWaveformBarsWrapper.appendChild(bar);
|
1085 |
}
|
1086 |
}
|
1087 |
|
|
|
1088 |
function formatTime(seconds) {
|
1089 |
if (isNaN(seconds) || seconds < 0) return '0:00';
|
1090 |
const minutes = Math.floor(seconds / 60);
|
|
|
1092 |
return `${minutes}:${remainingSeconds < 10 ? '0' : ''}${remainingSeconds}`;
|
1093 |
}
|
1094 |
|
1095 |
+
// Update custom player UI
|
1096 |
function updatePlayerUI() {
|
1097 |
if (hiddenAudioPlayer.paused || hiddenAudioPlayer.ended) {
|
1098 |
playIcon.style.display = 'block';
|
|
|
1104 |
|
1105 |
const currentTime = hiddenAudioPlayer.currentTime;
|
1106 |
const duration = hiddenAudioPlayer.duration;
|
|
|
1107 |
audioCurrentTimeSpan.textContent = formatTime(currentTime);
|
1108 |
|
1109 |
if (isFinite(duration) && duration > 0) {
|
1110 |
audioTotalTimeSpan.textContent = formatTime(duration);
|
1111 |
const progressRatio = currentTime / duration;
|
|
|
|
|
1112 |
const bars = audioWaveformBarsWrapper.children;
|
1113 |
+
const numBars = bars.length;
|
1114 |
+
const barsToColor = Math.floor(progressRatio * numBars);
|
1115 |
+
|
1116 |
+
for (let i = 0; i < numBars; i++) {
|
1117 |
if (i < barsToColor) {
|
1118 |
bars[i].style.backgroundColor = 'var(--waveform-color-active)';
|
1119 |
} else {
|
|
|
1122 |
}
|
1123 |
} else {
|
1124 |
audioTotalTimeSpan.textContent = '0:00';
|
|
|
1125 |
const bars = audioWaveformBarsWrapper.children;
|
1126 |
for (let i = 0; i < bars.length; i++) {
|
1127 |
bars[i].style.backgroundColor = 'var(--waveform-color-inactive)';
|
|
|
1129 |
}
|
1130 |
}
|
1131 |
|
|
|
1132 |
playPauseBtn.addEventListener('click', () => {
|
1133 |
if (hiddenAudioPlayer.paused) {
|
1134 |
hiddenAudioPlayer.play();
|
|
|
1136 |
hiddenAudioPlayer.pause();
|
1137 |
}
|
1138 |
});
|
|
|
1139 |
skipBackwardBtn.addEventListener('click', () => {
|
1140 |
+
hiddenAudioPlayer.currentTime = Math.max(0, hiddenAudioPlayer.currentTime - 5); // Skip 5 seconds back
|
1141 |
});
|
|
|
1142 |
skipForwardBtn.addEventListener('click', () => {
|
1143 |
+
hiddenAudioPlayer.currentTime = Math.min(hiddenAudioPlayer.duration, hiddenAudioPlayer.currentTime + 5); // Skip 5 seconds forward
|
1144 |
});
|
|
|
1145 |
volumeBtn.addEventListener('click', () => {
|
1146 |
+
hiddenAudioPlayer.muted = !hiddenAudioPlayer.muted;
|
1147 |
+
volumeHighIcon.style.display = hiddenAudioPlayer.muted ? 'none' : 'block';
|
1148 |
+
volumeMuteIcon.style.display = hiddenAudioPlayer.muted ? 'block' : 'none';
|
|
|
|
|
|
|
|
|
|
|
|
|
1149 |
});
|
|
|
1150 |
speedBtn.addEventListener('click', () => {
|
1151 |
currentPlaybackSpeedIndex = (currentPlaybackSpeedIndex + 1) % playbackSpeeds.length;
|
1152 |
const newSpeed = playbackSpeeds[currentPlaybackSpeedIndex];
|
|
|
1154 |
speedBtn.textContent = `${newSpeed}x`;
|
1155 |
});
|
1156 |
|
|
|
1157 |
hiddenAudioPlayer.addEventListener('timeupdate', updatePlayerUI);
|
1158 |
hiddenAudioPlayer.addEventListener('loadedmetadata', () => {
|
1159 |
+
generateWaveformBars();
|
1160 |
+
updatePlayerUI();
|
1161 |
});
|
1162 |
hiddenAudioPlayer.addEventListener('play', updatePlayerUI);
|
1163 |
hiddenAudioPlayer.addEventListener('pause', updatePlayerUI);
|
1164 |
hiddenAudioPlayer.addEventListener('ended', () => {
|
1165 |
+
hiddenAudioPlayer.currentTime = 0;
|
1166 |
updatePlayerUI();
|
1167 |
});
|
1168 |
|
1169 |
+
let resizeTimeout;
|
1170 |
+
window.addEventListener('resize', () => {
|
1171 |
+
clearTimeout(resizeTimeout);
|
1172 |
+
resizeTimeout = setTimeout(() => {
|
1173 |
+
if (outputSection.classList.contains('has-content')) {
|
1174 |
+
generateWaveformBars();
|
1175 |
+
updatePlayerUI();
|
1176 |
+
}
|
1177 |
+
}, 250);
|
1178 |
+
});
|
1179 |
+
|
1180 |
+
|
1181 |
// --- State Management Functions ---
|
1182 |
function showLoadingState() {
|
1183 |
+
audioPlayerContent.style.display = 'none';
|
1184 |
+
outputSection.classList.remove('has-content');
|
1185 |
+
statusMessage.style.display = 'block'; // Show status temporarily if needed
|
1186 |
loadingAnimationWrapper.style.display = 'flex';
|
1187 |
generateBtn.disabled = true;
|
1188 |
generateBtn.innerHTML = `
|
|
|
1198 |
loadingAnimationWrapper.style.display = 'none';
|
1199 |
if (isSuccess) {
|
1200 |
statusMessage.style.display = 'none';
|
1201 |
+
audioPlayerContent.style.display = 'flex';
|
1202 |
+
outputSection.classList.add('has-content');
|
1203 |
+
hiddenAudioPlayer.play();
|
|
|
|
|
1204 |
} else {
|
1205 |
statusMessage.textContent = message || 'خطایی رخ داد. لطفاً دوباره تلاش کنید.';
|
1206 |
statusMessage.style.display = 'block';
|
1207 |
+
audioPlayerContent.style.display = 'none';
|
1208 |
+
outputSection.classList.remove('has-content');
|
1209 |
+
hiddenAudioPlayer.pause();
|
1210 |
+
hiddenAudioPlayer.src = '';
|
1211 |
}
|
1212 |
generateBtn.disabled = false;
|
1213 |
generateBtn.innerHTML = '✨ تولید صدا با آلفا';
|
|
|
1311 |
// Initial setup
|
1312 |
updateSelectedSpeakerDisplay(selectedSpeakerIdStorage.value || speakers[0].id);
|
1313 |
form.addEventListener('submit', generateAudio);
|
|
|
|
|
|
|
1314 |
statusMessage.style.display = 'block';
|
1315 |
loadingAnimationWrapper.style.display = 'none';
|
1316 |
+
audioPlayerContent.style.display = 'none';
|
1317 |
+
outputSection.classList.remove('has-content');
|
1318 |
});
|
1319 |
</script>
|
1320 |
</body>
|