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