|
149 | 149 | 50% { transform: scaleY(1); } |
150 | 150 | } |
151 | 151 |
|
| 152 | + /* Playlist Styles */ |
| 153 | + .audio-playlist { |
| 154 | + max-height: 200px; |
| 155 | + overflow-y: auto; |
| 156 | + margin-bottom: 1rem; |
| 157 | + border-radius: 0.5rem; |
| 158 | + background: rgba(0, 0, 0, 0.2); |
| 159 | + } |
| 160 | +
|
| 161 | + .audio-playlist::-webkit-scrollbar { |
| 162 | + width: 6px; |
| 163 | + } |
| 164 | +
|
| 165 | + .audio-playlist::-webkit-scrollbar-track { |
| 166 | + background: rgba(255, 255, 255, 0.05); |
| 167 | + border-radius: 3px; |
| 168 | + } |
| 169 | +
|
| 170 | + .audio-playlist::-webkit-scrollbar-thumb { |
| 171 | + background: rgba(233, 69, 96, 0.5); |
| 172 | + border-radius: 3px; |
| 173 | + } |
| 174 | +
|
| 175 | + .playlist-item { |
| 176 | + display: flex; |
| 177 | + align-items: center; |
| 178 | + padding: 0.5rem 0.75rem; |
| 179 | + cursor: pointer; |
| 180 | + transition: background 0.2s ease; |
| 181 | + border-bottom: 1px solid rgba(255, 255, 255, 0.05); |
| 182 | + gap: 0.5rem; |
| 183 | + } |
| 184 | +
|
| 185 | + .playlist-item:last-child { |
| 186 | + border-bottom: none; |
| 187 | + } |
| 188 | +
|
| 189 | + .playlist-item:hover { |
| 190 | + background: rgba(233, 69, 96, 0.1); |
| 191 | + } |
| 192 | +
|
| 193 | + .playlist-item.active { |
| 194 | + background: rgba(233, 69, 96, 0.2); |
| 195 | + } |
| 196 | +
|
| 197 | + .playlist-item-index { |
| 198 | + color: rgba(255, 255, 255, 0.4); |
| 199 | + font-size: 0.75rem; |
| 200 | + width: 20px; |
| 201 | + text-align: center; |
| 202 | + } |
| 203 | +
|
| 204 | + .playlist-item.active .playlist-item-index { |
| 205 | + color: #e94560; |
| 206 | + } |
| 207 | +
|
| 208 | + .playlist-item-icon { |
| 209 | + color: #e94560; |
| 210 | + font-size: 0.875rem; |
| 211 | + } |
| 212 | +
|
| 213 | + .playlist-item-title { |
| 214 | + flex: 1; |
| 215 | + color: rgba(255, 255, 255, 0.8); |
| 216 | + font-size: 0.85rem; |
| 217 | + overflow: hidden; |
| 218 | + text-overflow: ellipsis; |
| 219 | + white-space: nowrap; |
| 220 | + } |
| 221 | +
|
| 222 | + .playlist-item.active .playlist-item-title { |
| 223 | + color: #fff; |
| 224 | + font-weight: 500; |
| 225 | + } |
| 226 | +
|
| 227 | + .playlist-item-remove { |
| 228 | + color: rgba(255, 255, 255, 0.3); |
| 229 | + background: none; |
| 230 | + border: none; |
| 231 | + padding: 0.25rem; |
| 232 | + cursor: pointer; |
| 233 | + opacity: 0; |
| 234 | + transition: all 0.2s ease; |
| 235 | + } |
| 236 | +
|
| 237 | + .playlist-item:hover .playlist-item-remove { |
| 238 | + opacity: 1; |
| 239 | + } |
| 240 | +
|
| 241 | + .playlist-item-remove:hover { |
| 242 | + color: #e94560; |
| 243 | + } |
| 244 | +
|
| 245 | + .playlist-header { |
| 246 | + display: flex; |
| 247 | + justify-content: space-between; |
| 248 | + align-items: center; |
| 249 | + margin-bottom: 0.5rem; |
| 250 | + padding: 0 0.25rem; |
| 251 | + } |
| 252 | +
|
| 253 | + .playlist-header-title { |
| 254 | + color: rgba(255, 255, 255, 0.6); |
| 255 | + font-size: 0.75rem; |
| 256 | + text-transform: uppercase; |
| 257 | + letter-spacing: 0.5px; |
| 258 | + } |
| 259 | +
|
| 260 | + .playlist-header-count { |
| 261 | + color: rgba(255, 255, 255, 0.4); |
| 262 | + font-size: 0.75rem; |
| 263 | + } |
| 264 | +
|
| 265 | + .playlist-empty { |
| 266 | + text-align: center; |
| 267 | + padding: 1.5rem; |
| 268 | + color: rgba(255, 255, 255, 0.4); |
| 269 | + font-size: 0.85rem; |
| 270 | + } |
| 271 | +
|
152 | 272 | /* Custom Audio Controls */ |
153 | 273 | .audio-controls { |
154 | 274 | background: linear-gradient(135deg, #0f3460 0%, #16213e 100%); |
|
159 | 279 | gap: 0.75rem; |
160 | 280 | } |
161 | 281 |
|
| 282 | + .audio-main-controls { |
| 283 | + display: flex; |
| 284 | + align-items: center; |
| 285 | + justify-content: center; |
| 286 | + gap: 1rem; |
| 287 | + margin-bottom: 0.5rem; |
| 288 | + } |
| 289 | +
|
| 290 | + .audio-btn-nav { |
| 291 | + width: 36px; |
| 292 | + height: 36px; |
| 293 | + border-radius: 50%; |
| 294 | + border: none; |
| 295 | + background: rgba(255, 255, 255, 0.1); |
| 296 | + color: white; |
| 297 | + font-size: 1rem; |
| 298 | + display: flex; |
| 299 | + align-items: center; |
| 300 | + justify-content: center; |
| 301 | + cursor: pointer; |
| 302 | + transition: all 0.2s ease; |
| 303 | + } |
| 304 | +
|
| 305 | + .audio-btn-nav:hover { |
| 306 | + background: rgba(233, 69, 96, 0.3); |
| 307 | + } |
| 308 | +
|
| 309 | + .audio-btn-nav:disabled { |
| 310 | + opacity: 0.3; |
| 311 | + cursor: not-allowed; |
| 312 | + } |
| 313 | +
|
162 | 314 | .audio-controls-row { |
163 | 315 | display: flex; |
164 | 316 | align-items: center; |
|
379 | 531 | </div> |
380 | 532 |
|
381 | 533 | <div class="audio-controls"> |
382 | | - <div class="audio-controls-row"> |
| 534 | + <div class="audio-main-controls"> |
| 535 | + <button class="audio-btn-nav" @onclick="PlayPrevious" disabled="@(!CanPlayPrevious)"> |
| 536 | + <i class="bi bi-skip-backward-fill"></i> |
| 537 | + </button> |
383 | 538 | <button class="audio-btn-play" @onclick="TogglePlayPause"> |
384 | 539 | <i class="bi @(isPlaying ? "bi-pause-fill" : "bi-play-fill")"></i> |
385 | 540 | </button> |
| 541 | + <button class="audio-btn-nav" @onclick="PlayNext" disabled="@(!CanPlayNext)"> |
| 542 | + <i class="bi bi-skip-forward-fill"></i> |
| 543 | + </button> |
| 544 | + </div> |
| 545 | + <div class="audio-controls-row"> |
386 | 546 | <div class="audio-progress-container"> |
387 | 547 | <div class="audio-progress-bar" @onclick="SeekAudio"> |
388 | 548 | <div class="audio-progress-buffer" style="width: @(bufferPercent.ToString("F1", System.Globalization.CultureInfo.InvariantCulture))%"></div> |
|
403 | 563 | </div> |
404 | 564 | </div> |
405 | 565 | </div> |
| 566 | + |
| 567 | + @if (playlist.Count > 0) |
| 568 | + { |
| 569 | + <div class="playlist-header"> |
| 570 | + <span class="playlist-header-title">Playlist</span> |
| 571 | + <span class="playlist-header-count">@playlist.Count @(playlist.Count == 1 ? "track" : "tracks")</span> |
| 572 | + </div> |
| 573 | + <div class="audio-playlist"> |
| 574 | + @for (int i = 0; i < playlist.Count; i++) |
| 575 | + { |
| 576 | + var index = i; |
| 577 | + var item = playlist[i]; |
| 578 | + <div class="playlist-item @(index == currentTrackIndex ? "active" : "")" @onclick="() => PlayTrack(index)"> |
| 579 | + <span class="playlist-item-index">@(index + 1)</span> |
| 580 | + @if (index == currentTrackIndex && isPlaying) |
| 581 | + { |
| 582 | + <i class="bi bi-volume-up-fill playlist-item-icon"></i> |
| 583 | + } |
| 584 | + else |
| 585 | + { |
| 586 | + <i class="bi bi-music-note playlist-item-icon"></i> |
| 587 | + } |
| 588 | + <span class="playlist-item-title" title="@item.Title">@item.Title</span> |
| 589 | + <button class="playlist-item-remove" @onclick="() => RemoveFromPlaylist(index)" @onclick:stopPropagation="true"> |
| 590 | + <i class="bi bi-x"></i> |
| 591 | + </button> |
| 592 | + </div> |
| 593 | + } |
| 594 | + </div> |
| 595 | + } |
406 | 596 | </div> |
407 | 597 | </BodyTemplate> |
408 | 598 | <FooterTemplate> |
|
436 | 626 | private string durationDisplay = "0:00"; |
437 | 627 | private CancellationTokenSource? progressCts; |
438 | 628 |
|
| 629 | + // Playlist |
| 630 | + private List<PlaylistItem> playlist = new(); |
| 631 | + private int currentTrackIndex = -1; |
| 632 | + |
| 633 | + public class PlaylistItem |
| 634 | + { |
| 635 | + public string Url { get; set; } = ""; |
| 636 | + public string Type { get; set; } = "audio/mpeg"; |
| 637 | + public string Title { get; set; } = ""; |
| 638 | + } |
| 639 | + |
| 640 | + private bool CanPlayPrevious => currentTrackIndex > 0; |
| 641 | + private bool CanPlayNext => currentTrackIndex < playlist.Count - 1; |
| 642 | + |
439 | 643 | public async Task ShowModal(string url, string type = "audio/mpeg", string title = "") |
440 | 644 | { |
441 | 645 | bool isSameTrack = audioUrl == url; |
|
444 | 648 | audioType = string.IsNullOrEmpty(type) ? "audio/mpeg" : type; |
445 | 649 | audioTitle = !string.IsNullOrEmpty(title) ? title : ExtractFileName(url); |
446 | 650 |
|
| 651 | + // Añadir a playlist si no existe |
| 652 | + var existingIndex = playlist.FindIndex(p => p.Url == url); |
| 653 | + if (existingIndex < 0) |
| 654 | + { |
| 655 | + playlist.Add(new PlaylistItem { Url = url, Type = audioType, Title = audioTitle }); |
| 656 | + currentTrackIndex = playlist.Count - 1; |
| 657 | + } |
| 658 | + else |
| 659 | + { |
| 660 | + currentTrackIndex = existingIndex; |
| 661 | + } |
| 662 | + |
447 | 663 | await modal.ShowAsync(); |
448 | 664 | await Task.Delay(100); |
449 | 665 |
|
|
601 | 817 | StateHasChanged(); |
602 | 818 | } |
603 | 819 |
|
604 | | - private void OnEnded() |
| 820 | + private async void OnEnded() |
605 | 821 | { |
606 | 822 | isPlaying = false; |
| 823 | + // Reproducir siguiente canción automáticamente |
| 824 | + if (CanPlayNext) |
| 825 | + { |
| 826 | + await PlayNext(); |
| 827 | + } |
607 | 828 | StateHasChanged(); |
608 | 829 | } |
609 | 830 |
|
| 831 | + // Playlist methods |
| 832 | + private async Task PlayTrack(int index) |
| 833 | + { |
| 834 | + if (index >= 0 && index < playlist.Count) |
| 835 | + { |
| 836 | + currentTrackIndex = index; |
| 837 | + var track = playlist[index]; |
| 838 | + audioUrl = track.Url; |
| 839 | + audioType = track.Type; |
| 840 | + audioTitle = track.Title; |
| 841 | + progressPercent = 0; |
| 842 | + currentTimeDisplay = "0:00"; |
| 843 | + await JSRuntime.InvokeVoidAsync("playAudioPlayer", audioUrl, audioType); |
| 844 | + StateHasChanged(); |
| 845 | + } |
| 846 | + } |
| 847 | + |
| 848 | + private async Task PlayPrevious() |
| 849 | + { |
| 850 | + if (CanPlayPrevious) |
| 851 | + { |
| 852 | + await PlayTrack(currentTrackIndex - 1); |
| 853 | + } |
| 854 | + } |
| 855 | + |
| 856 | + private async Task PlayNext() |
| 857 | + { |
| 858 | + if (CanPlayNext) |
| 859 | + { |
| 860 | + await PlayTrack(currentTrackIndex + 1); |
| 861 | + } |
| 862 | + } |
| 863 | + |
| 864 | + private void RemoveFromPlaylist(int index) |
| 865 | + { |
| 866 | + if (index >= 0 && index < playlist.Count) |
| 867 | + { |
| 868 | + playlist.RemoveAt(index); |
| 869 | + |
| 870 | + // Ajustar índice actual si es necesario |
| 871 | + if (index < currentTrackIndex) |
| 872 | + { |
| 873 | + currentTrackIndex--; |
| 874 | + } |
| 875 | + else if (index == currentTrackIndex) |
| 876 | + { |
| 877 | + // Si se elimina la canción actual, no cambiar nada |
| 878 | + // El audio sigue reproduciéndose pero ya no está en la lista |
| 879 | + currentTrackIndex = -1; |
| 880 | + } |
| 881 | + StateHasChanged(); |
| 882 | + } |
| 883 | + } |
| 884 | + |
| 885 | + public void AddToPlaylist(string url, string type = "audio/mpeg", string title = "") |
| 886 | + { |
| 887 | + var item = new PlaylistItem |
| 888 | + { |
| 889 | + Url = url, |
| 890 | + Type = string.IsNullOrEmpty(type) ? "audio/mpeg" : type, |
| 891 | + Title = !string.IsNullOrEmpty(title) ? title : ExtractFileName(url) |
| 892 | + }; |
| 893 | + |
| 894 | + // Evitar duplicados |
| 895 | + if (!playlist.Any(p => p.Url == url)) |
| 896 | + { |
| 897 | + playlist.Add(item); |
| 898 | + StateHasChanged(); |
| 899 | + } |
| 900 | + } |
| 901 | + |
| 902 | + public void AddToPlaylistAndPlay(string url, string type = "audio/mpeg", string title = "") |
| 903 | + { |
| 904 | + var existingIndex = playlist.FindIndex(p => p.Url == url); |
| 905 | + |
| 906 | + if (existingIndex >= 0) |
| 907 | + { |
| 908 | + // Ya existe, reproducir esa |
| 909 | + _ = PlayTrack(existingIndex); |
| 910 | + } |
| 911 | + else |
| 912 | + { |
| 913 | + // Añadir y reproducir |
| 914 | + AddToPlaylist(url, type, title); |
| 915 | + _ = PlayTrack(playlist.Count - 1); |
| 916 | + } |
| 917 | + } |
| 918 | + |
610 | 919 | private string ExtractFileName(string url) |
611 | 920 | { |
612 | 921 | if (string.IsNullOrEmpty(url)) return ""; |
|
0 commit comments