Вынесен глобальный плеер

This commit is contained in:
FrigaT
2026-04-14 18:59:00 +03:00
parent 65efb9ff76
commit e0fca7e55e
10 changed files with 379 additions and 191 deletions

View File

@@ -1,4 +1,5 @@
@using Microsoft.AspNetCore.Components.Web
@inject IAudioPlayerService AudioPlayerService
<div class="track-cover-container"
@onmouseenter="HandleMouseEnter"
@@ -7,7 +8,7 @@
<MudImage Src="@FormatCoverUrl(CoverUrl)" Height="@Height" Width="@Width" Class="rounded" Style="display: block;" />
@if (_isHovered || IsPlaying)
@if (_isHovered || IsCurrentTrackPlaying)
{
<div class="play-overlay"
style="position: absolute; top: 0; left: 0; right: 0; bottom: 0;
@@ -16,7 +17,7 @@
align-items: center;
justify-content: center;
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"
Size="Size.Large"
OnClick="OnPlayClick" />
@@ -27,19 +28,39 @@
@code {
[Parameter] public string CoverUrl { 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 Width { get; set; } = 50;
[Parameter] public string SharedPlaylistId { get; set; } = string.Empty;
private bool IsCurrentTrackPlaying => AudioPlayerService.IsPlaying && AudioPlayerService.CurrentTrackId == TrackId;
private bool _isHovered;
private void HandleMouseEnter() => _isHovered = true;
private void HandleMouseLeave() => _isHovered = false;
protected override void OnInitialized()
{
AudioPlayerService.OnStateChanged += OnPlayerStateChanged;
}
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)

View File

@@ -1,6 +1,10 @@
@using Microsoft.JSInterop
@using Microsoft.AspNetCore.Components.Authorization
@namespace PlaylistShared.Pwa.Components
@inject IAudioPlayerService AudioPlayerService
@inject IJSRuntime JS
@inject TokenStorage TokenStorage
@inject PlayerStorage PlayerStorage
@inject AuthenticationStateProvider AuthProvider
@inject ISnackbar Snackbar
@inject HttpClient Http
<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 Timer? _progressTimer;
private bool _isMuted;
private string? _currentAccessToken;
private string? _currentSharedPlaylistId;
[Inject] protected IJSRuntime JS { get; set; } = null!;
[Inject] private TokenStorage TokenStorage { get; set; } = null!;
[Inject] private PlayerStorage PlayerStorage { get; set; } = null!;
[Inject] private AuthenticationStateProvider AuthProvider { get; set; } = null!;
[Inject] private ISnackbar Snackbar { get; set; } = null!;
/// <summary>Требовать ли авторизацию для воспроизведения (по умолчанию true).</summary>
[Parameter] public bool RequireAuth { get; set; } = true;
/// <summary>ID расшаренного плейлиста.</summary>
[Parameter] public string SharedPlaylistId { get; set; } = string.Empty;
/// <summary>Событие при завершении трека.</summary>
[Parameter] public EventCallback OnTrackEnded { get; set; }
protected override async Task OnInitializedAsync()
{
AudioPlayerService.OnLoadAndPlayRequested += OnLoadAndPlay;
AudioPlayerService.OnPlayRequested += OnPlay;
AudioPlayerService.OnPauseRequested += OnPause;
AudioPlayerService.OnStopRequested += OnStop;
AudioPlayerService.OnSeekRequested += OnSeek;
AudioPlayerService.OnVolumeChangeRequested += OnVolumeChange;
await LoadSavedVolume();
await AudioPlayerService.SetVolumeAsync(_currentVolume); // синхронизация
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await EnsureAudioModuleAsync();
await ChangeVolume(await PlayerStorage.GetVolumeAsync());
StateHasChanged();
}
}
[JSInvokable]
public async Task OnAudioEnded()
private async Task LoadSavedVolume()
{
_isPlaying = false;
_currentProgress = 0;
StopProgressTimer();
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);
var savedVolume = await PlayerStorage.GetVolumeAsync();
_currentVolume = savedVolume;
await AudioPlayerService.SetVolumeAsync(savedVolume);
}
private async Task EnsureAudioModuleAsync()
@@ -123,14 +99,42 @@
_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()
{
if (!RequireAuth) return true;
var authState = await AuthProvider.GetAuthenticationStateAsync();
var isAuthenticated = authState.User.Identity?.IsAuthenticated == true;
if (!isAuthenticated)
if (!authState.User.Identity?.IsAuthenticated == true)
{
Snackbar.Add("Воспроизведение доступно только авторизованным пользователям", Severity.Warning);
return false;
@@ -138,32 +142,30 @@
return true;
}
public async Task LoadAndPlayAsync(string trackId)
private async Task OnLoadAndPlay(string trackId, string? accessToken, string? sharedPlaylistId)
{
if (!await CheckAuthAsync()) return;
var tokens = await TokenStorage.GetTokensAsync();
var accessToken = tokens.token;
if (string.IsNullOrWhiteSpace(accessToken) && string.IsNullOrWhiteSpace(SharedPlaylistId))
_currentAccessToken = accessToken ?? tokens.token;
_currentSharedPlaylistId = sharedPlaylistId;
if (string.IsNullOrWhiteSpace(_currentAccessToken) && string.IsNullOrWhiteSpace(_currentSharedPlaylistId))
{
Snackbar.Add("Токен авторизации не найден. Пожалуйста, войдите заново.", Severity.Error);
return;
}
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, _currentAccessToken, _currentSharedPlaylistId);
_isPlaying = true;
StartProgressTimer();
StateHasChanged();
}
public async Task PlayAsync()
private async Task OnPlay()
{
if (!await CheckAuthAsync()) return;
if (_audioElement == null) return;
await _audioElement.InvokeVoidAsync("play");
_isPlaying = true;
@@ -171,7 +173,7 @@
StateHasChanged();
}
public async Task PauseAsync()
private async Task OnPause()
{
if (_audioElement == null) return;
await _audioElement.InvokeVoidAsync("pause");
@@ -180,7 +182,7 @@
StateHasChanged();
}
public async Task StopAsync()
private async Task OnStop()
{
if (_audioElement == null) return;
await _audioElement.InvokeVoidAsync("stop");
@@ -190,20 +192,7 @@
StateHasChanged();
}
private async Task TogglePlayPause()
{
if (_isPlaying)
await PauseAsync();
else
await PlayAsync();
}
private async Task Stop()
{
await StopAsync();
}
private async Task SeekTo(double value)
private async Task OnSeek(double percent)
{
if (_audioElement == null) return;
try
@@ -211,71 +200,87 @@
var duration = await _audioElement.InvokeAsync<double>("getDuration");
if (duration > 0 && !double.IsNaN(duration))
{
var newTime = (value / 100) * duration;
var newTime = (percent / 100) * duration;
await _audioElement.InvokeVoidAsync("setCurrentTime", newTime);
}
}
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;
try
{
var volume = value / 100;
await _audioElement.InvokeVoidAsync("setVolume", volume);
await _audioElement.InvokeVoidAsync("setVolume", volume / 100);
_isMuted = false;
_currentVolume = value;
await PlayerStorage.SetVolumeAsync(value);
_currentVolume = volume;
await PlayerStorage.SetVolumeAsync(volume);
StateHasChanged();
}
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()
{
if (_audioElement == null) return;
_isMuted = !_isMuted;
var newVolume = _isMuted ? 0 : (_currentVolume / 100);
await _audioElement.InvokeVoidAsync("setVolume", newVolume);
StateHasChanged();
var newVolume = _isMuted ? 0 : _currentVolume;
await AudioPlayerService.SetVolumeAsync(newVolume);
}
private void StartProgressTimer()
{
StopProgressTimer();
_progressTimer = new Timer(async _ =>
{
await UpdateProgress();
}, null, 0, 500);
_progressTimer = new Timer(async _ => await UpdateProgress(), null, 0, 500);
}
private void StopProgressTimer() => _progressTimer?.Dispose();
private async Task UpdateProgress()
{
if (_audioElement == null)
{
Console.WriteLine("UpdateProgress: _audioElement is null");
return;
}
if (_audioElement == null) return;
try
{
var current = await _audioElement.InvokeAsync<double>("getCurrentTime");
var duration = await _audioElement.InvokeAsync<double>("getDuration");
if (duration > 0 && !double.IsNaN(duration) && !double.IsNaN(current))
{
_currentProgress = (current / duration) * 100;
_currentTime = FormatTime(current);
_totalTime = FormatTime(duration);
var progress = (current / duration) * 100;
var currentTime = FormatTime(current);
var totalTime = FormatTime(duration);
AudioPlayerService.UpdateProgress(progress, currentTime, totalTime);
_currentProgress = progress;
_currentTime = currentTime;
_totalTime = totalTime;
await InvokeAsync(StateHasChanged);
}
}

View File

@@ -37,9 +37,7 @@
<div style="width: 40px; height: 40px; flex-shrink: 0;">
<TrackCoverWithPlay CoverUrl="@track.CoverUri"
TrackId="@track.TrackId"
Width="40" Height="40"
IsPlaying="@(_currentPlayingTrackId == track.TrackId && _isPlaying)"
OnPlay="PlayTrack" />
Width="40" Height="40"/>
</div>
<div style="flex: 1; min-width: 0;">
<MudText Typo="Typo.body1" Style="font-weight: 500;">@track.Title</MudText>
@@ -67,23 +65,12 @@
@code {
[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? CurrentPlayingTrackId { get; set; }
[Parameter] public bool IsPlaying { get; set; }
private string _searchQuery = "";
private List<YandexTrackSearchResult> _searchResults = new();
private bool _isSearching;
private HashSet<string> _addingTrackIds = new();
private string? _currentPlayingTrackId;
private bool _isPlaying;
protected override void OnParametersSet()
{
_currentPlayingTrackId = CurrentPlayingTrackId;
_isPlaying = IsPlaying;
}
private async Task SearchTracks()
{
@@ -133,11 +120,6 @@
}
}
private async Task PlayTrack(string trackId)
{
await OnPlayTrack.InvokeAsync(trackId);
}
private string FormatDuration(long ms)
{
var seconds = ms / 1000;

View File

@@ -31,9 +31,6 @@
</MudTabPanel>
<MudTabPanel Text="Поиск" Style="padding: 16px;">
<AddTrackBySearch OnAddTrack="AddTrackById"
OnPlayTrack="PlayTrack"
CurrentPlayingTrackId="CurrentPlayingTrackId"
IsPlaying="IsPlaying"
ShareToken="@ShareToken" />
</MudTabPanel>
</MudTabs>
@@ -46,9 +43,6 @@
[Parameter] public string ShareToken { get; set; } = string.Empty;
[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()
{
@@ -99,11 +93,6 @@
}
}
private async Task PlayTrack(string trackId)
{
OnPlayTrack.InvokeAsync(trackId);
}
private string? ExtractTrackIdFromLink(string link)
{
var match = System.Text.RegularExpressions.Regex.Match(link, @"/track/(\d+)");

View File

@@ -26,9 +26,7 @@
{
<TrackCoverWithPlay CoverUrl="@context.CoverUri"
TrackId="@context.Id"
Width="50" Height="50"
IsPlaying="@(CurrentPlayingTrackId == context.Id && IsPlaying)"
OnPlay="PlayTrack" />
Width="50" Height="50"/>
}
else
{