Доработан плеер
This commit is contained in:
@@ -7,71 +7,88 @@
|
|||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
@inject HttpClient Http
|
@inject HttpClient Http
|
||||||
|
|
||||||
<MudPaper Class="pa-4" Elevation="0" Width="100%" Style="background-color: rgba(0,0,0,0.05); border-radius: 8px;">
|
<MudPaper Class="pa-2" 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 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">
|
<MudItem @onmouseenter="() => { _isPlayHovered = true; }"
|
||||||
<MudIconButton Icon="@(_isPlaying? Icons.Material.Filled.Pause : Icons.Material.Filled.PlayArrow)" Size="Size.Medium" Color="Color.Primary" OnClick="TogglePlayPause" />
|
@onmouseleave="() => { _isPlayHovered = false; }"
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Stop" Size="Size.Medium" Color="Color.Default" OnClick="Stop" />
|
style="position: relative; display: inline-block; cursor: pointer; border-radius: 4px; overflow: hidden; width: 50px; height: 50px;">
|
||||||
</MudStack>
|
|
||||||
|
|
||||||
<!-- Ползунок прогресса -->
|
@if (!string.IsNullOrEmpty(AudioPlayerService.CurrentTrackCoverUrl))
|
||||||
<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))" />
|
<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>
|
</MudItem>
|
||||||
|
|
||||||
<!-- Время и громкость -->
|
<!-- Название и прогресс -->
|
||||||
<MudStack Row Gap="2" AlignItems="AlignItems.Center">
|
<MudStack AlignItems="AlignItems.Stretch" Class="flex-grow-1" Style="height: 100%;" Spacing="0">
|
||||||
<MudText Typo="Typo.body2">@_currentTime / @_totalTime</MudText>
|
<MudStack Row AlignItems="AlignItems.Stretch" Class="flex-grow-1">
|
||||||
<MudStack Row Gap="1" AlignItems="AlignItems.Center" Style="width: 120px;">
|
<MudText Typo="Typo.body2" Style="font-weight: 500; line-height: 1.2;">@AudioPlayerService.CurrentTrackTitle</MudText>
|
||||||
<MudIconButton Icon="@(_currentVolume == 0 ? Icons.Material.Filled.VolumeOff : Icons.Material.Filled.VolumeUp)" Size="Size.Small" Color="Color.Default" OnClick="ToggleMute" />
|
<MudSpacer />
|
||||||
<MudSlider @bind-Value="_currentVolume" @bind-Value:event="oninput" Min="0" Max="100" Size="Size.Small" ValueChanged="@((double newValue) => ChangeVolume(newValue))" />
|
<MudText Typo="Typo.body2" Style="font-weight: 500; line-height: 1.2;">@AudioPlayerService.CurrentTimeString / @AudioPlayerService.TotalTimeString</MudText>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
|
|
||||||
|
<MudSlider Value="@AudioPlayerService.CurrentProgress" Class="mt-n1" Min="0" Max="100" Size="Size.Small" ValueChanged="@((double newValue) => SeekTo(newValue))" Step="0.01" />
|
||||||
</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="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>
|
</MudStack>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|
||||||
<audio id="@_audioId" style="display: none;"></audio>
|
<audio id="@_audioId" style="display: none;"></audio>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private const double _defaultVolume = 50;
|
private const double _volumeDefault = 50;
|
||||||
|
|
||||||
|
// Генерируем уникальный ID для аудиоэлемента, чтобы избежать конфликтов при множественных экземплярах
|
||||||
private string _audioId = $"audio_{Guid.NewGuid():N}";
|
private string _audioId = $"audio_{Guid.NewGuid():N}";
|
||||||
private IJSObjectReference? _audioModule;
|
private IJSObjectReference? _audioModule;
|
||||||
private IJSObjectReference? _audioElement;
|
private IJSObjectReference? _audioElement;
|
||||||
private double _currentProgress;
|
|
||||||
private double _currentVolume = _defaultVolume;
|
// Громкость
|
||||||
private string _currentTime = "0:00";
|
private bool _volumeIsOpen;
|
||||||
private string _totalTime = "0:00";
|
private double _volumeBeforeMute;
|
||||||
private bool _isPlaying;
|
|
||||||
private Timer? _progressTimer;
|
private bool _isPlayHovered;
|
||||||
private bool _isMuted;
|
|
||||||
private string? _currentAccessToken;
|
|
||||||
private string? _currentSharedPlaylistId;
|
|
||||||
private string? _currentTrackCoverUrl;
|
|
||||||
private string? _currentTrackTitle;
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
AudioPlayerService.OnLoadAndPlayRequested += OnLoadAndPlay;
|
AudioPlayerService.OnLoadAndPlayRequested += OnServiceLoadAndPlay;
|
||||||
AudioPlayerService.OnPlayRequested += OnPlay;
|
AudioPlayerService.OnPlayRequested += OnServicePlay;
|
||||||
AudioPlayerService.OnPauseRequested += OnPause;
|
AudioPlayerService.OnPauseRequested += OnServicePause;
|
||||||
AudioPlayerService.OnStopRequested += OnStop;
|
AudioPlayerService.OnSeekRequested += OnServiceSeek;
|
||||||
AudioPlayerService.OnSeekRequested += OnSeek;
|
AudioPlayerService.OnVolumeChangeRequested += OnServiceVolumeChange;
|
||||||
AudioPlayerService.OnVolumeChangeRequested += OnVolumeChange;
|
AudioPlayerService.OnStateChanged += OnServiceStateChanged;
|
||||||
AudioPlayerService.OnStateChanged += OnStateChanged;
|
|
||||||
|
|
||||||
await LoadSavedVolume();
|
|
||||||
await AudioPlayerService.SetVolumeAsync(_currentVolume); // синхронизация
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
@@ -82,20 +99,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
private async Task EnsureAudioModuleAsync()
|
||||||
{
|
{
|
||||||
if (_audioModule == null)
|
if (_audioModule == null)
|
||||||
@@ -108,32 +111,21 @@
|
|||||||
public async Task OnAudioEnded()
|
public async Task OnAudioEnded()
|
||||||
{
|
{
|
||||||
AudioPlayerService.NotifyTrackEnded();
|
AudioPlayerService.NotifyTrackEnded();
|
||||||
_isPlaying = false;
|
|
||||||
_currentProgress = 0;
|
|
||||||
StopProgressTimer();
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[JSInvokable]
|
[JSInvokable]
|
||||||
public async Task OnTimeUpdate(double currentTime, double duration)
|
public async Task OnTimeUpdate(double currentTime, double duration)
|
||||||
{
|
{
|
||||||
if (double.IsNaN(currentTime) || double.IsNaN(duration) || duration <= 0) return;
|
if (!double.IsNaN(duration) && !double.IsNaN(currentTime) && duration > 0)
|
||||||
var progress = (currentTime / duration) * 100;
|
{
|
||||||
var currentTimeStr = FormatTime(currentTime);
|
AudioPlayerService.UpdateProgress(currentTime, duration);
|
||||||
var totalTimeStr = FormatTime(duration);
|
}
|
||||||
AudioPlayerService.UpdateProgress(progress, currentTimeStr, totalTimeStr);
|
|
||||||
_currentProgress = progress;
|
|
||||||
_currentTime = currentTimeStr;
|
|
||||||
_totalTime = totalTimeStr;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[JSInvokable]
|
[JSInvokable]
|
||||||
public async Task OnDurationReady(double duration)
|
public async Task OnDownloadProgress(double second)
|
||||||
{
|
{
|
||||||
if (duration <= 0) return;
|
var x = second;
|
||||||
_totalTime = FormatTime(duration);
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> CheckAuthAsync()
|
private async Task<bool> CheckAuthAsync()
|
||||||
@@ -147,15 +139,16 @@
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnLoadAndPlay(string trackId, string? accessToken, string? sharedPlaylistId)
|
#region Обработка сервиса
|
||||||
|
private async Task OnServiceLoadAndPlay(string trackId, string? accessToken, string? sharedPlaylistId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(accessToken))
|
||||||
{
|
{
|
||||||
//if (!await CheckAuthAsync()) return;
|
|
||||||
|
|
||||||
var tokens = await TokenStorage.GetTokensAsync();
|
var tokens = await TokenStorage.GetTokensAsync();
|
||||||
_currentAccessToken = accessToken ?? tokens.token;
|
accessToken = tokens.token;
|
||||||
_currentSharedPlaylistId = sharedPlaylistId;
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(_currentAccessToken) && string.IsNullOrWhiteSpace(_currentSharedPlaylistId))
|
if (string.IsNullOrWhiteSpace(accessToken) && string.IsNullOrWhiteSpace(sharedPlaylistId))
|
||||||
{
|
{
|
||||||
Snackbar.Add("Токен авторизации не найден. Пожалуйста, войдите заново.", Severity.Error);
|
Snackbar.Add("Токен авторизации не найден. Пожалуйста, войдите заново.", Severity.Error);
|
||||||
return;
|
return;
|
||||||
@@ -163,51 +156,27 @@
|
|||||||
|
|
||||||
var streamUrl = new Uri(Http.BaseAddress!, $"/api/audio/track/{trackId}").ToString();
|
var streamUrl = new Uri(Http.BaseAddress!, $"/api/audio/track/{trackId}").ToString();
|
||||||
await EnsureAudioModuleAsync();
|
await EnsureAudioModuleAsync();
|
||||||
await _audioElement!.InvokeVoidAsync("loadAndPlay", streamUrl, _currentAccessToken, _currentSharedPlaylistId);
|
await _audioElement!.InvokeVoidAsync("loadAndPlay", streamUrl, accessToken, sharedPlaylistId);
|
||||||
_isPlaying = true;
|
|
||||||
StartProgressTimer();
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnPlay()
|
private async Task OnServicePlay()
|
||||||
{
|
{
|
||||||
if (_audioElement == null) return;
|
if (_audioElement == null) return;
|
||||||
await _audioElement.InvokeVoidAsync("play");
|
await _audioElement.InvokeVoidAsync("play");
|
||||||
_isPlaying = true;
|
|
||||||
StartProgressTimer();
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnPause()
|
private async Task OnServicePause()
|
||||||
{
|
{
|
||||||
if (_audioElement == null) return;
|
if (_audioElement == null) return;
|
||||||
await _audioElement.InvokeVoidAsync("pause");
|
await _audioElement.InvokeVoidAsync("pause");
|
||||||
_isPlaying = false;
|
|
||||||
StopProgressTimer();
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnStop()
|
private async Task OnServiceSeek(double time)
|
||||||
{
|
|
||||||
if (_audioElement == null) return;
|
|
||||||
await _audioElement.InvokeVoidAsync("stop");
|
|
||||||
_isPlaying = false;
|
|
||||||
_currentProgress = 0;
|
|
||||||
StopProgressTimer();
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task OnSeek(double percent)
|
|
||||||
{
|
{
|
||||||
if (_audioElement == null) return;
|
if (_audioElement == null) return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var duration = await _audioElement.InvokeAsync<double>("getDuration");
|
await _audioElement.InvokeVoidAsync("setCurrentTime", time);
|
||||||
if (duration > 0 && !double.IsNaN(duration))
|
|
||||||
{
|
|
||||||
var newTime = (percent / 100) * duration;
|
|
||||||
await _audioElement.InvokeVoidAsync("setCurrentTime", newTime);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -215,15 +184,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnVolumeChange(double volume)
|
private async Task OnServiceVolumeChange(double volume)
|
||||||
{
|
{
|
||||||
if (_audioElement == null) return;
|
if (_audioElement == null) return;
|
||||||
|
if (volume == AudioPlayerService.CurrentVolume) return;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _audioElement.InvokeVoidAsync("setVolume", volume / 100);
|
await _audioElement.InvokeVoidAsync("setVolume", volume / 100);
|
||||||
_isMuted = false;
|
|
||||||
_currentVolume = volume;
|
|
||||||
await PlayerStorage.SetVolumeAsync(volume);
|
|
||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -232,17 +200,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task TogglePlayPause()
|
private void OnServiceStateChanged()
|
||||||
{
|
{
|
||||||
if (_isPlaying)
|
InvokeAsync(StateHasChanged);
|
||||||
await AudioPlayerService.PauseAsync();
|
|
||||||
else
|
|
||||||
await AudioPlayerService.PlayAsync();
|
|
||||||
}
|
}
|
||||||
|
#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)
|
private async Task SeekTo(double value)
|
||||||
@@ -257,57 +229,30 @@
|
|||||||
|
|
||||||
private async Task ToggleMute()
|
private async Task ToggleMute()
|
||||||
{
|
{
|
||||||
_isMuted = !_isMuted;
|
if (AudioPlayerService.CurrentVolume > 0)
|
||||||
var newVolume = _isMuted ? 0 : _currentVolume;
|
{
|
||||||
await AudioPlayerService.SetVolumeAsync(newVolume);
|
_volumeBeforeMute = AudioPlayerService.CurrentVolume;
|
||||||
|
await AudioPlayerService.SetVolumeAsync(0);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
private void StartProgressTimer()
|
|
||||||
{
|
{
|
||||||
StopProgressTimer();
|
await AudioPlayerService.SetVolumeAsync(_volumeBeforeMute);
|
||||||
_progressTimer = new Timer(async _ => await UpdateProgress(), null, 0, 500);
|
_volumeBeforeMute = 0;
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
private async Task OnPlayClick()
|
||||||
{
|
{
|
||||||
var total = (int)seconds;
|
if (AudioPlayerService.IsPlaying)
|
||||||
var mins = total / 60;
|
await AudioPlayerService.PauseAsync();
|
||||||
var secs = total % 60;
|
else
|
||||||
return $"{mins}:{secs:D2}";
|
await AudioPlayerService.PlayAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
StopProgressTimer();
|
|
||||||
if (_audioElement != null)
|
if (_audioElement != null)
|
||||||
await _audioElement.DisposeAsync();
|
await _audioElement.DisposeAsync();
|
||||||
if (_audioModule != null)
|
if (_audioModule != null)
|
||||||
|
|||||||
@@ -10,15 +10,18 @@ public class AudioPlayerService : IAudioPlayerService
|
|||||||
private readonly TokenStorage _tokenStorage;
|
private readonly TokenStorage _tokenStorage;
|
||||||
private readonly ISnackbar _snackbar;
|
private readonly ISnackbar _snackbar;
|
||||||
private readonly HttpClient _http;
|
private readonly HttpClient _http;
|
||||||
|
private readonly PlayerStorage _playerStorage;
|
||||||
|
|
||||||
private string? _currentTrackId;
|
private string? _currentTrackId;
|
||||||
private string? _currentTrackTitle;
|
private string? _currentTrackTitle;
|
||||||
private string? _currentTrackCoverUrl;
|
private string? _currentTrackCoverUrl;
|
||||||
private bool _isPlaying;
|
private bool _isPlaying;
|
||||||
private double _currentVolume = 70;
|
private double _currentVolume = 50;
|
||||||
private double _currentProgress;
|
private double _currentProgress;
|
||||||
private string _currentTime = "0:00";
|
private double _currentTime = 0;
|
||||||
private string _totalTime = "0:00";
|
private double _totalTime = 0;
|
||||||
|
private string _currentTimeString = "0:00";
|
||||||
|
private string _totalTimeString = "0:00";
|
||||||
|
|
||||||
public string? CurrentTrackId => _currentTrackId;
|
public string? CurrentTrackId => _currentTrackId;
|
||||||
public string? CurrentTrackTitle => _currentTrackTitle;
|
public string? CurrentTrackTitle => _currentTrackTitle;
|
||||||
@@ -34,21 +37,42 @@ public class AudioPlayerService : IAudioPlayerService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
public double CurrentProgress => _currentProgress;
|
public double CurrentProgress => _currentProgress;
|
||||||
public string CurrentTime => _currentTime;
|
public double CurrentTime => _currentTime;
|
||||||
public string TotalTime => _totalTime;
|
public double TotalTime => _totalTime;
|
||||||
|
public string CurrentTimeString => _currentTimeString;
|
||||||
|
public string TotalTimeString => _totalTimeString;
|
||||||
|
|
||||||
public event Action? OnStateChanged;
|
public event Action? OnStateChanged;
|
||||||
|
|
||||||
public AudioPlayerService(TokenStorage tokenStorage, ISnackbar snackbar, HttpClient httpClient)
|
public AudioPlayerService(TokenStorage tokenStorage, ISnackbar snackbar, HttpClient httpClient, PlayerStorage playerStorage)
|
||||||
{
|
{
|
||||||
_tokenStorage = tokenStorage;
|
_tokenStorage = tokenStorage;
|
||||||
_snackbar = snackbar;
|
_snackbar = snackbar;
|
||||||
_http = httpClient;
|
_http = httpClient;
|
||||||
|
_playerStorage = playerStorage;
|
||||||
|
|
||||||
|
_ = LoadVolume();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadVolume()
|
||||||
|
{
|
||||||
|
var savedVolume = await _playerStorage.GetVolumeAsync();
|
||||||
|
|
||||||
|
if (savedVolume != null)
|
||||||
|
{
|
||||||
|
_currentVolume = savedVolume.Value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Внешние команды (вызываются из компонентов)
|
// Внешние команды (вызываются из компонентов)
|
||||||
public async Task LoadAndPlayAsync(string trackId, string? accessToken = null, string? playlistShareToken = null, string? title = null, string? coverUrl = null)
|
public async Task LoadAndPlayAsync(string trackId, string? accessToken = null, string? playlistShareToken = null, string? title = null, string? coverUrl = null)
|
||||||
{
|
{
|
||||||
|
if (_currentTrackId == trackId)
|
||||||
|
{
|
||||||
|
await PlayAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Если accessToken не передан, пытаемся получить его из хранилища
|
// Если accessToken не передан, пытаемся получить его из хранилища
|
||||||
if (string.IsNullOrWhiteSpace(accessToken))
|
if (string.IsNullOrWhiteSpace(accessToken))
|
||||||
{
|
{
|
||||||
@@ -101,19 +125,10 @@ public class AudioPlayerService : IAudioPlayerService
|
|||||||
OnPauseRequested?.Invoke();
|
OnPauseRequested?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StopAsync()
|
|
||||||
{
|
|
||||||
_isPlaying = false;
|
|
||||||
_currentTrackId = null;
|
|
||||||
_currentProgress = 0;
|
|
||||||
_currentTime = "0:00";
|
|
||||||
OnStateChanged?.Invoke();
|
|
||||||
OnStopRequested?.Invoke();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SeekToAsync(double percent)
|
public async Task SeekToAsync(double percent)
|
||||||
{
|
{
|
||||||
OnSeekRequested?.Invoke(percent);
|
var newTime = (percent / 100) * _totalTime;
|
||||||
|
OnSeekRequested?.Invoke(newTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetVolumeAsync(double volume)
|
public async Task SetVolumeAsync(double volume)
|
||||||
@@ -121,13 +136,13 @@ public class AudioPlayerService : IAudioPlayerService
|
|||||||
_currentVolume = volume;
|
_currentVolume = volume;
|
||||||
OnStateChanged?.Invoke();
|
OnStateChanged?.Invoke();
|
||||||
OnVolumeChangeRequested?.Invoke(volume);
|
OnVolumeChangeRequested?.Invoke(volume);
|
||||||
|
await _playerStorage.SetVolumeAsync(volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
// События для связи с реальным AudioPlayer компонентом
|
// События для связи с реальным AudioPlayer компонентом
|
||||||
public event Func<string, string?, string?, Task>? OnLoadAndPlayRequested;
|
public event Func<string, string?, string?, Task>? OnLoadAndPlayRequested;
|
||||||
public event Func<Task>? OnPlayRequested;
|
public event Func<Task>? OnPlayRequested;
|
||||||
public event Func<Task>? OnPauseRequested;
|
public event Func<Task>? OnPauseRequested;
|
||||||
public event Func<Task>? OnStopRequested;
|
|
||||||
public event Func<double, Task>? OnSeekRequested;
|
public event Func<double, Task>? OnSeekRequested;
|
||||||
public event Func<double, Task>? OnVolumeChangeRequested;
|
public event Func<double, Task>? OnVolumeChangeRequested;
|
||||||
|
|
||||||
@@ -144,11 +159,14 @@ public class AudioPlayerService : IAudioPlayerService
|
|||||||
OnStateChanged?.Invoke();
|
OnStateChanged?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateProgress(double progress, string currentTime, string totalTime)
|
public void UpdateProgress(double currentTime, double totalTime)
|
||||||
{
|
{
|
||||||
|
var progress = (currentTime / totalTime) * 100;
|
||||||
_currentProgress = progress;
|
_currentProgress = progress;
|
||||||
_currentTime = currentTime;
|
_currentTime = currentTime;
|
||||||
|
_currentTimeString = FormatDuration(currentTime);
|
||||||
_totalTime = totalTime;
|
_totalTime = totalTime;
|
||||||
|
_totalTimeString = FormatDuration(totalTime);
|
||||||
OnStateChanged?.Invoke();
|
OnStateChanged?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +175,10 @@ public class AudioPlayerService : IAudioPlayerService
|
|||||||
_isPlaying = false;
|
_isPlaying = false;
|
||||||
_currentTrackId = null;
|
_currentTrackId = null;
|
||||||
_currentProgress = 0;
|
_currentProgress = 0;
|
||||||
_currentTime = "0:00";
|
_currentTime = 0;
|
||||||
|
_currentTimeString = "0:00";
|
||||||
|
_totalTime = 0;
|
||||||
|
_currentTimeString = "0:00";
|
||||||
OnStateChanged?.Invoke();
|
OnStateChanged?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,4 +204,11 @@ public class AudioPlayerService : IAudioPlayerService
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string FormatDuration(double seconds)
|
||||||
|
{
|
||||||
|
var mins = (int)(seconds / 60);
|
||||||
|
var secs = (int)(seconds % 60);
|
||||||
|
return $"{mins}:{secs:D2}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -19,11 +19,17 @@ public interface IAudioPlayerService
|
|||||||
/// <summary>Прогресс воспроизведения в процентах (0–100).</summary>
|
/// <summary>Прогресс воспроизведения в процентах (0–100).</summary>
|
||||||
double CurrentProgress { get; }
|
double CurrentProgress { get; }
|
||||||
|
|
||||||
|
/// <summary>Текущее время в секундах.</summary>
|
||||||
|
double CurrentTime { get; }
|
||||||
|
|
||||||
|
/// <summary>Общая длительность в секундах</summary>
|
||||||
|
double TotalTime { get; }
|
||||||
|
|
||||||
/// <summary>Отформатированное текущее время (мм:сс).</summary>
|
/// <summary>Отформатированное текущее время (мм:сс).</summary>
|
||||||
string CurrentTime { get; }
|
string CurrentTimeString { get; }
|
||||||
|
|
||||||
/// <summary>Отформатированная общая длительность (мм:сс).</summary>
|
/// <summary>Отформатированная общая длительность (мм:сс).</summary>
|
||||||
string TotalTime { get; }
|
string TotalTimeString { get; }
|
||||||
|
|
||||||
/// <summary>Отформатированное название текущего трека.</summary>
|
/// <summary>Отформатированное название текущего трека.</summary>
|
||||||
string? CurrentTrackTitle { get; }
|
string? CurrentTrackTitle { get; }
|
||||||
@@ -47,9 +53,6 @@ public interface IAudioPlayerService
|
|||||||
/// <summary>Поставить на паузу.</summary>
|
/// <summary>Поставить на паузу.</summary>
|
||||||
Task PauseAsync();
|
Task PauseAsync();
|
||||||
|
|
||||||
/// <summary>Остановить воспроизведение и выгрузить трек.</summary>
|
|
||||||
Task StopAsync();
|
|
||||||
|
|
||||||
/// <summary>Перемотать на указанный процент (0–100).</summary>
|
/// <summary>Перемотать на указанный процент (0–100).</summary>
|
||||||
Task SeekToAsync(double percent);
|
Task SeekToAsync(double percent);
|
||||||
|
|
||||||
@@ -76,10 +79,7 @@ public interface IAudioPlayerService
|
|||||||
/// <summary>Запрос на паузу.</summary>
|
/// <summary>Запрос на паузу.</summary>
|
||||||
event Func<Task>? OnPauseRequested;
|
event Func<Task>? OnPauseRequested;
|
||||||
|
|
||||||
/// <summary>Запрос на остановку и выгрузку трека.</summary>
|
/// <summary>Запрос на перемотку (секунды).</summary>
|
||||||
event Func<Task>? OnStopRequested;
|
|
||||||
|
|
||||||
/// <summary>Запрос на перемотку (процент 0–100).</summary>
|
|
||||||
event Func<double, Task>? OnSeekRequested;
|
event Func<double, Task>? OnSeekRequested;
|
||||||
|
|
||||||
/// <summary>Запрос на изменение громкости (0–100).</summary>
|
/// <summary>Запрос на изменение громкости (0–100).</summary>
|
||||||
@@ -94,7 +94,7 @@ public interface IAudioPlayerService
|
|||||||
void SetCurrentTrack(string? trackId);
|
void SetCurrentTrack(string? trackId);
|
||||||
|
|
||||||
/// <summary>Обновить прогресс и отображаемое время.</summary>
|
/// <summary>Обновить прогресс и отображаемое время.</summary>
|
||||||
void UpdateProgress(double progress, string currentTime, string totalTime);
|
void UpdateProgress(double currentTime, double totalTime);
|
||||||
|
|
||||||
/// <summary>Уведомить об окончании трека.</summary>
|
/// <summary>Уведомить об окончании трека.</summary>
|
||||||
void NotifyTrackEnded();
|
void NotifyTrackEnded();
|
||||||
|
|||||||
@@ -26,14 +26,13 @@
|
|||||||
const stop = () => { audio.pause(); audio.currentTime = 0; };
|
const stop = () => { audio.pause(); audio.currentTime = 0; };
|
||||||
const setVolume = (volume) => { audio.volume = toNumber(volume); };
|
const setVolume = (volume) => { audio.volume = toNumber(volume); };
|
||||||
const setCurrentTime = (time) => { audio.currentTime = toNumber(time); };
|
const setCurrentTime = (time) => { audio.currentTime = toNumber(time); };
|
||||||
const getDuration = () => durationReady ? durationValue : 0;
|
|
||||||
const getCurrentTime = () => toNumber(audio.currentTime);
|
|
||||||
|
|
||||||
audio.addEventListener('loadedmetadata', () => {
|
audio.addEventListener('loadedmetadata', () => {
|
||||||
|
const current = toNumber(audio.currentTime);
|
||||||
durationValue = toNumber(audio.duration);
|
durationValue = toNumber(audio.duration);
|
||||||
durationReady = durationValue > 0;
|
durationReady = durationValue > 0;
|
||||||
if (dotNetHelper && durationReady) {
|
if (dotNetHelper && durationReady) {
|
||||||
dotNetHelper.invokeMethodAsync('OnDurationReady', durationValue);
|
dotNetHelper.invokeMethodAsync('OnTimeUpdate', current, durationValue);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -50,6 +49,15 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
audio.addEventListener('progress', () => {
|
||||||
|
if (dotNetHelper) {
|
||||||
|
if (audio.buffered.length > 0 && audio.duration) {
|
||||||
|
const bufferedEnd = toNumber(audio.buffered.end(audio.buffered.length - 1));
|
||||||
|
dotNetHelper.invokeMethodAsync('OnDownloadProgress', bufferedEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Возвращаем все методы, которые будут вызываться из C#
|
// Возвращаем все методы, которые будут вызываться из C#
|
||||||
return { loadAndPlay, play, pause, stop, setVolume, setCurrentTime, getDuration, getCurrentTime };
|
return { loadAndPlay, play, pause, stop, setVolume, setCurrentTime };
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user