From e0fca7e55e089f379446a214812cd49251ff0aaf Mon Sep 17 00:00:00 2001 From: FrigaT Date: Tue, 14 Apr 2026 18:59:00 +0300 Subject: [PATCH] =?UTF-8?q?=D0=92=D1=8B=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=20?= =?UTF-8?q?=D0=B3=D0=BB=D0=BE=D0=B1=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20?= =?UTF-8?q?=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 --- .../Common/TrackCoverWithPlay.razor | 31 ++- .../Components/Global/AudioPlayer.razor | 215 +++++++++--------- .../SharedPlaylist/AddTrackBySearch.razor | 20 +- .../SharedPlaylist/AddTrackSection.razor | 11 - .../SharedPlaylist/TracksTable.razor | 4 +- PlaylistShared.Pwa/Layout/MainLayout.razor | 5 + .../Pages/SharedPlaylistView.razor | 50 +--- PlaylistShared.Pwa/Program.cs | 1 + .../Services/AudioPlayerService.cs | 136 +++++++++++ .../Services/IAudioPlayerService.cs | 97 ++++++++ 10 files changed, 379 insertions(+), 191 deletions(-) create mode 100644 PlaylistShared.Pwa/Services/AudioPlayerService.cs create mode 100644 PlaylistShared.Pwa/Services/IAudioPlayerService.cs diff --git a/PlaylistShared.Pwa/Components/Common/TrackCoverWithPlay.razor b/PlaylistShared.Pwa/Components/Common/TrackCoverWithPlay.razor index 82d6a50..7f8c3f9 100644 --- a/PlaylistShared.Pwa/Components/Common/TrackCoverWithPlay.razor +++ b/PlaylistShared.Pwa/Components/Common/TrackCoverWithPlay.razor @@ -1,4 +1,5 @@ @using Microsoft.AspNetCore.Components.Web +@inject IAudioPlayerService AudioPlayerService
- @if (_isHovered || IsPlaying) + @if (_isHovered || IsCurrentTrackPlaying) {
- @@ -27,19 +28,39 @@ @code { [Parameter] public string CoverUrl { get; set; } = string.Empty; [Parameter] public string TrackId { get; set; } = string.Empty; - [Parameter] public bool IsPlaying { get; set; } = false; - [Parameter] public EventCallback OnPlay { get; set; } [Parameter] public int Height { get; set; } = 50; [Parameter] public int Width { get; set; } = 50; + [Parameter] public string SharedPlaylistId { get; set; } = string.Empty; + + private bool IsCurrentTrackPlaying => AudioPlayerService.IsPlaying && AudioPlayerService.CurrentTrackId == TrackId; private bool _isHovered; private void HandleMouseEnter() => _isHovered = true; private void HandleMouseLeave() => _isHovered = false; + protected override void OnInitialized() + { + AudioPlayerService.OnStateChanged += OnPlayerStateChanged; + } + private async Task OnPlayClick() { - await OnPlay.InvokeAsync(TrackId); + var sharedPlaylistId = string.IsNullOrWhiteSpace(SharedPlaylistId) ? null : SharedPlaylistId; + + if (IsCurrentTrackPlaying) + { + await AudioPlayerService.PauseAsync(); + } + else + { + await AudioPlayerService.LoadAndPlayAsync(TrackId, sharedPlaylistId: SharedPlaylistId); + } + } + + private void OnPlayerStateChanged() + { + InvokeAsync(StateHasChanged); } private string FormatCoverUrl(string? url) diff --git a/PlaylistShared.Pwa/Components/Global/AudioPlayer.razor b/PlaylistShared.Pwa/Components/Global/AudioPlayer.razor index 20ed141..76cae8d 100644 --- a/PlaylistShared.Pwa/Components/Global/AudioPlayer.razor +++ b/PlaylistShared.Pwa/Components/Global/AudioPlayer.razor @@ -1,6 +1,10 @@ @using Microsoft.JSInterop -@using Microsoft.AspNetCore.Components.Authorization -@namespace PlaylistShared.Pwa.Components +@inject IAudioPlayerService AudioPlayerService +@inject IJSRuntime JS +@inject TokenStorage TokenStorage +@inject PlayerStorage PlayerStorage +@inject AuthenticationStateProvider AuthProvider +@inject ISnackbar Snackbar @inject HttpClient Http @@ -56,63 +60,35 @@ private bool _isPlaying; private Timer? _progressTimer; private bool _isMuted; + private string? _currentAccessToken; + private string? _currentSharedPlaylistId; - [Inject] protected IJSRuntime JS { get; set; } = null!; - [Inject] private TokenStorage TokenStorage { get; set; } = null!; - [Inject] private PlayerStorage PlayerStorage { get; set; } = null!; - [Inject] private AuthenticationStateProvider AuthProvider { get; set; } = null!; - [Inject] private ISnackbar Snackbar { get; set; } = null!; - - /// Требовать ли авторизацию для воспроизведения (по умолчанию true). - [Parameter] public bool RequireAuth { get; set; } = true; - - /// ID расшаренного плейлиста. - [Parameter] public string SharedPlaylistId { get; set; } = string.Empty; - - /// Событие при завершении трека. - [Parameter] public EventCallback OnTrackEnded { get; set; } + protected override async Task OnInitializedAsync() + { + AudioPlayerService.OnLoadAndPlayRequested += OnLoadAndPlay; + AudioPlayerService.OnPlayRequested += OnPlay; + AudioPlayerService.OnPauseRequested += OnPause; + AudioPlayerService.OnStopRequested += OnStop; + AudioPlayerService.OnSeekRequested += OnSeek; + AudioPlayerService.OnVolumeChangeRequested += OnVolumeChange; + + await LoadSavedVolume(); + await AudioPlayerService.SetVolumeAsync(_currentVolume); // синхронизация + } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await EnsureAudioModuleAsync(); - await ChangeVolume(await PlayerStorage.GetVolumeAsync()); - StateHasChanged(); } } - [JSInvokable] - public async Task OnAudioEnded() + private async Task LoadSavedVolume() { - _isPlaying = false; - _currentProgress = 0; - StopProgressTimer(); - if (OnTrackEnded.HasDelegate) - await OnTrackEnded.InvokeAsync(); - StateHasChanged(); - } - - [JSInvokable] - public async Task OnTimeUpdate(double currentTime, double duration) - { - // Защита от некорректных значений - if (double.IsNaN(currentTime) || double.IsNaN(duration) || double.IsInfinity(currentTime) || double.IsInfinity(duration)) - return; - if (duration <= 0) return; - - _currentProgress = (currentTime / duration) * 100; - _currentTime = FormatTime(currentTime); - // Длительность не обновляем здесь - await InvokeAsync(StateHasChanged); - } - - [JSInvokable] - public async Task OnDurationReady(double duration) - { - if (double.IsNaN(duration) || double.IsInfinity(duration) || duration <= 0) return; - _totalTime = FormatTime(duration); - await InvokeAsync(StateHasChanged); + var savedVolume = await PlayerStorage.GetVolumeAsync(); + _currentVolume = savedVolume; + await AudioPlayerService.SetVolumeAsync(savedVolume); } private async Task EnsureAudioModuleAsync() @@ -123,14 +99,42 @@ _audioElement = await _audioModule.InvokeAsync("init", _audioId, DotNetObjectReference.Create(this)); } + [JSInvokable] + 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); + } + + [JSInvokable] + public async Task OnDurationReady(double duration) + { + if (duration <= 0) return; + _totalTime = FormatTime(duration); + await InvokeAsync(StateHasChanged); + } + private async Task CheckAuthAsync() { - if (!RequireAuth) return true; - var authState = await AuthProvider.GetAuthenticationStateAsync(); - var isAuthenticated = authState.User.Identity?.IsAuthenticated == true; - - if (!isAuthenticated) + if (!authState.User.Identity?.IsAuthenticated == true) { Snackbar.Add("Воспроизведение доступно только авторизованным пользователям", Severity.Warning); return false; @@ -138,32 +142,30 @@ return true; } - public async Task LoadAndPlayAsync(string trackId) + private async Task OnLoadAndPlay(string trackId, string? accessToken, string? sharedPlaylistId) { if (!await CheckAuthAsync()) return; - + var tokens = await TokenStorage.GetTokensAsync(); - var accessToken = tokens.token; - - if (string.IsNullOrWhiteSpace(accessToken) && string.IsNullOrWhiteSpace(SharedPlaylistId)) + _currentAccessToken = accessToken ?? tokens.token; + _currentSharedPlaylistId = sharedPlaylistId; + + if (string.IsNullOrWhiteSpace(_currentAccessToken) && string.IsNullOrWhiteSpace(_currentSharedPlaylistId)) { 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); + await _audioElement!.InvokeVoidAsync("loadAndPlay", streamUrl, _currentAccessToken, _currentSharedPlaylistId); _isPlaying = true; StartProgressTimer(); StateHasChanged(); } - public async Task PlayAsync() + private async Task OnPlay() { - if (!await CheckAuthAsync()) return; - if (_audioElement == null) return; await _audioElement.InvokeVoidAsync("play"); _isPlaying = true; @@ -171,7 +173,7 @@ StateHasChanged(); } - public async Task PauseAsync() + private async Task OnPause() { if (_audioElement == null) return; await _audioElement.InvokeVoidAsync("pause"); @@ -180,7 +182,7 @@ StateHasChanged(); } - public async Task StopAsync() + private async Task OnStop() { if (_audioElement == null) return; await _audioElement.InvokeVoidAsync("stop"); @@ -190,20 +192,7 @@ StateHasChanged(); } - private async Task TogglePlayPause() - { - if (_isPlaying) - await PauseAsync(); - else - await PlayAsync(); - } - - private async Task Stop() - { - await StopAsync(); - } - - private async Task SeekTo(double value) + private async Task OnSeek(double percent) { if (_audioElement == null) return; try @@ -211,71 +200,87 @@ var duration = await _audioElement.InvokeAsync("getDuration"); if (duration > 0 && !double.IsNaN(duration)) { - var newTime = (value / 100) * duration; + var newTime = (percent / 100) * duration; await _audioElement.InvokeVoidAsync("setCurrentTime", newTime); } } catch (Exception ex) { - Console.WriteLine($"SeekTo error: {ex.Message}"); + Console.WriteLine($"Seek error: {ex.Message}"); } } - private async Task ChangeVolume(double value) + private async Task OnVolumeChange(double volume) { if (_audioElement == null) return; try { - var volume = value / 100; - await _audioElement.InvokeVoidAsync("setVolume", volume); + await _audioElement.InvokeVoidAsync("setVolume", volume / 100); _isMuted = false; - _currentVolume = value; - await PlayerStorage.SetVolumeAsync(value); + _currentVolume = volume; + await PlayerStorage.SetVolumeAsync(volume); StateHasChanged(); } catch (Exception ex) { - Console.WriteLine($"ChangeVolume error: {ex.Message}"); + Console.WriteLine($"Volume change error: {ex.Message}"); } } + private async Task TogglePlayPause() + { + if (_isPlaying) + await AudioPlayerService.PauseAsync(); + else + await AudioPlayerService.PlayAsync(); + } + + private async Task Stop() + { + await AudioPlayerService.StopAsync(); + } + + 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 (_audioElement == null) return; _isMuted = !_isMuted; - var newVolume = _isMuted ? 0 : (_currentVolume / 100); - await _audioElement.InvokeVoidAsync("setVolume", newVolume); - StateHasChanged(); + var newVolume = _isMuted ? 0 : _currentVolume; + await AudioPlayerService.SetVolumeAsync(newVolume); } private void StartProgressTimer() { StopProgressTimer(); - _progressTimer = new Timer(async _ => - { - await UpdateProgress(); - }, null, 0, 500); + _progressTimer = new Timer(async _ => await UpdateProgress(), null, 0, 500); } private void StopProgressTimer() => _progressTimer?.Dispose(); private async Task UpdateProgress() { - if (_audioElement == null) - { - Console.WriteLine("UpdateProgress: _audioElement is null"); - return; - } + if (_audioElement == null) return; try { var current = await _audioElement.InvokeAsync("getCurrentTime"); var duration = await _audioElement.InvokeAsync("getDuration"); - if (duration > 0 && !double.IsNaN(duration) && !double.IsNaN(current)) { - _currentProgress = (current / duration) * 100; - _currentTime = FormatTime(current); - _totalTime = FormatTime(duration); + 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); } } diff --git a/PlaylistShared.Pwa/Components/SharedPlaylist/AddTrackBySearch.razor b/PlaylistShared.Pwa/Components/SharedPlaylist/AddTrackBySearch.razor index e45aec0..4632e25 100644 --- a/PlaylistShared.Pwa/Components/SharedPlaylist/AddTrackBySearch.razor +++ b/PlaylistShared.Pwa/Components/SharedPlaylist/AddTrackBySearch.razor @@ -37,9 +37,7 @@
+ Width="40" Height="40"/>
@track.Title @@ -67,23 +65,12 @@ @code { [Parameter] public EventCallback OnAddTrack { get; set; } - [Parameter] public EventCallback OnPlayTrack { get; set; } [Parameter] public string ShareToken { get; set; } = string.Empty; - [Parameter] public string? CurrentPlayingTrackId { get; set; } - [Parameter] public bool IsPlaying { get; set; } private string _searchQuery = ""; private List _searchResults = new(); private bool _isSearching; private HashSet _addingTrackIds = new(); - private string? _currentPlayingTrackId; - private bool _isPlaying; - - protected override void OnParametersSet() - { - _currentPlayingTrackId = CurrentPlayingTrackId; - _isPlaying = IsPlaying; - } private async Task SearchTracks() { @@ -133,11 +120,6 @@ } } - private async Task PlayTrack(string trackId) - { - await OnPlayTrack.InvokeAsync(trackId); - } - private string FormatDuration(long ms) { var seconds = ms / 1000; diff --git a/PlaylistShared.Pwa/Components/SharedPlaylist/AddTrackSection.razor b/PlaylistShared.Pwa/Components/SharedPlaylist/AddTrackSection.razor index 3f165b4..fcdca5c 100644 --- a/PlaylistShared.Pwa/Components/SharedPlaylist/AddTrackSection.razor +++ b/PlaylistShared.Pwa/Components/SharedPlaylist/AddTrackSection.razor @@ -31,9 +31,6 @@ @@ -46,9 +43,6 @@ [Parameter] public string ShareToken { get; set; } = string.Empty; [Parameter] public EventCallback OnTrackAdded { get; set; } - [Parameter] public EventCallback OnPlayTrack { get; set; } - [Parameter] public string? CurrentPlayingTrackId { get; set; } - [Parameter] public bool IsPlaying { get; set; } private async Task AddTrackByLink() { @@ -99,11 +93,6 @@ } } - private async Task PlayTrack(string trackId) - { - OnPlayTrack.InvokeAsync(trackId); - } - private string? ExtractTrackIdFromLink(string link) { var match = System.Text.RegularExpressions.Regex.Match(link, @"/track/(\d+)"); diff --git a/PlaylistShared.Pwa/Components/SharedPlaylist/TracksTable.razor b/PlaylistShared.Pwa/Components/SharedPlaylist/TracksTable.razor index 012f3f0..671cdc8 100644 --- a/PlaylistShared.Pwa/Components/SharedPlaylist/TracksTable.razor +++ b/PlaylistShared.Pwa/Components/SharedPlaylist/TracksTable.razor @@ -26,9 +26,7 @@ { + Width="50" Height="50"/> } else { diff --git a/PlaylistShared.Pwa/Layout/MainLayout.razor b/PlaylistShared.Pwa/Layout/MainLayout.razor index e4da386..cb70950 100644 --- a/PlaylistShared.Pwa/Layout/MainLayout.razor +++ b/PlaylistShared.Pwa/Layout/MainLayout.razor @@ -1,3 +1,4 @@ +@using PlaylistShared.Pwa.Components.Global @inherits LayoutComponentBase @@ -24,6 +25,10 @@ @Body + +
+ +
@code { diff --git a/PlaylistShared.Pwa/Pages/SharedPlaylistView.razor b/PlaylistShared.Pwa/Pages/SharedPlaylistView.razor index 89c0f76..799a19b 100644 --- a/PlaylistShared.Pwa/Pages/SharedPlaylistView.razor +++ b/PlaylistShared.Pwa/Pages/SharedPlaylistView.razor @@ -34,9 +34,7 @@ { + /> } @@ -50,16 +48,10 @@ CanPlay="@_canPlay" CanRemove="@_canRemove" CurrentPlayingTrackId="_currentTrackId" - IsPlaying="_isPlaying" - OnPlayTrack="PlayTrack" /> + /> } - - -
- -
@@ -68,11 +60,7 @@ private int _addTrackTabIndex = 0; // 0 - ссылка, 1 - поиск - private AudioPlayer? _audioPlayer; private TracksTable? _tracksTableRef; - private string? _currentTrackId { get; set; } - private bool _isPlaying = false; - private bool _isPlayerVisible = false; private SharedPlaylistDto? _playlist; private bool _loading = true; @@ -180,38 +168,4 @@ _tracksLoading = false; StateHasChanged(); } - - private async Task PlayTrack(string trackId) - { - if (_audioPlayer == null) return; - - if (_currentTrackId == trackId && _isPlaying) - { - await _audioPlayer.PauseAsync(); - _isPlaying = false; - } - else if (_currentTrackId == trackId && !_isPlaying) - { - await _audioPlayer.PlayAsync(); - _isPlaying = true; - } - else - { - if (!string.IsNullOrEmpty(_currentTrackId) && _isPlaying) - await _audioPlayer.StopAsync(); - - _currentTrackId = trackId; - await _audioPlayer.LoadAndPlayAsync(trackId); - _isPlaying = true; - } - - _isPlayerVisible = true; - } - - private async Task OnTrackEnded() - { - _currentTrackId = null; - _isPlaying = false; - StateHasChanged(); - } } \ No newline at end of file diff --git a/PlaylistShared.Pwa/Program.cs b/PlaylistShared.Pwa/Program.cs index dcfaa94..35c2246 100644 --- a/PlaylistShared.Pwa/Program.cs +++ b/PlaylistShared.Pwa/Program.cs @@ -25,6 +25,7 @@ internal class Program builder.Services.AddScoped(); builder.Services.AddScoped(sp => sp.GetRequiredService()); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddAuthorizationCore(); builder.Services.AddCascadingAuthenticationState(); diff --git a/PlaylistShared.Pwa/Services/AudioPlayerService.cs b/PlaylistShared.Pwa/Services/AudioPlayerService.cs new file mode 100644 index 0000000..5a6be9d --- /dev/null +++ b/PlaylistShared.Pwa/Services/AudioPlayerService.cs @@ -0,0 +1,136 @@ +using MudBlazor; + +namespace PlaylistShared.Pwa.Services; + +public class AudioPlayerService : IAudioPlayerService +{ + private readonly TokenStorage _tokenStorage; + private readonly ISnackbar _snackbar; + + private string? _currentTrackId; + private bool _isPlaying; + private double _currentVolume = 70; + private double _currentProgress; + private string _currentTime = "0:00"; + private string _totalTime = "0:00"; + + public string? CurrentTrackId => _currentTrackId; + public bool IsPlaying => _isPlaying; + public double CurrentVolume + { + get => _currentVolume; + set + { + _currentVolume = value; + OnStateChanged?.Invoke(); + } + } + public double CurrentProgress => _currentProgress; + public string CurrentTime => _currentTime; + public string TotalTime => _totalTime; + + public event Action? OnStateChanged; + + public AudioPlayerService(TokenStorage tokenStorage, ISnackbar snackbar) + { + _tokenStorage = tokenStorage; + _snackbar = snackbar; + } + + // Внешние команды (вызываются из компонентов) + public async Task LoadAndPlayAsync(string trackId, string? accessToken = null, string? sharedPlaylistId = null) + { + // Если accessToken не передан, пытаемся получить его из хранилища + if (string.IsNullOrWhiteSpace(accessToken)) + { + var tokens = await _tokenStorage.GetTokensAsync(); + accessToken = tokens.token; + } + + // Проверяем, есть ли чем авторизоваться + if (string.IsNullOrWhiteSpace(accessToken) && string.IsNullOrWhiteSpace(sharedPlaylistId)) + { + _snackbar.Add("Не удалось воспроизвести трек: отсутствует токен авторизации или идентификатор расшаренного плейлиста.", Severity.Error); + return; + } + + _currentTrackId = trackId; + _isPlaying = true; + OnStateChanged?.Invoke(); + OnLoadAndPlayRequested?.Invoke(trackId, accessToken, sharedPlaylistId); + } + + public async Task PlayAsync() + { + _isPlaying = true; + OnStateChanged?.Invoke(); + OnPlayRequested?.Invoke(); + } + + public async Task PauseAsync() + { + _isPlaying = false; + OnStateChanged?.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) + { + OnSeekRequested?.Invoke(percent); + } + + public async Task SetVolumeAsync(double volume) + { + _currentVolume = volume; + OnStateChanged?.Invoke(); + OnVolumeChangeRequested?.Invoke(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; + + // Внутренние методы для обновления состояния из AudioPlayer + public void SetPlayingState(bool isPlaying) + { + _isPlaying = isPlaying; + OnStateChanged?.Invoke(); + } + + public void SetCurrentTrack(string? trackId) + { + _currentTrackId = trackId; + OnStateChanged?.Invoke(); + } + + public void UpdateProgress(double progress, string currentTime, string totalTime) + { + _currentProgress = progress; + _currentTime = currentTime; + _totalTime = totalTime; + OnStateChanged?.Invoke(); + } + + public void NotifyTrackEnded() + { + _isPlaying = false; + _currentTrackId = null; + _currentProgress = 0; + _currentTime = "0:00"; + OnStateChanged?.Invoke(); + } +} \ No newline at end of file diff --git a/PlaylistShared.Pwa/Services/IAudioPlayerService.cs b/PlaylistShared.Pwa/Services/IAudioPlayerService.cs new file mode 100644 index 0000000..886f7e6 --- /dev/null +++ b/PlaylistShared.Pwa/Services/IAudioPlayerService.cs @@ -0,0 +1,97 @@ +namespace PlaylistShared.Pwa.Services; + +/// +/// Глобальный сервис управления аудиоплеером. +/// Позволяет управлять воспроизведением из любого компонента. +/// +public interface IAudioPlayerService +{ + // ---------- Состояние плеера (для чтения) ---------- + + /// ID текущего воспроизводимого трека (null, если ничего не играет). + string? CurrentTrackId { get; } + + /// Играет ли в данный момент (true) или приостановлен (false). + bool IsPlaying { get; } + + /// Текущая громкость (0–100). + double CurrentVolume { get; set; } + + /// Прогресс воспроизведения в процентах (0–100). + double CurrentProgress { get; } + + /// Отформатированное текущее время (мм:сс). + string CurrentTime { get; } + + /// Отформатированная общая длительность (мм:сс). + string TotalTime { get; } + + // ---------- Команды управления (вызываются из компонентов) ---------- + + /// Загрузить и начать воспроизведение трека. + /// ID трека. + /// Опциональный access-токен (если не указан, будет взят из хранилища). + /// ID расшаренного плейлиста (для неавторизованного доступа). + Task LoadAndPlayAsync(string trackId, string? accessToken = null, string? sharedPlaylistId = null); + + /// Воспроизвести (если трек загружен и на паузе). + Task PlayAsync(); + + /// Поставить на паузу. + Task PauseAsync(); + + /// Остановить воспроизведение и выгрузить трек. + Task StopAsync(); + + /// Перемотать на указанный процент (0–100). + Task SeekToAsync(double percent); + + /// Установить громкость (0–100). + Task SetVolumeAsync(double volume); + + // ---------- События для подписки на изменения состояния ---------- + + /// + /// Событие, возникающее при любом изменении состояния плеера: + /// смена трека, старт/пауза/стоп, обновление прогресса, изменение громкости, окончание трека. + /// Подписывайтесь на него, чтобы перерисовывать UI (например, иконку "пауза/плей"). + /// + event Action? OnStateChanged; + + // ---------- События для связи с реальным компонентом AudioPlayer ---------- + // (Эти события вызываются сервисом, а компонент AudioPlayer на них подписывается, + // чтобы выполнить фактические операции с HTML5 Audio.) + + /// Запрос на загрузку и воспроизведение трека. + event Func? OnLoadAndPlayRequested; + + /// Запрос на воспроизведение (снять с паузы). + event Func? OnPlayRequested; + + /// Запрос на паузу. + event Func? OnPauseRequested; + + /// Запрос на остановку и выгрузку трека. + event Func? OnStopRequested; + + /// Запрос на перемотку (процент 0–100). + event Func? OnSeekRequested; + + /// Запрос на изменение громкости (0–100). + event Func? OnVolumeChangeRequested; + + // ---------- Методы для обновления состояния из AudioPlayer ---------- + // (Вызываются компонентом AudioPlayer, когда реальный аудиоэлемент меняет своё состояние.) + + /// Уведомить сервис о том, что трек начал или прекратил играть. + void SetPlayingState(bool isPlaying); + + /// Установить ID текущего трека. + void SetCurrentTrack(string? trackId); + + /// Обновить прогресс и отображаемое время. + void UpdateProgress(double progress, string currentTime, string totalTime); + + /// Уведомить об окончании трека. + void NotifyTrackEnded(); +} \ No newline at end of file