Вынесен глобальный плеер
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
@using Microsoft.AspNetCore.Components.Web
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@inject IAudioPlayerService AudioPlayerService
|
||||||
|
|
||||||
<div class="track-cover-container"
|
<div class="track-cover-container"
|
||||||
@onmouseenter="HandleMouseEnter"
|
@onmouseenter="HandleMouseEnter"
|
||||||
@@ -7,7 +8,7 @@
|
|||||||
|
|
||||||
<MudImage Src="@FormatCoverUrl(CoverUrl)" Height="@Height" Width="@Width" Class="rounded" Style="display: block;" />
|
<MudImage Src="@FormatCoverUrl(CoverUrl)" Height="@Height" Width="@Width" Class="rounded" Style="display: block;" />
|
||||||
|
|
||||||
@if (_isHovered || IsPlaying)
|
@if (_isHovered || IsCurrentTrackPlaying)
|
||||||
{
|
{
|
||||||
<div class="play-overlay"
|
<div class="play-overlay"
|
||||||
style="position: absolute; top: 0; left: 0; right: 0; bottom: 0;
|
style="position: absolute; top: 0; left: 0; right: 0; bottom: 0;
|
||||||
@@ -16,7 +17,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: 4px;">
|
border-radius: 4px;">
|
||||||
<MudIconButton Icon="@(IsPlaying? Icons.Material.Filled.Pause : Icons.Material.Filled.PlayArrow)"
|
<MudIconButton Icon="@(IsCurrentTrackPlaying? Icons.Material.Filled.Pause : Icons.Material.Filled.PlayArrow)"
|
||||||
Color="Color.Inherit"
|
Color="Color.Inherit"
|
||||||
Size="Size.Large"
|
Size="Size.Large"
|
||||||
OnClick="OnPlayClick" />
|
OnClick="OnPlayClick" />
|
||||||
@@ -27,19 +28,39 @@
|
|||||||
@code {
|
@code {
|
||||||
[Parameter] public string CoverUrl { get; set; } = string.Empty;
|
[Parameter] public string CoverUrl { get; set; } = string.Empty;
|
||||||
[Parameter] public string TrackId { get; set; } = string.Empty;
|
[Parameter] public string TrackId { get; set; } = string.Empty;
|
||||||
[Parameter] public bool IsPlaying { get; set; } = false;
|
|
||||||
[Parameter] public EventCallback<string> OnPlay { get; set; }
|
|
||||||
[Parameter] public int Height { get; set; } = 50;
|
[Parameter] public int Height { get; set; } = 50;
|
||||||
[Parameter] public int Width { get; set; } = 50;
|
[Parameter] public int Width { get; set; } = 50;
|
||||||
|
[Parameter] public string SharedPlaylistId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
private bool IsCurrentTrackPlaying => AudioPlayerService.IsPlaying && AudioPlayerService.CurrentTrackId == TrackId;
|
||||||
|
|
||||||
private bool _isHovered;
|
private bool _isHovered;
|
||||||
|
|
||||||
private void HandleMouseEnter() => _isHovered = true;
|
private void HandleMouseEnter() => _isHovered = true;
|
||||||
private void HandleMouseLeave() => _isHovered = false;
|
private void HandleMouseLeave() => _isHovered = false;
|
||||||
|
|
||||||
|
protected override void OnInitialized()
|
||||||
|
{
|
||||||
|
AudioPlayerService.OnStateChanged += OnPlayerStateChanged;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task OnPlayClick()
|
private async Task OnPlayClick()
|
||||||
{
|
{
|
||||||
await OnPlay.InvokeAsync(TrackId);
|
var sharedPlaylistId = string.IsNullOrWhiteSpace(SharedPlaylistId) ? null : SharedPlaylistId;
|
||||||
|
|
||||||
|
if (IsCurrentTrackPlaying)
|
||||||
|
{
|
||||||
|
await AudioPlayerService.PauseAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await AudioPlayerService.LoadAndPlayAsync(TrackId, sharedPlaylistId: SharedPlaylistId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPlayerStateChanged()
|
||||||
|
{
|
||||||
|
InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string FormatCoverUrl(string? url)
|
private string FormatCoverUrl(string? url)
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
@using Microsoft.AspNetCore.Components.Authorization
|
@inject IAudioPlayerService AudioPlayerService
|
||||||
@namespace PlaylistShared.Pwa.Components
|
@inject IJSRuntime JS
|
||||||
|
@inject TokenStorage TokenStorage
|
||||||
|
@inject PlayerStorage PlayerStorage
|
||||||
|
@inject AuthenticationStateProvider AuthProvider
|
||||||
|
@inject ISnackbar Snackbar
|
||||||
@inject HttpClient Http
|
@inject HttpClient Http
|
||||||
|
|
||||||
<MudPaper Class="pa-4" Elevation="0" Style="background-color: rgba(0,0,0,0.05); border-radius: 8px;">
|
<MudPaper Class="pa-4" Elevation="0" Style="background-color: rgba(0,0,0,0.05); border-radius: 8px;">
|
||||||
@@ -56,63 +60,35 @@
|
|||||||
private bool _isPlaying;
|
private bool _isPlaying;
|
||||||
private Timer? _progressTimer;
|
private Timer? _progressTimer;
|
||||||
private bool _isMuted;
|
private bool _isMuted;
|
||||||
|
private string? _currentAccessToken;
|
||||||
|
private string? _currentSharedPlaylistId;
|
||||||
|
|
||||||
[Inject] protected IJSRuntime JS { get; set; } = null!;
|
protected override async Task OnInitializedAsync()
|
||||||
[Inject] private TokenStorage TokenStorage { get; set; } = null!;
|
{
|
||||||
[Inject] private PlayerStorage PlayerStorage { get; set; } = null!;
|
AudioPlayerService.OnLoadAndPlayRequested += OnLoadAndPlay;
|
||||||
[Inject] private AuthenticationStateProvider AuthProvider { get; set; } = null!;
|
AudioPlayerService.OnPlayRequested += OnPlay;
|
||||||
[Inject] private ISnackbar Snackbar { get; set; } = null!;
|
AudioPlayerService.OnPauseRequested += OnPause;
|
||||||
|
AudioPlayerService.OnStopRequested += OnStop;
|
||||||
/// <summary>Требовать ли авторизацию для воспроизведения (по умолчанию true).</summary>
|
AudioPlayerService.OnSeekRequested += OnSeek;
|
||||||
[Parameter] public bool RequireAuth { get; set; } = true;
|
AudioPlayerService.OnVolumeChangeRequested += OnVolumeChange;
|
||||||
|
|
||||||
/// <summary>ID расшаренного плейлиста.</summary>
|
await LoadSavedVolume();
|
||||||
[Parameter] public string SharedPlaylistId { get; set; } = string.Empty;
|
await AudioPlayerService.SetVolumeAsync(_currentVolume); // синхронизация
|
||||||
|
}
|
||||||
/// <summary>Событие при завершении трека.</summary>
|
|
||||||
[Parameter] public EventCallback OnTrackEnded { get; set; }
|
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
if (firstRender)
|
if (firstRender)
|
||||||
{
|
{
|
||||||
await EnsureAudioModuleAsync();
|
await EnsureAudioModuleAsync();
|
||||||
await ChangeVolume(await PlayerStorage.GetVolumeAsync());
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[JSInvokable]
|
private async Task LoadSavedVolume()
|
||||||
public async Task OnAudioEnded()
|
|
||||||
{
|
{
|
||||||
_isPlaying = false;
|
var savedVolume = await PlayerStorage.GetVolumeAsync();
|
||||||
_currentProgress = 0;
|
_currentVolume = savedVolume;
|
||||||
StopProgressTimer();
|
await AudioPlayerService.SetVolumeAsync(savedVolume);
|
||||||
if (OnTrackEnded.HasDelegate)
|
|
||||||
await OnTrackEnded.InvokeAsync();
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
[JSInvokable]
|
|
||||||
public async Task OnTimeUpdate(double currentTime, double duration)
|
|
||||||
{
|
|
||||||
// Защита от некорректных значений
|
|
||||||
if (double.IsNaN(currentTime) || double.IsNaN(duration) || double.IsInfinity(currentTime) || double.IsInfinity(duration))
|
|
||||||
return;
|
|
||||||
if (duration <= 0) return;
|
|
||||||
|
|
||||||
_currentProgress = (currentTime / duration) * 100;
|
|
||||||
_currentTime = FormatTime(currentTime);
|
|
||||||
// Длительность не обновляем здесь
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
|
|
||||||
[JSInvokable]
|
|
||||||
public async Task OnDurationReady(double duration)
|
|
||||||
{
|
|
||||||
if (double.IsNaN(duration) || double.IsInfinity(duration) || duration <= 0) return;
|
|
||||||
_totalTime = FormatTime(duration);
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task EnsureAudioModuleAsync()
|
private async Task EnsureAudioModuleAsync()
|
||||||
@@ -123,14 +99,42 @@
|
|||||||
_audioElement = await _audioModule.InvokeAsync<IJSObjectReference>("init", _audioId, DotNetObjectReference.Create(this));
|
_audioElement = await _audioModule.InvokeAsync<IJSObjectReference>("init", _audioId, DotNetObjectReference.Create(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[JSInvokable]
|
||||||
|
public async Task OnAudioEnded()
|
||||||
|
{
|
||||||
|
AudioPlayerService.NotifyTrackEnded();
|
||||||
|
_isPlaying = false;
|
||||||
|
_currentProgress = 0;
|
||||||
|
StopProgressTimer();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
[JSInvokable]
|
||||||
|
public async Task OnTimeUpdate(double currentTime, double duration)
|
||||||
|
{
|
||||||
|
if (double.IsNaN(currentTime) || double.IsNaN(duration) || duration <= 0) return;
|
||||||
|
var progress = (currentTime / duration) * 100;
|
||||||
|
var currentTimeStr = FormatTime(currentTime);
|
||||||
|
var totalTimeStr = FormatTime(duration);
|
||||||
|
AudioPlayerService.UpdateProgress(progress, currentTimeStr, totalTimeStr);
|
||||||
|
_currentProgress = progress;
|
||||||
|
_currentTime = currentTimeStr;
|
||||||
|
_totalTime = totalTimeStr;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
[JSInvokable]
|
||||||
|
public async Task OnDurationReady(double duration)
|
||||||
|
{
|
||||||
|
if (duration <= 0) return;
|
||||||
|
_totalTime = FormatTime(duration);
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<bool> CheckAuthAsync()
|
private async Task<bool> CheckAuthAsync()
|
||||||
{
|
{
|
||||||
if (!RequireAuth) return true;
|
|
||||||
|
|
||||||
var authState = await AuthProvider.GetAuthenticationStateAsync();
|
var authState = await AuthProvider.GetAuthenticationStateAsync();
|
||||||
var isAuthenticated = authState.User.Identity?.IsAuthenticated == true;
|
if (!authState.User.Identity?.IsAuthenticated == true)
|
||||||
|
|
||||||
if (!isAuthenticated)
|
|
||||||
{
|
{
|
||||||
Snackbar.Add("Воспроизведение доступно только авторизованным пользователям", Severity.Warning);
|
Snackbar.Add("Воспроизведение доступно только авторизованным пользователям", Severity.Warning);
|
||||||
return false;
|
return false;
|
||||||
@@ -138,32 +142,30 @@
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LoadAndPlayAsync(string trackId)
|
private async Task OnLoadAndPlay(string trackId, string? accessToken, string? sharedPlaylistId)
|
||||||
{
|
{
|
||||||
if (!await CheckAuthAsync()) return;
|
if (!await CheckAuthAsync()) return;
|
||||||
|
|
||||||
var tokens = await TokenStorage.GetTokensAsync();
|
var tokens = await TokenStorage.GetTokensAsync();
|
||||||
var accessToken = tokens.token;
|
_currentAccessToken = accessToken ?? tokens.token;
|
||||||
|
_currentSharedPlaylistId = sharedPlaylistId;
|
||||||
if (string.IsNullOrWhiteSpace(accessToken) && string.IsNullOrWhiteSpace(SharedPlaylistId))
|
|
||||||
|
if (string.IsNullOrWhiteSpace(_currentAccessToken) && string.IsNullOrWhiteSpace(_currentSharedPlaylistId))
|
||||||
{
|
{
|
||||||
Snackbar.Add("Токен авторизации не найден. Пожалуйста, войдите заново.", Severity.Error);
|
Snackbar.Add("Токен авторизации не найден. Пожалуйста, войдите заново.", Severity.Error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var streamUrl = new Uri(Http.BaseAddress!, $"/api/audio/track/{trackId}").ToString();
|
var streamUrl = new Uri(Http.BaseAddress!, $"/api/audio/track/{trackId}").ToString();
|
||||||
|
|
||||||
await EnsureAudioModuleAsync();
|
await EnsureAudioModuleAsync();
|
||||||
await _audioElement!.InvokeVoidAsync("loadAndPlay", streamUrl, accessToken, SharedPlaylistId);
|
await _audioElement!.InvokeVoidAsync("loadAndPlay", streamUrl, _currentAccessToken, _currentSharedPlaylistId);
|
||||||
_isPlaying = true;
|
_isPlaying = true;
|
||||||
StartProgressTimer();
|
StartProgressTimer();
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task PlayAsync()
|
private async Task OnPlay()
|
||||||
{
|
{
|
||||||
if (!await CheckAuthAsync()) return;
|
|
||||||
|
|
||||||
if (_audioElement == null) return;
|
if (_audioElement == null) return;
|
||||||
await _audioElement.InvokeVoidAsync("play");
|
await _audioElement.InvokeVoidAsync("play");
|
||||||
_isPlaying = true;
|
_isPlaying = true;
|
||||||
@@ -171,7 +173,7 @@
|
|||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task PauseAsync()
|
private async Task OnPause()
|
||||||
{
|
{
|
||||||
if (_audioElement == null) return;
|
if (_audioElement == null) return;
|
||||||
await _audioElement.InvokeVoidAsync("pause");
|
await _audioElement.InvokeVoidAsync("pause");
|
||||||
@@ -180,7 +182,7 @@
|
|||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StopAsync()
|
private async Task OnStop()
|
||||||
{
|
{
|
||||||
if (_audioElement == null) return;
|
if (_audioElement == null) return;
|
||||||
await _audioElement.InvokeVoidAsync("stop");
|
await _audioElement.InvokeVoidAsync("stop");
|
||||||
@@ -190,20 +192,7 @@
|
|||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task TogglePlayPause()
|
private async Task OnSeek(double percent)
|
||||||
{
|
|
||||||
if (_isPlaying)
|
|
||||||
await PauseAsync();
|
|
||||||
else
|
|
||||||
await PlayAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task Stop()
|
|
||||||
{
|
|
||||||
await StopAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SeekTo(double value)
|
|
||||||
{
|
{
|
||||||
if (_audioElement == null) return;
|
if (_audioElement == null) return;
|
||||||
try
|
try
|
||||||
@@ -211,71 +200,87 @@
|
|||||||
var duration = await _audioElement.InvokeAsync<double>("getDuration");
|
var duration = await _audioElement.InvokeAsync<double>("getDuration");
|
||||||
if (duration > 0 && !double.IsNaN(duration))
|
if (duration > 0 && !double.IsNaN(duration))
|
||||||
{
|
{
|
||||||
var newTime = (value / 100) * duration;
|
var newTime = (percent / 100) * duration;
|
||||||
await _audioElement.InvokeVoidAsync("setCurrentTime", newTime);
|
await _audioElement.InvokeVoidAsync("setCurrentTime", newTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"SeekTo error: {ex.Message}");
|
Console.WriteLine($"Seek error: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ChangeVolume(double value)
|
private async Task OnVolumeChange(double volume)
|
||||||
{
|
{
|
||||||
if (_audioElement == null) return;
|
if (_audioElement == null) return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var volume = value / 100;
|
await _audioElement.InvokeVoidAsync("setVolume", volume / 100);
|
||||||
await _audioElement.InvokeVoidAsync("setVolume", volume);
|
|
||||||
_isMuted = false;
|
_isMuted = false;
|
||||||
_currentVolume = value;
|
_currentVolume = volume;
|
||||||
await PlayerStorage.SetVolumeAsync(value);
|
await PlayerStorage.SetVolumeAsync(volume);
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"ChangeVolume error: {ex.Message}");
|
Console.WriteLine($"Volume change error: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task TogglePlayPause()
|
||||||
|
{
|
||||||
|
if (_isPlaying)
|
||||||
|
await AudioPlayerService.PauseAsync();
|
||||||
|
else
|
||||||
|
await AudioPlayerService.PlayAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task Stop()
|
||||||
|
{
|
||||||
|
await AudioPlayerService.StopAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SeekTo(double value)
|
||||||
|
{
|
||||||
|
await AudioPlayerService.SeekToAsync(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ChangeVolume(double value)
|
||||||
|
{
|
||||||
|
await AudioPlayerService.SetVolumeAsync(value);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task ToggleMute()
|
private async Task ToggleMute()
|
||||||
{
|
{
|
||||||
if (_audioElement == null) return;
|
|
||||||
_isMuted = !_isMuted;
|
_isMuted = !_isMuted;
|
||||||
var newVolume = _isMuted ? 0 : (_currentVolume / 100);
|
var newVolume = _isMuted ? 0 : _currentVolume;
|
||||||
await _audioElement.InvokeVoidAsync("setVolume", newVolume);
|
await AudioPlayerService.SetVolumeAsync(newVolume);
|
||||||
StateHasChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void StartProgressTimer()
|
private void StartProgressTimer()
|
||||||
{
|
{
|
||||||
StopProgressTimer();
|
StopProgressTimer();
|
||||||
_progressTimer = new Timer(async _ =>
|
_progressTimer = new Timer(async _ => await UpdateProgress(), null, 0, 500);
|
||||||
{
|
|
||||||
await UpdateProgress();
|
|
||||||
}, null, 0, 500);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void StopProgressTimer() => _progressTimer?.Dispose();
|
private void StopProgressTimer() => _progressTimer?.Dispose();
|
||||||
|
|
||||||
private async Task UpdateProgress()
|
private async Task UpdateProgress()
|
||||||
{
|
{
|
||||||
if (_audioElement == null)
|
if (_audioElement == null) return;
|
||||||
{
|
|
||||||
Console.WriteLine("UpdateProgress: _audioElement is null");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var current = await _audioElement.InvokeAsync<double>("getCurrentTime");
|
var current = await _audioElement.InvokeAsync<double>("getCurrentTime");
|
||||||
var duration = await _audioElement.InvokeAsync<double>("getDuration");
|
var duration = await _audioElement.InvokeAsync<double>("getDuration");
|
||||||
|
|
||||||
if (duration > 0 && !double.IsNaN(duration) && !double.IsNaN(current))
|
if (duration > 0 && !double.IsNaN(duration) && !double.IsNaN(current))
|
||||||
{
|
{
|
||||||
_currentProgress = (current / duration) * 100;
|
var progress = (current / duration) * 100;
|
||||||
_currentTime = FormatTime(current);
|
var currentTime = FormatTime(current);
|
||||||
_totalTime = FormatTime(duration);
|
var totalTime = FormatTime(duration);
|
||||||
|
AudioPlayerService.UpdateProgress(progress, currentTime, totalTime);
|
||||||
|
_currentProgress = progress;
|
||||||
|
_currentTime = currentTime;
|
||||||
|
_totalTime = totalTime;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,9 +37,7 @@
|
|||||||
<div style="width: 40px; height: 40px; flex-shrink: 0;">
|
<div style="width: 40px; height: 40px; flex-shrink: 0;">
|
||||||
<TrackCoverWithPlay CoverUrl="@track.CoverUri"
|
<TrackCoverWithPlay CoverUrl="@track.CoverUri"
|
||||||
TrackId="@track.TrackId"
|
TrackId="@track.TrackId"
|
||||||
Width="40" Height="40"
|
Width="40" Height="40"/>
|
||||||
IsPlaying="@(_currentPlayingTrackId == track.TrackId && _isPlaying)"
|
|
||||||
OnPlay="PlayTrack" />
|
|
||||||
</div>
|
</div>
|
||||||
<div style="flex: 1; min-width: 0;">
|
<div style="flex: 1; min-width: 0;">
|
||||||
<MudText Typo="Typo.body1" Style="font-weight: 500;">@track.Title</MudText>
|
<MudText Typo="Typo.body1" Style="font-weight: 500;">@track.Title</MudText>
|
||||||
@@ -67,23 +65,12 @@
|
|||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter] public EventCallback<string> OnAddTrack { get; set; }
|
[Parameter] public EventCallback<string> OnAddTrack { get; set; }
|
||||||
[Parameter] public EventCallback<string> OnPlayTrack { get; set; }
|
|
||||||
[Parameter] public string ShareToken { get; set; } = string.Empty;
|
[Parameter] public string ShareToken { get; set; } = string.Empty;
|
||||||
[Parameter] public string? CurrentPlayingTrackId { get; set; }
|
|
||||||
[Parameter] public bool IsPlaying { get; set; }
|
|
||||||
|
|
||||||
private string _searchQuery = "";
|
private string _searchQuery = "";
|
||||||
private List<YandexTrackSearchResult> _searchResults = new();
|
private List<YandexTrackSearchResult> _searchResults = new();
|
||||||
private bool _isSearching;
|
private bool _isSearching;
|
||||||
private HashSet<string> _addingTrackIds = new();
|
private HashSet<string> _addingTrackIds = new();
|
||||||
private string? _currentPlayingTrackId;
|
|
||||||
private bool _isPlaying;
|
|
||||||
|
|
||||||
protected override void OnParametersSet()
|
|
||||||
{
|
|
||||||
_currentPlayingTrackId = CurrentPlayingTrackId;
|
|
||||||
_isPlaying = IsPlaying;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SearchTracks()
|
private async Task SearchTracks()
|
||||||
{
|
{
|
||||||
@@ -133,11 +120,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task PlayTrack(string trackId)
|
|
||||||
{
|
|
||||||
await OnPlayTrack.InvokeAsync(trackId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string FormatDuration(long ms)
|
private string FormatDuration(long ms)
|
||||||
{
|
{
|
||||||
var seconds = ms / 1000;
|
var seconds = ms / 1000;
|
||||||
|
|||||||
@@ -31,9 +31,6 @@
|
|||||||
</MudTabPanel>
|
</MudTabPanel>
|
||||||
<MudTabPanel Text="Поиск" Style="padding: 16px;">
|
<MudTabPanel Text="Поиск" Style="padding: 16px;">
|
||||||
<AddTrackBySearch OnAddTrack="AddTrackById"
|
<AddTrackBySearch OnAddTrack="AddTrackById"
|
||||||
OnPlayTrack="PlayTrack"
|
|
||||||
CurrentPlayingTrackId="CurrentPlayingTrackId"
|
|
||||||
IsPlaying="IsPlaying"
|
|
||||||
ShareToken="@ShareToken" />
|
ShareToken="@ShareToken" />
|
||||||
</MudTabPanel>
|
</MudTabPanel>
|
||||||
</MudTabs>
|
</MudTabs>
|
||||||
@@ -46,9 +43,6 @@
|
|||||||
|
|
||||||
[Parameter] public string ShareToken { get; set; } = string.Empty;
|
[Parameter] public string ShareToken { get; set; } = string.Empty;
|
||||||
[Parameter] public EventCallback OnTrackAdded { get; set; }
|
[Parameter] public EventCallback OnTrackAdded { get; set; }
|
||||||
[Parameter] public EventCallback<string> OnPlayTrack { get; set; }
|
|
||||||
[Parameter] public string? CurrentPlayingTrackId { get; set; }
|
|
||||||
[Parameter] public bool IsPlaying { get; set; }
|
|
||||||
|
|
||||||
private async Task AddTrackByLink()
|
private async Task AddTrackByLink()
|
||||||
{
|
{
|
||||||
@@ -99,11 +93,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task PlayTrack(string trackId)
|
|
||||||
{
|
|
||||||
OnPlayTrack.InvokeAsync(trackId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string? ExtractTrackIdFromLink(string link)
|
private string? ExtractTrackIdFromLink(string link)
|
||||||
{
|
{
|
||||||
var match = System.Text.RegularExpressions.Regex.Match(link, @"/track/(\d+)");
|
var match = System.Text.RegularExpressions.Regex.Match(link, @"/track/(\d+)");
|
||||||
|
|||||||
@@ -26,9 +26,7 @@
|
|||||||
{
|
{
|
||||||
<TrackCoverWithPlay CoverUrl="@context.CoverUri"
|
<TrackCoverWithPlay CoverUrl="@context.CoverUri"
|
||||||
TrackId="@context.Id"
|
TrackId="@context.Id"
|
||||||
Width="50" Height="50"
|
Width="50" Height="50"/>
|
||||||
IsPlaying="@(CurrentPlayingTrackId == context.Id && IsPlaying)"
|
|
||||||
OnPlay="PlayTrack" />
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
@using PlaylistShared.Pwa.Components.Global
|
||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
<MudThemeProvider Theme="@_theme" IsDarkMode="_isDarkMode" />
|
<MudThemeProvider Theme="@_theme" IsDarkMode="_isDarkMode" />
|
||||||
@@ -24,6 +25,10 @@
|
|||||||
<MudMainContent Class="pt-16 pa-4">
|
<MudMainContent Class="pt-16 pa-4">
|
||||||
@Body
|
@Body
|
||||||
</MudMainContent>
|
</MudMainContent>
|
||||||
|
|
||||||
|
<div class="fixed-player" style="position: fixed; bottom: 0; left: 0; right: 0; z-index: 1000;">
|
||||||
|
<AudioPlayer />
|
||||||
|
</div>
|
||||||
</MudLayout>
|
</MudLayout>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|||||||
@@ -34,9 +34,7 @@
|
|||||||
{
|
{
|
||||||
<AddTrackSection ShareToken="@Token"
|
<AddTrackSection ShareToken="@Token"
|
||||||
OnTrackAdded="LoadTracks"
|
OnTrackAdded="LoadTracks"
|
||||||
OnPlayTrack="PlayTrack"
|
/>
|
||||||
CurrentPlayingTrackId="_currentTrackId"
|
|
||||||
IsPlaying="_isPlaying" />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Список треков -->
|
<!-- Список треков -->
|
||||||
@@ -50,16 +48,10 @@
|
|||||||
CanPlay="@_canPlay"
|
CanPlay="@_canPlay"
|
||||||
CanRemove="@_canRemove"
|
CanRemove="@_canRemove"
|
||||||
CurrentPlayingTrackId="_currentTrackId"
|
CurrentPlayingTrackId="_currentTrackId"
|
||||||
IsPlaying="_isPlaying"
|
/>
|
||||||
OnPlayTrack="PlayTrack" />
|
|
||||||
</MudCardContent>
|
</MudCardContent>
|
||||||
</MudCard>
|
</MudCard>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Фиксированный плеер внизу -->
|
|
||||||
<div class="fixed-player" style="display: @(_isPlayerVisible ? "block" : "none");">
|
|
||||||
<AudioPlayer @ref="_audioPlayer" OnTrackEnded="OnTrackEnded" RequireAuth="false" SharedPlaylistId="@Token" />
|
|
||||||
</div>
|
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
|
|
||||||
|
|
||||||
@@ -68,11 +60,7 @@
|
|||||||
|
|
||||||
private int _addTrackTabIndex = 0; // 0 - ссылка, 1 - поиск
|
private int _addTrackTabIndex = 0; // 0 - ссылка, 1 - поиск
|
||||||
|
|
||||||
private AudioPlayer? _audioPlayer;
|
|
||||||
private TracksTable? _tracksTableRef;
|
private TracksTable? _tracksTableRef;
|
||||||
private string? _currentTrackId { get; set; }
|
|
||||||
private bool _isPlaying = false;
|
|
||||||
private bool _isPlayerVisible = false;
|
|
||||||
|
|
||||||
private SharedPlaylistDto? _playlist;
|
private SharedPlaylistDto? _playlist;
|
||||||
private bool _loading = true;
|
private bool _loading = true;
|
||||||
@@ -180,38 +168,4 @@
|
|||||||
_tracksLoading = false;
|
_tracksLoading = false;
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task PlayTrack(string trackId)
|
|
||||||
{
|
|
||||||
if (_audioPlayer == null) return;
|
|
||||||
|
|
||||||
if (_currentTrackId == trackId && _isPlaying)
|
|
||||||
{
|
|
||||||
await _audioPlayer.PauseAsync();
|
|
||||||
_isPlaying = false;
|
|
||||||
}
|
|
||||||
else if (_currentTrackId == trackId && !_isPlaying)
|
|
||||||
{
|
|
||||||
await _audioPlayer.PlayAsync();
|
|
||||||
_isPlaying = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(_currentTrackId) && _isPlaying)
|
|
||||||
await _audioPlayer.StopAsync();
|
|
||||||
|
|
||||||
_currentTrackId = trackId;
|
|
||||||
await _audioPlayer.LoadAndPlayAsync(trackId);
|
|
||||||
_isPlaying = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_isPlayerVisible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnTrackEnded()
|
|
||||||
{
|
|
||||||
_currentTrackId = null;
|
|
||||||
_isPlaying = false;
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -25,6 +25,7 @@ internal class Program
|
|||||||
builder.Services.AddScoped<AuthStateProvider>();
|
builder.Services.AddScoped<AuthStateProvider>();
|
||||||
builder.Services.AddScoped<AuthenticationStateProvider>(sp => sp.GetRequiredService<AuthStateProvider>());
|
builder.Services.AddScoped<AuthenticationStateProvider>(sp => sp.GetRequiredService<AuthStateProvider>());
|
||||||
builder.Services.AddScoped<ApiClient>();
|
builder.Services.AddScoped<ApiClient>();
|
||||||
|
builder.Services.AddScoped<IAudioPlayerService, AudioPlayerService>();
|
||||||
|
|
||||||
builder.Services.AddAuthorizationCore();
|
builder.Services.AddAuthorizationCore();
|
||||||
builder.Services.AddCascadingAuthenticationState();
|
builder.Services.AddCascadingAuthenticationState();
|
||||||
|
|||||||
136
PlaylistShared.Pwa/Services/AudioPlayerService.cs
Normal file
136
PlaylistShared.Pwa/Services/AudioPlayerService.cs
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
using MudBlazor;
|
||||||
|
|
||||||
|
namespace PlaylistShared.Pwa.Services;
|
||||||
|
|
||||||
|
public class AudioPlayerService : IAudioPlayerService
|
||||||
|
{
|
||||||
|
private readonly TokenStorage _tokenStorage;
|
||||||
|
private readonly ISnackbar _snackbar;
|
||||||
|
|
||||||
|
private string? _currentTrackId;
|
||||||
|
private bool _isPlaying;
|
||||||
|
private double _currentVolume = 70;
|
||||||
|
private double _currentProgress;
|
||||||
|
private string _currentTime = "0:00";
|
||||||
|
private string _totalTime = "0:00";
|
||||||
|
|
||||||
|
public string? CurrentTrackId => _currentTrackId;
|
||||||
|
public bool IsPlaying => _isPlaying;
|
||||||
|
public double CurrentVolume
|
||||||
|
{
|
||||||
|
get => _currentVolume;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_currentVolume = value;
|
||||||
|
OnStateChanged?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public double CurrentProgress => _currentProgress;
|
||||||
|
public string CurrentTime => _currentTime;
|
||||||
|
public string TotalTime => _totalTime;
|
||||||
|
|
||||||
|
public event Action? OnStateChanged;
|
||||||
|
|
||||||
|
public AudioPlayerService(TokenStorage tokenStorage, ISnackbar snackbar)
|
||||||
|
{
|
||||||
|
_tokenStorage = tokenStorage;
|
||||||
|
_snackbar = snackbar;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Внешние команды (вызываются из компонентов)
|
||||||
|
public async Task LoadAndPlayAsync(string trackId, string? accessToken = null, string? sharedPlaylistId = null)
|
||||||
|
{
|
||||||
|
// Если accessToken не передан, пытаемся получить его из хранилища
|
||||||
|
if (string.IsNullOrWhiteSpace(accessToken))
|
||||||
|
{
|
||||||
|
var tokens = await _tokenStorage.GetTokensAsync();
|
||||||
|
accessToken = tokens.token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, есть ли чем авторизоваться
|
||||||
|
if (string.IsNullOrWhiteSpace(accessToken) && string.IsNullOrWhiteSpace(sharedPlaylistId))
|
||||||
|
{
|
||||||
|
_snackbar.Add("Не удалось воспроизвести трек: отсутствует токен авторизации или идентификатор расшаренного плейлиста.", Severity.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentTrackId = trackId;
|
||||||
|
_isPlaying = true;
|
||||||
|
OnStateChanged?.Invoke();
|
||||||
|
OnLoadAndPlayRequested?.Invoke(trackId, accessToken, sharedPlaylistId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PlayAsync()
|
||||||
|
{
|
||||||
|
_isPlaying = true;
|
||||||
|
OnStateChanged?.Invoke();
|
||||||
|
OnPlayRequested?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PauseAsync()
|
||||||
|
{
|
||||||
|
_isPlaying = false;
|
||||||
|
OnStateChanged?.Invoke();
|
||||||
|
OnPauseRequested?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task StopAsync()
|
||||||
|
{
|
||||||
|
_isPlaying = false;
|
||||||
|
_currentTrackId = null;
|
||||||
|
_currentProgress = 0;
|
||||||
|
_currentTime = "0:00";
|
||||||
|
OnStateChanged?.Invoke();
|
||||||
|
OnStopRequested?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SeekToAsync(double percent)
|
||||||
|
{
|
||||||
|
OnSeekRequested?.Invoke(percent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetVolumeAsync(double volume)
|
||||||
|
{
|
||||||
|
_currentVolume = volume;
|
||||||
|
OnStateChanged?.Invoke();
|
||||||
|
OnVolumeChangeRequested?.Invoke(volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
// События для связи с реальным AudioPlayer компонентом
|
||||||
|
public event Func<string, string?, string?, Task>? OnLoadAndPlayRequested;
|
||||||
|
public event Func<Task>? OnPlayRequested;
|
||||||
|
public event Func<Task>? OnPauseRequested;
|
||||||
|
public event Func<Task>? OnStopRequested;
|
||||||
|
public event Func<double, Task>? OnSeekRequested;
|
||||||
|
public event Func<double, Task>? OnVolumeChangeRequested;
|
||||||
|
|
||||||
|
// Внутренние методы для обновления состояния из AudioPlayer
|
||||||
|
public void SetPlayingState(bool isPlaying)
|
||||||
|
{
|
||||||
|
_isPlaying = isPlaying;
|
||||||
|
OnStateChanged?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetCurrentTrack(string? trackId)
|
||||||
|
{
|
||||||
|
_currentTrackId = trackId;
|
||||||
|
OnStateChanged?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateProgress(double progress, string currentTime, string totalTime)
|
||||||
|
{
|
||||||
|
_currentProgress = progress;
|
||||||
|
_currentTime = currentTime;
|
||||||
|
_totalTime = totalTime;
|
||||||
|
OnStateChanged?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void NotifyTrackEnded()
|
||||||
|
{
|
||||||
|
_isPlaying = false;
|
||||||
|
_currentTrackId = null;
|
||||||
|
_currentProgress = 0;
|
||||||
|
_currentTime = "0:00";
|
||||||
|
OnStateChanged?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
97
PlaylistShared.Pwa/Services/IAudioPlayerService.cs
Normal file
97
PlaylistShared.Pwa/Services/IAudioPlayerService.cs
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
namespace PlaylistShared.Pwa.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Глобальный сервис управления аудиоплеером.
|
||||||
|
/// Позволяет управлять воспроизведением из любого компонента.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAudioPlayerService
|
||||||
|
{
|
||||||
|
// ---------- Состояние плеера (для чтения) ----------
|
||||||
|
|
||||||
|
/// <summary>ID текущего воспроизводимого трека (null, если ничего не играет).</summary>
|
||||||
|
string? CurrentTrackId { get; }
|
||||||
|
|
||||||
|
/// <summary>Играет ли в данный момент (true) или приостановлен (false).</summary>
|
||||||
|
bool IsPlaying { get; }
|
||||||
|
|
||||||
|
/// <summary>Текущая громкость (0–100).</summary>
|
||||||
|
double CurrentVolume { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Прогресс воспроизведения в процентах (0–100).</summary>
|
||||||
|
double CurrentProgress { get; }
|
||||||
|
|
||||||
|
/// <summary>Отформатированное текущее время (мм:сс).</summary>
|
||||||
|
string CurrentTime { get; }
|
||||||
|
|
||||||
|
/// <summary>Отформатированная общая длительность (мм:сс).</summary>
|
||||||
|
string TotalTime { get; }
|
||||||
|
|
||||||
|
// ---------- Команды управления (вызываются из компонентов) ----------
|
||||||
|
|
||||||
|
/// <summary>Загрузить и начать воспроизведение трека.</summary>
|
||||||
|
/// <param name="trackId">ID трека.</param>
|
||||||
|
/// <param name="accessToken">Опциональный access-токен (если не указан, будет взят из хранилища).</param>
|
||||||
|
/// <param name="sharedPlaylistId">ID расшаренного плейлиста (для неавторизованного доступа).</param>
|
||||||
|
Task LoadAndPlayAsync(string trackId, string? accessToken = null, string? sharedPlaylistId = null);
|
||||||
|
|
||||||
|
/// <summary>Воспроизвести (если трек загружен и на паузе).</summary>
|
||||||
|
Task PlayAsync();
|
||||||
|
|
||||||
|
/// <summary>Поставить на паузу.</summary>
|
||||||
|
Task PauseAsync();
|
||||||
|
|
||||||
|
/// <summary>Остановить воспроизведение и выгрузить трек.</summary>
|
||||||
|
Task StopAsync();
|
||||||
|
|
||||||
|
/// <summary>Перемотать на указанный процент (0–100).</summary>
|
||||||
|
Task SeekToAsync(double percent);
|
||||||
|
|
||||||
|
/// <summary>Установить громкость (0–100).</summary>
|
||||||
|
Task SetVolumeAsync(double volume);
|
||||||
|
|
||||||
|
// ---------- События для подписки на изменения состояния ----------
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Событие, возникающее при любом изменении состояния плеера:
|
||||||
|
/// смена трека, старт/пауза/стоп, обновление прогресса, изменение громкости, окончание трека.
|
||||||
|
/// Подписывайтесь на него, чтобы перерисовывать UI (например, иконку "пауза/плей").
|
||||||
|
/// </summary>
|
||||||
|
event Action? OnStateChanged;
|
||||||
|
|
||||||
|
// ---------- События для связи с реальным компонентом AudioPlayer ----------
|
||||||
|
// (Эти события вызываются сервисом, а компонент AudioPlayer на них подписывается,
|
||||||
|
// чтобы выполнить фактические операции с HTML5 Audio.)
|
||||||
|
|
||||||
|
/// <summary>Запрос на загрузку и воспроизведение трека.</summary>
|
||||||
|
event Func<string, string?, string?, Task>? OnLoadAndPlayRequested;
|
||||||
|
|
||||||
|
/// <summary>Запрос на воспроизведение (снять с паузы).</summary>
|
||||||
|
event Func<Task>? OnPlayRequested;
|
||||||
|
|
||||||
|
/// <summary>Запрос на паузу.</summary>
|
||||||
|
event Func<Task>? OnPauseRequested;
|
||||||
|
|
||||||
|
/// <summary>Запрос на остановку и выгрузку трека.</summary>
|
||||||
|
event Func<Task>? OnStopRequested;
|
||||||
|
|
||||||
|
/// <summary>Запрос на перемотку (процент 0–100).</summary>
|
||||||
|
event Func<double, Task>? OnSeekRequested;
|
||||||
|
|
||||||
|
/// <summary>Запрос на изменение громкости (0–100).</summary>
|
||||||
|
event Func<double, Task>? OnVolumeChangeRequested;
|
||||||
|
|
||||||
|
// ---------- Методы для обновления состояния из AudioPlayer ----------
|
||||||
|
// (Вызываются компонентом AudioPlayer, когда реальный аудиоэлемент меняет своё состояние.)
|
||||||
|
|
||||||
|
/// <summary>Уведомить сервис о том, что трек начал или прекратил играть.</summary>
|
||||||
|
void SetPlayingState(bool isPlaying);
|
||||||
|
|
||||||
|
/// <summary>Установить ID текущего трека.</summary>
|
||||||
|
void SetCurrentTrack(string? trackId);
|
||||||
|
|
||||||
|
/// <summary>Обновить прогресс и отображаемое время.</summary>
|
||||||
|
void UpdateProgress(double progress, string currentTime, string totalTime);
|
||||||
|
|
||||||
|
/// <summary>Уведомить об окончании трека.</summary>
|
||||||
|
void NotifyTrackEnded();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user