279 lines
10 KiB
Plaintext
279 lines
10 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-2" Elevation="0" Width="100%" Style="background-color: rgba(0,0,0,0.05); border-radius: 8px;">
|
|
<MudStack Row AlignItems="AlignItems.Center" Wrap="Wrap.Wrap">
|
|
<!-- Кнопки управления -->
|
|
<MudItem @onmouseenter="() => { _isPlayHovered = true; }"
|
|
@onmouseleave="() => { _isPlayHovered = false; }"
|
|
style="position: relative; display: inline-block; cursor: pointer; border-radius: 4px; overflow: hidden; width: 50px; height: 50px;">
|
|
|
|
@if (!string.IsNullOrEmpty(AudioPlayerService.CurrentTrackCoverUrl))
|
|
{
|
|
<MudImage Src="@AudioPlayerService.CurrentTrackCoverUrl.FormatCoverUrl(50, 50)" Height="50" Width="50" Class="rounded" Style="display: block;" />
|
|
}
|
|
|
|
<MudItem class="play-overlay"
|
|
style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: transparent; display: flex; align-items: center; justify-content: center; border-radius: 4px;">
|
|
<MudToggleIconButton Toggled="@AudioPlayerService.IsPlaying"
|
|
Icon="@Icons.Material.Filled.PlayArrow"
|
|
Color="@Color.Primary"
|
|
ToggledIcon="@Icons.Material.Filled.Pause"
|
|
ToggledColor="@Color.Primary"
|
|
ToggledChanged="OnPlayClick" />
|
|
</MudItem>
|
|
</MudItem>
|
|
|
|
<!-- Название и прогресс -->
|
|
<MudStack AlignItems="AlignItems.Stretch" Class="d-flex flex-grow-1 relative overflow-hidden align-center rounded-sm" Style="height: 50px;">
|
|
<MudItem Class="absolute " style="top: 0; left: 0; right: 0; bottom: 0; z-index: 1;">
|
|
<TrackProgress Value="@AudioPlayerService.CurrentProgress"
|
|
Min="0" Max="100"
|
|
Height="50"
|
|
BufferValue="@_bufferValue"
|
|
Color="Color.Primary"
|
|
Icon=""
|
|
Step="0.1"
|
|
Opacity="0.3"
|
|
Buffer
|
|
ValueChanged="SeekTo" />
|
|
</MudItem>
|
|
|
|
<MudStack Row AlignItems="AlignItems.Center" Class="px-3 relative pointer-events-none" Style="z-index: 2; width: 100%;">
|
|
<MudText Typo="Typo.body2" Style="font-weight: 600; text-shadow: 0px 0px 4px rgba(255,255,255,0.8);">
|
|
@AudioPlayerService.CurrentTrackTitle
|
|
</MudText>
|
|
<MudSpacer />
|
|
<MudText Typo="Typo.body2" Style="font-family: monospace; font-weight: 600;">
|
|
@AudioPlayerService.CurrentTimeString / @AudioPlayerService.TotalTimeString
|
|
</MudText>
|
|
</MudStack>
|
|
</MudStack>
|
|
|
|
<!-- Громкость -->
|
|
<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>
|
|
</MudStack>
|
|
</MudPaper>
|
|
|
|
<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 _bufferValue;
|
|
|
|
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 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));
|
|
}
|
|
|
|
[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)
|
|
{
|
|
_bufferValue = second / AudioPlayerService.TotalTime * 100;
|
|
}
|
|
|
|
private async Task<bool> CheckAuthAsync()
|
|
{
|
|
var authState = await AuthProvider.GetAuthenticationStateAsync();
|
|
if (!authState.User.Identity?.IsAuthenticated == true)
|
|
{
|
|
Snackbar.Add("Воспроизведение доступно только авторизованным пользователям", Severity.Warning);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
#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;
|
|
if (volume == AudioPlayerService.CurrentVolume) 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 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 { }
|
|
}
|
|
} |