Files
PlaylistShared/PlaylistShared.Pwa/Components/Global/AudioPlayer.razor
2026-04-15 09:52:56 +03:00

318 lines
11 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@using Microsoft.JSInterop
@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" Width="100%" Style="background-color: rgba(0,0,0,0.05); border-radius: 8px;">
<MudStack Row Gap="2" AlignItems="AlignItems.Center" Wrap="Wrap.Wrap">
<!-- Информация о треке -->
<MudStack Row Gap="2" AlignItems="AlignItems.Center">
@if (!string.IsNullOrEmpty(_currentTrackCoverUrl))
{
<MudImage Src="@_currentTrackCoverUrl" Height="40" Width="40" Class="rounded" />
}
<MudText Typo="Typo.body1" Style="font-weight: 500;">@_currentTrackTitle</MudText>
</MudStack>
<!-- Кнопки управления -->
<MudStack Row Gap="1">
<MudIconButton Icon="@(_isPlaying? Icons.Material.Filled.Pause : Icons.Material.Filled.PlayArrow)" Size="Size.Medium" Color="Color.Primary" OnClick="TogglePlayPause" />
<MudIconButton Icon="@Icons.Material.Filled.Stop" Size="Size.Medium" Color="Color.Default" OnClick="Stop" />
</MudStack>
<!-- Ползунок прогресса -->
<MudItem Style="flex-grow: 1; min-width: 150px;">
<MudSlider @bind-Value="_currentProgress" @bind-Value:event="oninput" Min="0" Max="100" Size="Size.Small" ValueChanged="@((double newValue) => SeekTo(newValue))" />
</MudItem>
<!-- Время и громкость -->
<MudStack Row Gap="2" AlignItems="AlignItems.Center">
<MudText Typo="Typo.body2">@_currentTime / @_totalTime</MudText>
<MudStack Row Gap="1" AlignItems="AlignItems.Center" Style="width: 120px;">
<MudIconButton Icon="@(_currentVolume == 0 ? Icons.Material.Filled.VolumeOff : Icons.Material.Filled.VolumeUp)" Size="Size.Small" Color="Color.Default" OnClick="ToggleMute" />
<MudSlider @bind-Value="_currentVolume" @bind-Value:event="oninput" Min="0" Max="100" Size="Size.Small" ValueChanged="@((double newValue) => ChangeVolume(newValue))" />
</MudStack>
</MudStack>
</MudStack>
</MudPaper>
<audio id="@_audioId" style="display: none;"></audio>
@code {
private const double _defaultVolume = 50;
private string _audioId = $"audio_{Guid.NewGuid():N}";
private IJSObjectReference? _audioModule;
private IJSObjectReference? _audioElement;
private double _currentProgress;
private double _currentVolume = _defaultVolume;
private string _currentTime = "0:00";
private string _totalTime = "0:00";
private bool _isPlaying;
private Timer? _progressTimer;
private bool _isMuted;
private string? _currentAccessToken;
private string? _currentSharedPlaylistId;
private string? _currentTrackCoverUrl;
private string? _currentTrackTitle;
protected override async Task OnInitializedAsync()
{
AudioPlayerService.OnLoadAndPlayRequested += OnLoadAndPlay;
AudioPlayerService.OnPlayRequested += OnPlay;
AudioPlayerService.OnPauseRequested += OnPause;
AudioPlayerService.OnStopRequested += OnStop;
AudioPlayerService.OnSeekRequested += OnSeek;
AudioPlayerService.OnVolumeChangeRequested += OnVolumeChange;
AudioPlayerService.OnStateChanged += OnStateChanged;
await LoadSavedVolume();
await AudioPlayerService.SetVolumeAsync(_currentVolume); // синхронизация
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await EnsureAudioModuleAsync();
}
}
private void OnStateChanged()
{
_currentTrackTitle = AudioPlayerService.CurrentTrackTitle;
_currentTrackCoverUrl = AudioPlayerService.CurrentTrackCoverUrl?.FormatCoverUrl(40, 40);
InvokeAsync(StateHasChanged);
}
private async Task LoadSavedVolume()
{
var savedVolume = await PlayerStorage.GetVolumeAsync() ?? _defaultVolume;
_currentVolume = savedVolume;
await AudioPlayerService.SetVolumeAsync(savedVolume);
}
private async Task EnsureAudioModuleAsync()
{
if (_audioModule == null)
_audioModule = await JS.InvokeAsync<IJSObjectReference>("import", "/js/AudioPlayer.js");
if (_audioElement == null)
_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()
{
var authState = await AuthProvider.GetAuthenticationStateAsync();
if (!authState.User.Identity?.IsAuthenticated == true)
{
Snackbar.Add("Воспроизведение доступно только авторизованным пользователям", Severity.Warning);
return false;
}
return true;
}
private async Task OnLoadAndPlay(string trackId, string? accessToken, string? sharedPlaylistId)
{
//if (!await CheckAuthAsync()) return;
var tokens = await TokenStorage.GetTokensAsync();
_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, _currentAccessToken, _currentSharedPlaylistId);
_isPlaying = true;
StartProgressTimer();
StateHasChanged();
}
private async Task OnPlay()
{
if (_audioElement == null) return;
await _audioElement.InvokeVoidAsync("play");
_isPlaying = true;
StartProgressTimer();
StateHasChanged();
}
private async Task OnPause()
{
if (_audioElement == null) return;
await _audioElement.InvokeVoidAsync("pause");
_isPlaying = false;
StopProgressTimer();
StateHasChanged();
}
private async Task OnStop()
{
if (_audioElement == null) return;
await _audioElement.InvokeVoidAsync("stop");
_isPlaying = false;
_currentProgress = 0;
StopProgressTimer();
StateHasChanged();
}
private async Task OnSeek(double percent)
{
if (_audioElement == null) return;
try
{
var duration = await _audioElement.InvokeAsync<double>("getDuration");
if (duration > 0 && !double.IsNaN(duration))
{
var newTime = (percent / 100) * duration;
await _audioElement.InvokeVoidAsync("setCurrentTime", newTime);
}
}
catch (Exception ex)
{
Console.WriteLine($"Seek error: {ex.Message}");
}
}
private async Task OnVolumeChange(double volume)
{
if (_audioElement == null) return;
try
{
await _audioElement.InvokeVoidAsync("setVolume", volume / 100);
_isMuted = false;
_currentVolume = volume;
await PlayerStorage.SetVolumeAsync(volume);
StateHasChanged();
}
catch (Exception ex)
{
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()
{
_isMuted = !_isMuted;
var newVolume = _isMuted ? 0 : _currentVolume;
await AudioPlayerService.SetVolumeAsync(newVolume);
}
private void StartProgressTimer()
{
StopProgressTimer();
_progressTimer = new Timer(async _ => await UpdateProgress(), null, 0, 500);
}
private void StopProgressTimer() => _progressTimer?.Dispose();
private async Task UpdateProgress()
{
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))
{
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);
}
}
catch (Exception ex)
{
Console.WriteLine($"UpdateProgress error: {ex.Message}");
}
}
private string FormatTime(double seconds)
{
var total = (int)seconds;
var mins = total / 60;
var secs = total % 60;
return $"{mins}:{secs:D2}";
}
public async ValueTask DisposeAsync()
{
try
{
StopProgressTimer();
if (_audioElement != null)
await _audioElement.DisposeAsync();
if (_audioModule != null)
await _audioModule.DisposeAsync();
}
catch { }
}
}