@using Microsoft.JSInterop @using Microsoft.AspNetCore.Components.Authorization @namespace PlaylistShared.Pwa.Components @inject HttpClient Http
@_currentTime / @_totalTime
@code { private string _audioId = $"audio_{Guid.NewGuid():N}"; private IJSObjectReference? _audioModule; private IJSObjectReference? _audioElement; private double _currentProgress; private double _currentVolume = 70; private string _currentTime = "0:00"; private string _totalTime = "0:00"; private bool _isPlaying; private Timer? _progressTimer; private bool _isMuted; [Inject] protected IJSRuntime JS { get; set; } = null!; [Inject] private TokenStorage TokenStorage { get; set; } = null!; [Inject] private AuthenticationStateProvider AuthProvider { get; set; } = null!; [Inject] private ISnackbar Snackbar { get; set; } = null!; /// Требовать ли авторизацию для воспроизведения (по умолчанию true). [Parameter] public bool RequireAuth { get; set; } = true; /// ID расшаренного плейлиста. [Parameter] public string SharedPlaylistId { get; set; } = string.Empty; /// Событие при завершении трека. [Parameter] public EventCallback OnTrackEnded { get; set; } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await EnsureAudioModuleAsync(); } } [JSInvokable] public async Task OnAudioEnded() { _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); } private async Task EnsureAudioModuleAsync() { if (_audioModule == null) _audioModule = await JS.InvokeAsync("import", "/js/AudioPlayer.js"); if (_audioElement == null) _audioElement = await _audioModule.InvokeAsync("init", _audioId, DotNetObjectReference.Create(this)); } private async Task CheckAuthAsync() { if (!RequireAuth) return true; var authState = await AuthProvider.GetAuthenticationStateAsync(); var isAuthenticated = authState.User.Identity?.IsAuthenticated == true; if (!isAuthenticated) { Snackbar.Add("Воспроизведение доступно только авторизованным пользователям", Severity.Warning); return false; } return true; } public async Task LoadAndPlayAsync(string trackId) { if (!await CheckAuthAsync()) return; var tokens = await TokenStorage.GetTokensAsync(); var accessToken = tokens.token; if (string.IsNullOrWhiteSpace(accessToken) && string.IsNullOrWhiteSpace(SharedPlaylistId)) { 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); _isPlaying = true; StartProgressTimer(); StateHasChanged(); } public async Task PlayAsync() { if (!await CheckAuthAsync()) return; if (_audioElement == null) return; await _audioElement.InvokeVoidAsync("play"); _isPlaying = true; StartProgressTimer(); StateHasChanged(); } public async Task PauseAsync() { if (_audioElement == null) return; await _audioElement.InvokeVoidAsync("pause"); _isPlaying = false; StopProgressTimer(); StateHasChanged(); } public async Task StopAsync() { if (_audioElement == null) return; await _audioElement.InvokeVoidAsync("stop"); _isPlaying = false; _currentProgress = 0; StopProgressTimer(); StateHasChanged(); } private async Task TogglePlayPause() { if (_isPlaying) await PauseAsync(); else await PlayAsync(); } private async Task Stop() { await StopAsync(); } private async Task SeekTo(double value) { if (_audioElement == null) return; try { var duration = await _audioElement.InvokeAsync("getDuration"); if (duration > 0 && !double.IsNaN(duration)) { var newTime = (value / 100) * duration; await _audioElement.InvokeVoidAsync("setCurrentTime", newTime); } } catch (Exception ex) { Console.WriteLine($"SeekTo error: {ex.Message}"); } } private async Task ChangeVolume(double value) { if (_audioElement == null) return; try { var volume = value / 100; await _audioElement.InvokeVoidAsync("setVolume", volume); _isMuted = false; StateHasChanged(); } catch (Exception ex) { Console.WriteLine($"ChangeVolume error: {ex.Message}"); } } private async Task ToggleMute() { if (_audioElement == null) return; _isMuted = !_isMuted; var newVolume = _isMuted ? 0 : (_currentVolume / 100); await _audioElement.InvokeVoidAsync("setVolume", newVolume); StateHasChanged(); } 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) { Console.WriteLine("UpdateProgress: _audioElement is null"); return; } try { var current = await _audioElement.InvokeAsync("getCurrentTime"); var duration = await _audioElement.InvokeAsync("getDuration"); if (duration > 0 && !double.IsNaN(duration) && !double.IsNaN(current)) { _currentProgress = (current / duration) * 100; _currentTime = FormatTime(current); _totalTime = FormatTime(duration); 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 { } } }