Files
PlaylistShared/PlaylistShared.Pwa/Components/Global/AudioPlayer.razor
2026-04-23 09:11:43 +03:00

301 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
<MudStack Spacing="1" Row AlignItems="AlignItems.Center" Wrap="Wrap.NoWrap">
<!-- Кнопки управления -->
<MudItem @onmouseenter="() => { _isPlayHovered = true; }"
@onmouseleave="() => { _isPlayHovered = false; }"
Class="relative d-inline-block rounded overflow-hidden cursor-pointer"
Style="width: 50px; height: 50px;">
@if (!string.IsNullOrEmpty(AudioPlayerService.CurrentTrack?.CoverUri))
{
<MudImage Src="@AudioPlayerService.CurrentTrack.CoverUri.FormatCoverUrl(50, 50)" Height="50" Width="50" Class="rounded d-block" />
}
<MudItem Class="absolute d-flex align-center justify-center rounded"
Style="top: 0; left: 0; right: 0; bottom: 0; background: transparent;">
<MudToggleIconButton Toggled="@AudioPlayerService.IsPlaying"
Icon="@Icons.Material.Filled.PlayArrow"
Color="@Color.Primary"
Size="Size.Large"
ToggledIcon="@Icons.Material.Filled.Pause"
ToggledColor="@Color.Primary"
ToggledChanged="OnPlayClick" />
</MudItem>
</MudItem>
<!-- Название и прогресс -->
@if (AudioPlayerService.CurrentTrack != null)
{
<MudStack Spacing="0" AlignItems="AlignItems.Stretch" Class="d-flex flex-grow-1 relative overflow-hidden align-center rounded" Style="height: 50px;">
<MudItem Class="absolute" style="top: 0; left: 0; right: 0; bottom: 0; z-index: 1;">
<TrackProgress Value="@AudioPlayerService.CurrentTime"
Min="0" Max="@AudioPlayerService.TotalTime"
Height="50"
BufferValue="@_bufferSecond"
Color="Color.Primary"
Icon=""
Step="0.1"
Opacity="0.3"
Buffer
ValueChanged="SeekTo" />
</MudItem>
<MudStack Spacing="0" Row AlignItems="AlignItems.Center" Class="px-3 relative pointer-events-none" Style="z-index: 2; width: 100%; height: 100%;">
<MudStack AlignItems="AlignItems.Start" Spacing="0" Style="min-width: 0; width: 100%;">
<MudText Typo="Typo.body2" Inline Color="Color.Default" Style="font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;">
@AudioPlayerService.CurrentTrack.Title
</MudText>
<MudText Typo="Typo.body2" Inline Color="Color.Default" Style="font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;">
@string.Join(", ", AudioPlayerService.CurrentTrack.Artists.Select(a => a.Name))
</MudText>
</MudStack>
<MudSpacer />
<MudText Typo="Typo.body2">
@AudioPlayerService.CurrentTimeString
</MudText>
</MudStack>
</MudStack>
}
else
{
<MudSpacer />
}
<MudHidden Breakpoint="Breakpoint.SmAndDown">
<!-- Громкость -->
<MudItem @onmouseenter="() => _volumeIsOpen = true"
@onmouseleave="() => _volumeIsOpen = false"
@onwheel="OnVolumeHandleWheel"
Style="position: relative; display: flex; align-items: center;">
<MudIconButton Icon="@(AudioPlayerService.CurrentVolume == 0 ? Icons.Material.Filled.VolumeOff : Icons.Material.Filled.VolumeUp)"
Size="Size.Small"
Color="Color.Default"
OnClick="ToggleMute" />
<MudPopover Open="@_volumeIsOpen"
AnchorOrigin="Origin.TopCenter"
TransformOrigin="Origin.BottomCenter"
Fixed
Class="pa-0 mt-n5"
Style="height:120px; width: 10px; background-color: transparent !important; overflow: visible !important;">
<MudProgressLinear Vertical Color="Color.Primary" Size="Size.Medium" Value="@AudioPlayerService.CurrentVolume" />
</MudPopover>
</MudItem>
</MudHidden>
</MudStack>
<audio id="@_audioId" style="display: none;"></audio>
@code {
private const double _volumeDefault = 50;
// Генерируем уникальный ID для аудиоэлемента, чтобы избежать конфликтов при множественных экземплярах
private string _audioId = $"audio_{Guid.NewGuid():N}";
private IJSObjectReference? _audioModule;
private IJSObjectReference? _audioElement;
// Громкость
private bool _volumeIsOpen;
private double _volumeBeforeMute;
private double _bufferSecond;
private bool _isPlayHovered;
protected override async Task OnInitializedAsync()
{
AudioPlayerService.OnLoadAndPlayRequested += OnServiceLoadAndPlay;
AudioPlayerService.OnPlayRequested += OnServicePlay;
AudioPlayerService.OnPauseRequested += OnServicePause;
AudioPlayerService.OnSeekRequested += OnServiceSeek;
AudioPlayerService.OnVolumeChangeRequested += OnServiceVolumeChange;
AudioPlayerService.OnStateChanged += OnServiceStateChanged;
}
protected override void OnParametersSet()
{
StateHasChanged();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await EnsureAudioModuleAsync();
}
}
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));
}
#region Обработка JS
[JSInvokable]
public async Task OnAudioEnded()
{
AudioPlayerService.NotifyTrackEnded();
}
[JSInvokable]
public async Task OnTimeUpdate(double currentTime, double duration)
{
if (!double.IsNaN(duration) && !double.IsNaN(currentTime) && duration > 0)
{
AudioPlayerService.UpdateProgress(currentTime, duration);
}
}
[JSInvokable]
public async Task OnDownloadProgress(double second)
{
_bufferSecond = second;
}
#endregion
#region Обработка сервиса
private async Task OnServiceLoadAndPlay(string trackId, string? accessToken, string? sharedPlaylistId)
{
if (string.IsNullOrWhiteSpace(accessToken))
{
var tokens = await TokenStorage.GetTokensAsync();
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);
}
private async Task OnServicePlay()
{
if (_audioElement == null) return;
await _audioElement.InvokeVoidAsync("play");
}
private async Task OnServicePause()
{
if (_audioElement == null) return;
await _audioElement.InvokeVoidAsync("pause");
}
private async Task OnServiceSeek(double time)
{
if (_audioElement == null) return;
try
{
await _audioElement.InvokeVoidAsync("setCurrentTime", time);
}
catch (Exception ex)
{
Console.WriteLine($"Seek error: {ex.Message}");
}
}
private async Task OnServiceVolumeChange(double volume)
{
if (_audioElement == null) return;
try
{
await _audioElement.InvokeVoidAsync("setVolume", volume / 100);
StateHasChanged();
}
catch (Exception ex)
{
Console.WriteLine($"Volume change error: {ex.Message}");
}
}
private void OnServiceStateChanged()
{
InvokeAsync(StateHasChanged);
}
#endregion
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 OnVolumeHandleWheel(WheelEventArgs e)
{
// Изменяем громкость на 5 единиц за один тик колесика
double step = 5;
double newVolume = e.DeltaY < 0
? Math.Min(AudioPlayerService.CurrentVolume + step, 100)
: Math.Max(AudioPlayerService.CurrentVolume - step, 0);
await ChangeVolume(newVolume);
}
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 (AudioPlayerService.CurrentVolume > 0)
{
_volumeBeforeMute = AudioPlayerService.CurrentVolume;
await AudioPlayerService.SetVolumeAsync(0);
}
else
{
await AudioPlayerService.SetVolumeAsync(_volumeBeforeMute);
_volumeBeforeMute = 0;
}
}
private async Task OnPlayClick()
{
if (AudioPlayerService.IsPlaying)
await AudioPlayerService.PauseAsync();
else
await AudioPlayerService.PlayAsync();
}
public async ValueTask DisposeAsync()
{
try
{
if (_audioElement != null)
await _audioElement.DisposeAsync();
if (_audioModule != null)
await _audioModule.DisposeAsync();
}
catch { }
}
}