318 lines
11 KiB
Plaintext
318 lines
11 KiB
Plaintext
@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 { }
|
||
}
|
||
} |