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