Доработан плеер

This commit is contained in:
FrigaT
2026-04-16 04:11:04 +03:00
parent 3e18537a0e
commit 35140b71b7
4 changed files with 181 additions and 200 deletions

View File

@@ -7,71 +7,88 @@
@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>
<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">
<!-- Кнопки управления -->
<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 @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 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 AlignItems="AlignItems.Stretch" Class="flex-grow-1" Style="height: 100%;" Spacing="0">
<MudStack Row AlignItems="AlignItems.Stretch" Class="flex-grow-1">
<MudText Typo="Typo.body2" Style="font-weight: 500; line-height: 1.2;">@AudioPlayerService.CurrentTrackTitle</MudText>
<MudSpacer />
<MudText Typo="Typo.body2" Style="font-weight: 500; line-height: 1.2;">@AudioPlayerService.CurrentTimeString / @AudioPlayerService.TotalTimeString</MudText>
</MudStack>
<MudSlider Value="@AudioPlayerService.CurrentProgress" Class="mt-n1" Min="0" Max="100" Size="Size.Small" ValueChanged="@((double newValue) => SeekTo(newValue))" Step="0.01" />
</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="true"
Class="pa-0 mt-n5"
Style="height:120px; width: 10px; background-color: transparent !important; overflow: visible !important;">
<MudProgressLinear Vertical="true" Color="Color.Primary" Size="Size.Medium" Value="@AudioPlayerService.CurrentVolume" />
</MudPopover>
</MudItem>
</MudStack>
</MudPaper>
<audio id="@_audioId" style="display: none;"></audio>
@code {
private const double _defaultVolume = 50;
private const double _volumeDefault = 50;
// Генерируем уникальный ID для аудиоэлемента, чтобы избежать конфликтов при множественных экземплярах
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;
// Громкость
private bool _volumeIsOpen;
private double _volumeBeforeMute;
private bool _isPlayHovered;
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); // синхронизация
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)
@@ -81,20 +98,6 @@
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()
{
@@ -108,32 +111,21 @@
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);
if (!double.IsNaN(duration) && !double.IsNaN(currentTime) && duration > 0)
{
AudioPlayerService.UpdateProgress(currentTime, duration);
}
}
[JSInvokable]
public async Task OnDurationReady(double duration)
public async Task OnDownloadProgress(double second)
{
if (duration <= 0) return;
_totalTime = FormatTime(duration);
await InvokeAsync(StateHasChanged);
var x = second;
}
private async Task<bool> CheckAuthAsync()
@@ -147,67 +139,44 @@
return true;
}
private async Task OnLoadAndPlay(string trackId, string? accessToken, string? sharedPlaylistId)
#region Обработка сервиса
private async Task OnServiceLoadAndPlay(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))
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, _currentAccessToken, _currentSharedPlaylistId);
_isPlaying = true;
StartProgressTimer();
StateHasChanged();
await _audioElement!.InvokeVoidAsync("loadAndPlay", streamUrl, accessToken, sharedPlaylistId);
}
private async Task OnPlay()
private async Task OnServicePlay()
{
if (_audioElement == null) return;
await _audioElement.InvokeVoidAsync("play");
_isPlaying = true;
StartProgressTimer();
StateHasChanged();
}
private async Task OnPause()
private async Task OnServicePause()
{
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)
private async Task OnServiceSeek(double time)
{
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);
}
await _audioElement.InvokeVoidAsync("setCurrentTime", time);
}
catch (Exception ex)
{
@@ -215,15 +184,14 @@
}
}
private async Task OnVolumeChange(double volume)
private async Task OnServiceVolumeChange(double volume)
{
if (_audioElement == null) return;
if (volume == AudioPlayerService.CurrentVolume) return;
try
{
await _audioElement.InvokeVoidAsync("setVolume", volume / 100);
_isMuted = false;
_currentVolume = volume;
await PlayerStorage.SetVolumeAsync(volume);
StateHasChanged();
}
catch (Exception ex)
@@ -232,17 +200,21 @@
}
}
private async Task TogglePlayPause()
private void OnServiceStateChanged()
{
if (_isPlaying)
await AudioPlayerService.PauseAsync();
else
await AudioPlayerService.PlayAsync();
InvokeAsync(StateHasChanged);
}
#endregion
private async Task Stop()
private async Task OnVolumeHandleWheel(WheelEventArgs e)
{
await AudioPlayerService.StopAsync();
// Изменяем громкость на 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)
@@ -257,57 +229,30 @@
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
if (AudioPlayerService.CurrentVolume > 0)
{
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);
}
_volumeBeforeMute = AudioPlayerService.CurrentVolume;
await AudioPlayerService.SetVolumeAsync(0);
}
catch (Exception ex)
else
{
Console.WriteLine($"UpdateProgress error: {ex.Message}");
await AudioPlayerService.SetVolumeAsync(_volumeBeforeMute);
_volumeBeforeMute = 0;
}
}
private string FormatTime(double seconds)
private async Task OnPlayClick()
{
var total = (int)seconds;
var mins = total / 60;
var secs = total % 60;
return $"{mins}:{secs:D2}";
if (AudioPlayerService.IsPlaying)
await AudioPlayerService.PauseAsync();
else
await AudioPlayerService.PlayAsync();
}
public async ValueTask DisposeAsync()
{
try
{
StopProgressTimer();
if (_audioElement != null)
await _audioElement.DisposeAsync();
if (_audioModule != null)