Hamed744 commited on
Commit
c3c2a7a
·
verified ·
1 Parent(s): 68f7624

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +137 -187
index.html CHANGED
@@ -464,25 +464,22 @@
464
  align-items: center;
465
  justify-content: center;
466
  flex-direction: column;
467
- gap: 1.5rem;
468
- min-height: 200px; /* Default height for empty/loading state */
469
  position: relative;
470
  box-sizing: border-box;
471
- padding: 2rem; /* Default padding for status/loading */
472
- background-color: var(--input-bg); /* Default background for empty/loading */
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), border-color 0.5s, box-shadow 0.5s;
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); /* Solid white background like the image */
481
- border-style: solid; /* Solid border for active state */
482
- border-color: var(--panel-border); /* Standard border */
483
- box-shadow: var(--shadow-medium); /* Nicer shadow for active state */
484
- padding: 0; /* No padding on the main output-section, inner div handles it */
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; /* Controlled by JS */
560
  width: 100%;
561
- padding: 1.5rem; /* Padding applied here, inside the output-section card */
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: 0.8rem;
596
  width: 100%;
597
- margin-bottom: 1rem; /* Space between waveform and controls */
598
  }
599
  .audio-time {
600
  font-size: 0.9em;
601
  color: var(--text-secondary);
602
- min-width: 40px; /* Ensure consistent width */
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; /* Fixed height for the waveform area */
611
  position: relative;
612
- overflow: hidden; /* To clip bars */
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; /* Distribute bars */
625
- align-items: center; /* Center bars vertically */
626
- z-index: 0; /* Under the dashed line */
627
  }
628
-
629
  .waveform-bar {
630
- width: 4px; /* Width of each bar */
631
- min-width: 2px; /* Ensure small bars are still visible */
632
- background-color: var(--waveform-color-inactive); /* Default grey color */
633
- border-radius: 2px; /* Rounded ends for bars */
634
- transition: background-color 0.1s linear; /* Smooth color change */
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; /* Height of the dashed line itself */
646
  background-image: linear-gradient(to right, var(--waveform-dashed-line-color) 33%, transparent 0%);
647
  background-position: center;
648
- background-size: 10px 1px; /* Dash length + gap */
649
  transform: translateY(-50%);
650
- z-index: 1; /* Above the bars */
651
  }
652
 
653
  /* Audio Controls */
654
  .audio-controls-group {
655
  display: flex;
656
- justify-content: center; /* Center the main controls */
657
  align-items: center;
658
- gap: 1.5rem; /* Space between buttons */
659
- margin-bottom: 1rem; /* Space between main controls and bottom controls */
660
  }
661
-
662
- .audio-skip-btn {
663
  background: none;
664
  border: none;
665
  cursor: pointer;
666
  padding: 8px;
667
- color: var(--text-secondary); /* Grey color */
668
- transition: transform 0.2s;
669
  }
670
- .audio-skip-btn svg {
671
- width: 24px;
672
- height: 24px;
673
- fill: currentColor;
674
  }
675
- .audio-skip-btn:hover {
676
- transform: scale(1.1);
677
  }
678
- .audio-skip-btn:active {
679
  transform: scale(0.9);
680
  }
681
-
682
- .audio-play-pause-btn-large {
683
- background: none;
684
- border: none;
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:active {
701
- transform: scale(0.95);
 
 
702
  }
703
  .audio-play-pause-btn-large svg {
704
- width: 36px; /* Icon size */
705
- height: 36px;
706
  fill: currentColor;
707
  }
708
 
709
- .audio-bottom-controls {
710
  display: flex;
711
  align-items: center;
712
- justify-content: flex-start; /* Volume/Speed on the left, nothing on the right */
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); /* Match the image's "1x" button background */
742
- border: 1px solid var(--panel-border); /* Match the image's border */
743
- padding: 6px 12px;
744
  border-radius: 8px;
745
  min-width: 40px;
746
  text-align: center;
747
  color: var(--text-primary);
748
- cursor: pointer;
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); /* Slightly darker on hover */
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-bottom-controls">
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'); // Main player content wrapper
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, 1.75, 2.0, 0.75, 0.5]; // Common speeds
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 = ''; // Clear existing bars
1120
- const maxBarHeight = 100; // Percentage of total waveform height
1121
- const minBarHeight = 10;
1122
- for (let i = 0; i < NUM_WAVEFORM_BARS; i++) {
 
 
 
 
 
 
1123
  const bar = document.createElement('div');
1124
  bar.classList.add('waveform-bar');
1125
  // Simple randomized heights for visual variety
1126
- const height = Math.random() * (maxBarHeight - minBarHeight) + minBarHeight;
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 (play/pause icon, time, progress)
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
- for (let i = 0; i < bars.length; i++) {
 
 
 
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 - 10); // Skip 10 seconds back
1189
  });
1190
-
1191
  skipForwardBtn.addEventListener('click', () => {
1192
- hiddenAudioPlayer.currentTime = Math.min(hiddenAudioPlayer.duration, hiddenAudioPlayer.currentTime + 10); // Skip 10 seconds forward
1193
  });
1194
-
1195
  volumeBtn.addEventListener('click', () => {
1196
- if (hiddenAudioPlayer.muted) {
1197
- hiddenAudioPlayer.muted = false;
1198
- volumeHighIcon.style.display = 'block';
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(); // Generate bars once metadata is loaded
1218
- updatePlayerUI(); // Update UI with total duration and initial state
1219
  });
1220
  hiddenAudioPlayer.addEventListener('play', updatePlayerUI);
1221
  hiddenAudioPlayer.addEventListener('pause', updatePlayerUI);
1222
  hiddenAudioPlayer.addEventListener('ended', () => {
1223
- hiddenAudioPlayer.currentTime = 0; // Reset to start
1224
  updatePlayerUI();
1225
  });
1226
 
 
 
 
 
 
 
 
 
 
 
 
 
1227
  // --- State Management Functions ---
1228
  function showLoadingState() {
1229
- audioPlayerContent.style.display = 'none'; // Hide custom player
1230
- outputSection.classList.remove('has-content'); // Remove has-content class to revert output-section styling
1231
- statusMessage.style.display = 'none';
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'; // Show custom player content
1248
- outputSection.classList.add('has-content'); // Add class to output section parent
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'; // Hide custom player content
1256
- outputSection.classList.remove('has-content'); // Remove class to revert output-section styling
1257
- hiddenAudioPlayer.pause(); // Stop any potential audio
1258
- hiddenAudioPlayer.src = ''; // Clear audio source
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
- outputSection.classList.remove('has-content'); // Ensure initial state is clean
 
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>