From 35140b71b7d229f3a0347b587224db12d7a284e6 Mon Sep 17 00:00:00 2001 From: FrigaT Date: Thu, 16 Apr 2026 04:11:04 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=B0=D0=BD=20=D0=BF=D0=BB=D0=B5=D0=B5=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Components/Global/AudioPlayer.razor | 277 +++++++----------- .../Services/AudioPlayerService.cs | 68 +++-- .../Services/IAudioPlayerService.cs | 20 +- PlaylistShared.Pwa/wwwroot/js/AudioPlayer.js | 16 +- 4 files changed, 181 insertions(+), 200 deletions(-) diff --git a/PlaylistShared.Pwa/Components/Global/AudioPlayer.razor b/PlaylistShared.Pwa/Components/Global/AudioPlayer.razor index ffc0feb..5d548ff 100644 --- a/PlaylistShared.Pwa/Components/Global/AudioPlayer.razor +++ b/PlaylistShared.Pwa/Components/Global/AudioPlayer.razor @@ -7,71 +7,88 @@ @inject ISnackbar Snackbar @inject HttpClient Http - - - - - @if (!string.IsNullOrEmpty(_currentTrackCoverUrl)) - { - - } - @_currentTrackTitle - - + + - - - - - - - - + + + @if (!string.IsNullOrEmpty(AudioPlayerService.CurrentTrackCoverUrl)) + { + + } + + + + - - - - @_currentTime / @_totalTime - - - + + + + + @AudioPlayerService.CurrentTrackTitle + + @AudioPlayerService.CurrentTimeString / @AudioPlayerService.TotalTimeString + + + + + + + + + @* Попавер с минимальной шириной *@ + + + + @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 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("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("getCurrentTime"); - var duration = await _audioElement.InvokeAsync("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) diff --git a/PlaylistShared.Pwa/Services/AudioPlayerService.cs b/PlaylistShared.Pwa/Services/AudioPlayerService.cs index 554a5a8..d8fb637 100644 --- a/PlaylistShared.Pwa/Services/AudioPlayerService.cs +++ b/PlaylistShared.Pwa/Services/AudioPlayerService.cs @@ -10,15 +10,18 @@ public class AudioPlayerService : IAudioPlayerService private readonly TokenStorage _tokenStorage; private readonly ISnackbar _snackbar; private readonly HttpClient _http; + private readonly PlayerStorage _playerStorage; private string? _currentTrackId; private string? _currentTrackTitle; private string? _currentTrackCoverUrl; private bool _isPlaying; - private double _currentVolume = 70; + private double _currentVolume = 50; private double _currentProgress; - private string _currentTime = "0:00"; - private string _totalTime = "0:00"; + private double _currentTime = 0; + private double _totalTime = 0; + private string _currentTimeString = "0:00"; + private string _totalTimeString = "0:00"; public string? CurrentTrackId => _currentTrackId; public string? CurrentTrackTitle => _currentTrackTitle; @@ -34,21 +37,42 @@ public class AudioPlayerService : IAudioPlayerService } } public double CurrentProgress => _currentProgress; - public string CurrentTime => _currentTime; - public string TotalTime => _totalTime; + public double CurrentTime => _currentTime; + public double TotalTime => _totalTime; + public string CurrentTimeString => _currentTimeString; + public string TotalTimeString => _totalTimeString; public event Action? OnStateChanged; - public AudioPlayerService(TokenStorage tokenStorage, ISnackbar snackbar, HttpClient httpClient) + public AudioPlayerService(TokenStorage tokenStorage, ISnackbar snackbar, HttpClient httpClient, PlayerStorage playerStorage) { _tokenStorage = tokenStorage; _snackbar = snackbar; _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) { + if (_currentTrackId == trackId) + { + await PlayAsync(); + return; + } + // Если accessToken не передан, пытаемся получить его из хранилища if (string.IsNullOrWhiteSpace(accessToken)) { @@ -101,19 +125,10 @@ public class AudioPlayerService : IAudioPlayerService 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) { - OnSeekRequested?.Invoke(percent); + var newTime = (percent / 100) * _totalTime; + OnSeekRequested?.Invoke(newTime); } public async Task SetVolumeAsync(double volume) @@ -121,13 +136,13 @@ public class AudioPlayerService : IAudioPlayerService _currentVolume = volume; OnStateChanged?.Invoke(); OnVolumeChangeRequested?.Invoke(volume); + await _playerStorage.SetVolumeAsync(volume); } // События для связи с реальным AudioPlayer компонентом public event Func? OnLoadAndPlayRequested; public event Func? OnPlayRequested; public event Func? OnPauseRequested; - public event Func? OnStopRequested; public event Func? OnSeekRequested; public event Func? OnVolumeChangeRequested; @@ -144,11 +159,14 @@ public class AudioPlayerService : IAudioPlayerService 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; _currentTime = currentTime; + _currentTimeString = FormatDuration(currentTime); _totalTime = totalTime; + _totalTimeString = FormatDuration(totalTime); OnStateChanged?.Invoke(); } @@ -157,7 +175,10 @@ public class AudioPlayerService : IAudioPlayerService _isPlaying = false; _currentTrackId = null; _currentProgress = 0; - _currentTime = "0:00"; + _currentTime = 0; + _currentTimeString = "0:00"; + _totalTime = 0; + _currentTimeString = "0:00"; OnStateChanged?.Invoke(); } @@ -183,4 +204,11 @@ public class AudioPlayerService : IAudioPlayerService } return null; } + + private string FormatDuration(double seconds) + { + var mins = (int)(seconds / 60); + var secs = (int)(seconds % 60); + return $"{mins}:{secs:D2}"; + } } \ No newline at end of file diff --git a/PlaylistShared.Pwa/Services/IAudioPlayerService.cs b/PlaylistShared.Pwa/Services/IAudioPlayerService.cs index 8ade31a..da83a65 100644 --- a/PlaylistShared.Pwa/Services/IAudioPlayerService.cs +++ b/PlaylistShared.Pwa/Services/IAudioPlayerService.cs @@ -19,11 +19,17 @@ public interface IAudioPlayerService /// Прогресс воспроизведения в процентах (0–100). double CurrentProgress { get; } + /// Текущее время в секундах. + double CurrentTime { get; } + + /// Общая длительность в секундах + double TotalTime { get; } + /// Отформатированное текущее время (мм:сс). - string CurrentTime { get; } + string CurrentTimeString { get; } /// Отформатированная общая длительность (мм:сс). - string TotalTime { get; } + string TotalTimeString { get; } /// Отформатированное название текущего трека. string? CurrentTrackTitle { get; } @@ -47,9 +53,6 @@ public interface IAudioPlayerService /// Поставить на паузу. Task PauseAsync(); - /// Остановить воспроизведение и выгрузить трек. - Task StopAsync(); - /// Перемотать на указанный процент (0–100). Task SeekToAsync(double percent); @@ -76,10 +79,7 @@ public interface IAudioPlayerService /// Запрос на паузу. event Func? OnPauseRequested; - /// Запрос на остановку и выгрузку трека. - event Func? OnStopRequested; - - /// Запрос на перемотку (процент 0–100). + /// Запрос на перемотку (секунды). event Func? OnSeekRequested; /// Запрос на изменение громкости (0–100). @@ -94,7 +94,7 @@ public interface IAudioPlayerService void SetCurrentTrack(string? trackId); /// Обновить прогресс и отображаемое время. - void UpdateProgress(double progress, string currentTime, string totalTime); + void UpdateProgress(double currentTime, double totalTime); /// Уведомить об окончании трека. void NotifyTrackEnded(); diff --git a/PlaylistShared.Pwa/wwwroot/js/AudioPlayer.js b/PlaylistShared.Pwa/wwwroot/js/AudioPlayer.js index 9202e24..015ece4 100644 --- a/PlaylistShared.Pwa/wwwroot/js/AudioPlayer.js +++ b/PlaylistShared.Pwa/wwwroot/js/AudioPlayer.js @@ -26,14 +26,13 @@ const stop = () => { audio.pause(); audio.currentTime = 0; }; const setVolume = (volume) => { audio.volume = toNumber(volume); }; const setCurrentTime = (time) => { audio.currentTime = toNumber(time); }; - const getDuration = () => durationReady ? durationValue : 0; - const getCurrentTime = () => toNumber(audio.currentTime); audio.addEventListener('loadedmetadata', () => { + const current = toNumber(audio.currentTime); durationValue = toNumber(audio.duration); durationReady = durationValue > 0; 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# - return { loadAndPlay, play, pause, stop, setVolume, setCurrentTime, getDuration, getCurrentTime }; + return { loadAndPlay, play, pause, stop, setVolume, setCurrentTime }; } \ No newline at end of file