Проведен аудит. Добавлено переключение треков
This commit is contained in:
@@ -17,7 +17,13 @@
|
||||
<MudItem>
|
||||
<MudStack Spacing="0">
|
||||
<MudText Typo="Typo.body1" Color="Color.Secondary">@Track.Title</MudText>
|
||||
<MudText Typo="Typo.body2" >@string.Join(", ", Track.Artists.Select(a => a.Name))</MudText>
|
||||
<MudStack Row AlignItems="AlignItems.Center" Spacing="1">
|
||||
<MudText Typo="Typo.body2">@string.Join(", ", Track.Artists.Select(a => a.Name))</MudText>
|
||||
@if (!string.IsNullOrEmpty(AddedByName))
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="Color.Secondary" Class="pa-0 px-1" Style="height: 18px; font-size: 10px;">@AddedByName</MudChip>
|
||||
}
|
||||
</MudStack>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
|
||||
@@ -33,4 +39,5 @@
|
||||
[Parameter] public YandexTrack Track { get; set; } = null!;
|
||||
[Parameter] public string PlaylistShareToken { get; set; } = string.Empty;
|
||||
[Parameter] public bool CanPlay { get; set; } = true;
|
||||
[Parameter] public string? AddedByName { get; set; }
|
||||
}
|
||||
@@ -11,6 +11,12 @@
|
||||
|
||||
<MudStack Spacing="1" Row AlignItems="AlignItems.Center" Wrap="Wrap.NoWrap">
|
||||
<!-- Кнопки управления -->
|
||||
<MudIconButton Icon="@Icons.Material.Filled.SkipPrevious"
|
||||
Size="Size.Small"
|
||||
Color="Color.Default"
|
||||
Disabled="@(!AudioPlayerService.HasPrevious)"
|
||||
OnClick="() => AudioPlayerService.PlayPreviousAsync()" />
|
||||
|
||||
<MudItem @onmouseenter="() => { _isPlayHovered = true; }"
|
||||
@onmouseleave="() => { _isPlayHovered = false; }"
|
||||
Class="relative d-inline-block rounded overflow-hidden cursor-pointer"
|
||||
@@ -32,7 +38,13 @@
|
||||
ToggledChanged="OnPlayClick" />
|
||||
</MudItem>
|
||||
</MudItem>
|
||||
|
||||
|
||||
<MudIconButton Icon="@Icons.Material.Filled.SkipNext"
|
||||
Size="Size.Small"
|
||||
Color="Color.Default"
|
||||
Disabled="@(!AudioPlayerService.HasNext)"
|
||||
OnClick="() => AudioPlayerService.PlayNextAsync()" />
|
||||
|
||||
<!-- Название и прогресс -->
|
||||
@if (AudioPlayerService.CurrentTrack != null)
|
||||
{
|
||||
@@ -170,15 +182,9 @@
|
||||
#endregion
|
||||
|
||||
#region Обработка сервиса
|
||||
private async Task OnServiceLoadAndPlay(string trackId, string? accessToken, string? sharedPlaylistId)
|
||||
private async Task OnServiceLoadAndPlay(string trackId, string? playToken, string? sharedPlaylistId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(accessToken))
|
||||
{
|
||||
var tokens = await TokenStorage.GetTokensAsync();
|
||||
accessToken = tokens.token;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(accessToken) && string.IsNullOrWhiteSpace(sharedPlaylistId))
|
||||
if (string.IsNullOrWhiteSpace(playToken) && string.IsNullOrWhiteSpace(sharedPlaylistId))
|
||||
{
|
||||
Snackbar.Add("Токен авторизации не найден. Пожалуйста, войдите заново.", Severity.Error);
|
||||
return;
|
||||
@@ -186,7 +192,7 @@
|
||||
|
||||
var streamUrl = new Uri(Http.BaseAddress!, $"/api/audio/track/{trackId}").ToString();
|
||||
await EnsureAudioModuleAsync();
|
||||
await _audioElement!.InvokeVoidAsync("loadAndPlay", streamUrl, accessToken, sharedPlaylistId);
|
||||
await _audioElement!.InvokeVoidAsync("loadAndPlay", streamUrl, playToken, sharedPlaylistId);
|
||||
}
|
||||
|
||||
private async Task OnServicePlay()
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
@page "/shared/{token}"
|
||||
<PageTitle>@_playlist?.Title - Playlist Share</PageTitle>
|
||||
|
||||
<HeadContent>
|
||||
<meta property="og:title" content="@(_playlist?.Title ?? "Поделиться плейлистом")" />
|
||||
<meta property="og:description" content="@($"Слушайте плейлист «{_playlist?.Title}» на Playlist Share")" />
|
||||
@if (!string.IsNullOrEmpty(_playlist?.CoverUrl))
|
||||
{
|
||||
<meta property="og:image" content="@_playlist.CoverUrl" />
|
||||
}
|
||||
<meta property="og:url" content="@Navigation.Uri" />
|
||||
<meta property="og:type" content="music.playlist" />
|
||||
</HeadContent>
|
||||
|
||||
@using PlaylistShared.Pwa.Components.Common
|
||||
@using PlaylistShared.Pwa.Components.Global
|
||||
@using PlaylistShared.Pwa.Components.SharedPlaylist
|
||||
@@ -245,7 +256,8 @@
|
||||
Style="min-height: 0;">
|
||||
<RowTemplate>
|
||||
<MudTd Class="py-1 px-0" Style="width: 100%;">
|
||||
<TrackItem Track="@context" PlaylistShareToken="@Token" CanPlay="@_canPlay" />
|
||||
<TrackItem Track="@context" PlaylistShareToken="@Token" CanPlay="@_canPlay"
|
||||
AddedByName="@(_trackAdditions.TryGetValue(context.TrackId, out var n) ? n : null)" />
|
||||
</MudTd>
|
||||
@if (_canRemove)
|
||||
{
|
||||
@@ -444,6 +456,10 @@
|
||||
/// </summary>
|
||||
private List<YandexTrack> _tracks = new();
|
||||
/// <summary>
|
||||
/// Словарь trackId → имя пользователя, добавившего трек.
|
||||
/// </summary>
|
||||
private Dictionary<string, string?> _trackAdditions = new();
|
||||
/// <summary>
|
||||
/// Продолжительность плейлиста.
|
||||
/// </summary>
|
||||
long _playlistDurationMs;
|
||||
@@ -621,6 +637,15 @@
|
||||
_existingTrackIds = _tracks.Select(t => t.TrackId).ToHashSet();
|
||||
_playlistDurationMs = _tracks.Sum(t => t.DurationMs);
|
||||
_playlistTrackCount = _tracks.Count();
|
||||
AudioPlayerService.SetQueue(_tracks, shareToken: Token);
|
||||
|
||||
try
|
||||
{
|
||||
var additionsResp = await Http.GetFromJsonAsync<ApiResponse<Dictionary<string, string?>>>($"/api/sharedplaylist/{Token}/additions");
|
||||
if (additionsResp?.Success == true && additionsResp.Data != null)
|
||||
_trackAdditions = additionsResp.Data;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using MudBlazor;
|
||||
using MudBlazor;
|
||||
using PlaylistShared.Shared;
|
||||
using PlaylistShared.Shared.Yandex;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace PlaylistShared.Pwa.Services;
|
||||
@@ -22,9 +23,16 @@ public class AudioPlayerService : IAudioPlayerService
|
||||
private string _currentTimeString = "0:00";
|
||||
private string _totalTimeString = "0:00";
|
||||
|
||||
private List<YandexTrack> _queue = new();
|
||||
private int _queueIndex = -1;
|
||||
private string? _queueShareToken;
|
||||
|
||||
public string? CurrentTrackId => _currentTrackId;
|
||||
public YandexTrack? CurrentTrack => _currentTrack;
|
||||
public bool IsPlaying => _isPlaying;
|
||||
public IReadOnlyList<YandexTrack> CurrentQueue => _queue;
|
||||
public bool HasNext => _queueIndex >= 0 && _queueIndex < _queue.Count - 1;
|
||||
public bool HasPrevious => _queueIndex > 0;
|
||||
public double CurrentVolume
|
||||
{
|
||||
get => _currentVolume;
|
||||
@@ -57,14 +65,10 @@ public class AudioPlayerService : IAudioPlayerService
|
||||
private async Task LoadVolume()
|
||||
{
|
||||
var savedVolume = await _playerStorage.GetVolumeAsync();
|
||||
|
||||
if (savedVolume != null)
|
||||
{
|
||||
_currentVolume = savedVolume.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// Внешние команды (вызываются из компонентов)
|
||||
public async Task LoadAndPlayAsync(string trackId, string? accessToken = null, string? playlistShareToken = null, YandexTrack? track = null)
|
||||
{
|
||||
if (_currentTrackId == trackId)
|
||||
@@ -75,30 +79,33 @@ public class AudioPlayerService : IAudioPlayerService
|
||||
|
||||
_currentTrackId = trackId;
|
||||
|
||||
// Если accessToken не передан, пытаемся получить его из хранилища
|
||||
var idx = _queue.FindIndex(t => t.TrackId == trackId);
|
||||
if (idx >= 0) _queueIndex = idx;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(accessToken))
|
||||
{
|
||||
var tokens = await _tokenStorage.GetTokensAsync();
|
||||
accessToken = tokens.token;
|
||||
}
|
||||
|
||||
// Проверяем, есть ли чем авторизоваться
|
||||
if (string.IsNullOrWhiteSpace(accessToken) && string.IsNullOrWhiteSpace(playlistShareToken))
|
||||
string? playToken = null;
|
||||
if (!string.IsNullOrWhiteSpace(accessToken))
|
||||
playToken = await FetchPlayTokenAsync(accessToken);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(playToken) && string.IsNullOrWhiteSpace(playlistShareToken))
|
||||
{
|
||||
_snackbar.Add("Не удалось воспроизвести трек: отсутствует токен авторизации или идентификатор расшаренного плейлиста.", Severity.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Если title и coverUrl не переданы, нужно запросить через API
|
||||
if (track is null)
|
||||
{
|
||||
try
|
||||
{
|
||||
track = await GetTrackInfo(trackId, accessToken, playlistShareToken);
|
||||
track = await GetTrackInfo(trackId, playToken, playlistShareToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Логируем ошибку, но продолжаем без обложки/названия
|
||||
Console.WriteLine($"Failed to fetch track info: {ex.Message}");
|
||||
}
|
||||
}
|
||||
@@ -106,7 +113,7 @@ public class AudioPlayerService : IAudioPlayerService
|
||||
_currentTrack = track;
|
||||
_isPlaying = true;
|
||||
OnStateChanged?.Invoke();
|
||||
OnLoadAndPlayRequested?.Invoke(trackId, accessToken, playlistShareToken);
|
||||
OnLoadAndPlayRequested?.Invoke(trackId, playToken, playlistShareToken);
|
||||
OnStartedTrack?.Invoke();
|
||||
}
|
||||
|
||||
@@ -138,14 +145,36 @@ public class AudioPlayerService : IAudioPlayerService
|
||||
await _playerStorage.SetVolumeAsync(volume);
|
||||
}
|
||||
|
||||
// События для связи с реальным AudioPlayer компонентом
|
||||
public void SetQueue(IEnumerable<YandexTrack> tracks, int startIndex = 0, string? shareToken = null)
|
||||
{
|
||||
_queue = tracks.ToList();
|
||||
_queueIndex = _queue.Count > 0 ? Math.Clamp(startIndex, 0, _queue.Count - 1) : -1;
|
||||
_queueShareToken = shareToken;
|
||||
OnStateChanged?.Invoke();
|
||||
}
|
||||
|
||||
public async Task PlayNextAsync()
|
||||
{
|
||||
if (!HasNext) return;
|
||||
_queueIndex++;
|
||||
var track = _queue[_queueIndex];
|
||||
await LoadAndPlayAsync(track.TrackId, playlistShareToken: _queueShareToken, track: track);
|
||||
}
|
||||
|
||||
public async Task PlayPreviousAsync()
|
||||
{
|
||||
if (!HasPrevious) return;
|
||||
_queueIndex--;
|
||||
var track = _queue[_queueIndex];
|
||||
await LoadAndPlayAsync(track.TrackId, playlistShareToken: _queueShareToken, track: track);
|
||||
}
|
||||
|
||||
public event Func<string, string?, string?, Task>? OnLoadAndPlayRequested;
|
||||
public event Func<Task>? OnPlayRequested;
|
||||
public event Func<Task>? OnPauseRequested;
|
||||
public event Func<double, Task>? OnSeekRequested;
|
||||
public event Func<double, Task>? OnVolumeChangeRequested;
|
||||
|
||||
// Внутренние методы для обновления состояния из AudioPlayer
|
||||
public void SetPlayingState(bool isPlaying)
|
||||
{
|
||||
_isPlaying = isPlaying;
|
||||
@@ -172,37 +201,40 @@ public class AudioPlayerService : IAudioPlayerService
|
||||
public void NotifyTrackEnded()
|
||||
{
|
||||
_isPlaying = false;
|
||||
_currentTrackId = null;
|
||||
_currentProgress = 0;
|
||||
_currentTime = 0;
|
||||
_currentTimeString = "0:00";
|
||||
_totalTime = 0;
|
||||
_currentTimeString = "0:00";
|
||||
_totalTimeString = "0:00";
|
||||
OnStateChanged?.Invoke();
|
||||
OnEndedTrack?.Invoke();
|
||||
|
||||
if (HasNext)
|
||||
_ = PlayNextAsync();
|
||||
else
|
||||
_currentTrackId = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Вспомогательный метод для получения информации о треке через API
|
||||
/// </summary>
|
||||
/// <param name="trackId"></param>
|
||||
/// <param name="accessToken"></param>
|
||||
/// <param name="sharedPlaylistId"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<YandexTrack?> GetTrackInfo(string trackId, string? accessToken, string? sharedPlaylistId)
|
||||
private async Task<string?> FetchPlayTokenAsync(string jwt)
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, "/api/audio/play-token");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
|
||||
using var response = await _http.SendAsync(request);
|
||||
if (!response.IsSuccessStatusCode) return null;
|
||||
var result = await response.Content.ReadFromJsonAsync<ApiResponse<string>>();
|
||||
return result?.Data;
|
||||
}
|
||||
|
||||
private async Task<YandexTrack?> GetTrackInfo(string trackId, string? playToken, string? sharedPlaylistId)
|
||||
{
|
||||
var url = $"/api/audio/track-info/{trackId}";
|
||||
if (!string.IsNullOrEmpty(accessToken))
|
||||
url += $"?access_token={accessToken}";
|
||||
if (!string.IsNullOrEmpty(playToken))
|
||||
url += $"?play_token={playToken}";
|
||||
else if (!string.IsNullOrEmpty(sharedPlaylistId))
|
||||
url += $"?shared_id={sharedPlaylistId}";
|
||||
|
||||
var response = await _http.GetFromJsonAsync<ApiResponse<YandexTrack>>(url);
|
||||
if (response?.Success == true)
|
||||
{
|
||||
return response.Data;
|
||||
}
|
||||
return null;
|
||||
return response?.Success == true ? response.Data : null;
|
||||
}
|
||||
|
||||
private string FormatDuration(double seconds)
|
||||
@@ -211,4 +243,4 @@ public class AudioPlayerService : IAudioPlayerService
|
||||
var secs = (int)(seconds % 60);
|
||||
return $"{mins}:{secs:D2}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,15 @@ public interface IAudioPlayerService
|
||||
/// <summary>ID текущего воспроизводимого трека (null, если ничего не играет).</summary>
|
||||
string? CurrentTrackId { get; }
|
||||
|
||||
/// <summary>Очередь треков.</summary>
|
||||
IReadOnlyList<YandexTrack> CurrentQueue { get; }
|
||||
|
||||
/// <summary>Есть ли следующий трек в очереди.</summary>
|
||||
bool HasNext { get; }
|
||||
|
||||
/// <summary>Есть ли предыдущий трек в очереди.</summary>
|
||||
bool HasPrevious { get; }
|
||||
|
||||
/// <summary>Играет ли в данный момент (true) или приостановлен (false).</summary>
|
||||
bool IsPlaying { get; }
|
||||
|
||||
@@ -57,6 +66,15 @@ public interface IAudioPlayerService
|
||||
|
||||
/// <summary>Установить громкость (0–100).</summary>
|
||||
Task SetVolumeAsync(double volume);
|
||||
|
||||
/// <summary>Установить очередь треков и начать воспроизведение с указанного индекса.</summary>
|
||||
void SetQueue(IEnumerable<YandexTrack> tracks, int startIndex = 0, string? shareToken = null);
|
||||
|
||||
/// <summary>Перейти к следующему треку в очереди.</summary>
|
||||
Task PlayNextAsync();
|
||||
|
||||
/// <summary>Перейти к предыдущему треку в очереди.</summary>
|
||||
Task PlayPreviousAsync();
|
||||
#endregion
|
||||
|
||||
#region События для подписки на изменения состояния
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
const loadAndPlay = (src, token, sharedPlaylistId) => {
|
||||
const url = new URL(src, window.location.href);
|
||||
if (token) url.searchParams.set('access_token', token);
|
||||
if (token) url.searchParams.set('play_token', token);
|
||||
if (sharedPlaylistId) url.searchParams.set('shared_id', sharedPlaylistId);
|
||||
audio.src = url.toString();
|
||||
audio.load();
|
||||
|
||||
Reference in New Issue
Block a user