Skip to content

Commit 3a2476f

Browse files
committed
feat: add audio playlist
1 parent 5d7bc8a commit 3a2476f

9 files changed

Lines changed: 472 additions & 14 deletions

File tree

TelegramDownloader/Pages/Modals/AudioPlayerModal.razor

Lines changed: 311 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,126 @@
149149
50% { transform: scaleY(1); }
150150
}
151151
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+
152272
/* Custom Audio Controls */
153273
.audio-controls {
154274
background: linear-gradient(135deg, #0f3460 0%, #16213e 100%);
@@ -159,6 +279,38 @@
159279
gap: 0.75rem;
160280
}
161281
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+
162314
.audio-controls-row {
163315
display: flex;
164316
align-items: center;
@@ -379,10 +531,18 @@
379531
</div>
380532

381533
<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>
383538
<button class="audio-btn-play" @onclick="TogglePlayPause">
384539
<i class="bi @(isPlaying ? "bi-pause-fill" : "bi-play-fill")"></i>
385540
</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">
386546
<div class="audio-progress-container">
387547
<div class="audio-progress-bar" @onclick="SeekAudio">
388548
<div class="audio-progress-buffer" style="width: @(bufferPercent.ToString("F1", System.Globalization.CultureInfo.InvariantCulture))%"></div>
@@ -403,6 +563,36 @@
403563
</div>
404564
</div>
405565
</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+
}
406596
</div>
407597
</BodyTemplate>
408598
<FooterTemplate>
@@ -436,6 +626,20 @@
436626
private string durationDisplay = "0:00";
437627
private CancellationTokenSource? progressCts;
438628

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+
439643
public async Task ShowModal(string url, string type = "audio/mpeg", string title = "")
440644
{
441645
bool isSameTrack = audioUrl == url;
@@ -444,6 +648,18 @@
444648
audioType = string.IsNullOrEmpty(type) ? "audio/mpeg" : type;
445649
audioTitle = !string.IsNullOrEmpty(title) ? title : ExtractFileName(url);
446650

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+
447663
await modal.ShowAsync();
448664
await Task.Delay(100);
449665

@@ -601,12 +817,105 @@
601817
StateHasChanged();
602818
}
603819

604-
private void OnEnded()
820+
private async void OnEnded()
605821
{
606822
isPlaying = false;
823+
// Reproducir siguiente canción automáticamente
824+
if (CanPlayNext)
825+
{
826+
await PlayNext();
827+
}
607828
StateHasChanged();
608829
}
609830

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+
610919
private string ExtractFileName(string url)
611920
{
612921
if (string.IsNullOrEmpty(url)) return "";

TelegramDownloader/Pages/Modals/FileUploadModalTemplate.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
if (firstRender)
6161
{
6262
await JS.InvokeAsync<IJSObjectReference>("import",
63-
"/js/fileUploadVue.js?v=11");
63+
"/js/fileUploadVue.js?v=12");
6464

6565
}
6666

0 commit comments

Comments
 (0)