diff --git a/PlaylistShared.Pwa/Components/SharedPlaylist/AddTrackSection.razor b/PlaylistShared.Pwa/Components/SharedPlaylist/AddTrackSection.razor deleted file mode 100644 index 7fbe6a4..0000000 --- a/PlaylistShared.Pwa/Components/SharedPlaylist/AddTrackSection.razor +++ /dev/null @@ -1,312 +0,0 @@ -@using PlaylistShared.Pwa.Components.Common -@using PlaylistShared.Pwa.Components.SharedPlaylist.Cards -@using PlaylistShared.Shared.DTO -@using PlaylistShared.Shared.Enums -@using PlaylistShared.Shared.SharedPlaylist -@using PlaylistShared.Shared.Yandex -@inject HttpClient Http -@inject ISnackbar Snackbar - - - - - - - - - - - - - - - - @if (_isSearching) - { - - } - else if (_searchResult != null) - { - - @* Секция исполнителей *@ - @if (_searchResult?.Artists != null) - { - - - @foreach (var artist in _searchResult.Artists) - { - - - - } - - - } - - @* Секция альбомов *@ - @if (_searchResult?.Albums != null) - { - - - @foreach (var album in _searchResult.Albums) - { - - - - } - - - } - - @* Секция плейлистов *@ - @if (_searchResult?.Playlists != null) - { - - - @foreach (var playlist in _searchResult.Playlists) - { - - - - } - - - } - - @* Секция треков *@ - @if (_searchResult?.Tracks != null) - { - - - - - - - - - - - - - } - - } - - - -@code { - [Parameter] public string ShareToken { get; set; } = string.Empty; - [Parameter] public EventCallback OnTrackAdded { get; set; } - [Parameter] public EventCallback OnTrackRemoved { get; set; } - [Parameter] public HashSet ExistingTrackIds { get; set; } = new(); - - private string _searchQuery = ""; - private bool _isSearching = false; - private TrackSearchType _searchType = TrackSearchType.All; - private YandexSearchResult? _searchResult = null; - - private async Task OnSearchQueryChanged() - { - await SearchTracks(byId: false); - } - - private async Task OnSearchTypeChanged() - { - await SearchTracks(byId: false); - } - - private async Task SearchTracks(bool byId = false, string? forcedQuery = null) - { - var query = forcedQuery ?? _searchQuery; - if (string.IsNullOrWhiteSpace(query)) - { - _searchResult = null; - return; - } - - var type = _searchType; - - // Распознавание ссылки Яндекс.Музыки - if (!byId && Uri.TryCreate(query, UriKind.Absolute, out var uri) && uri.Host == "music.yandex.ru") - { - try - { - (type, query) = ParseYandexMusicUrl(uri); - byId = true; - } - catch (Exception ex) - { - Snackbar.Add($"Ошибка распознавания URL: {ex.Message}", Severity.Error); - return; - } - } - - _isSearching = true; - _searchResult = null; - StateHasChanged(); - - try - { - var url = $"/api/yandexsearch/search?query={Uri.EscapeDataString(query)}&searchType={Uri.EscapeDataString(type.ToString())}&limit=20"; - if (byId) - url += "&byId=true"; - if (!string.IsNullOrEmpty(ShareToken)) - url += $"&shared_id={Uri.EscapeDataString(ShareToken)}"; - - var response = await Http.GetFromJsonAsync>(url); - if (response?.Success == true) - { - _searchResult = response.Data ?? new YandexSearchResult(); - } - else - { - Snackbar.Add(response?.Error?.Message ?? "Ошибка поиска", Severity.Error); - _searchResult = null; - } - } - catch (Exception ex) - { - Snackbar.Add($"Ошибка: {ex.Message}", Severity.Error); - _searchResult = null; - } - finally - { - _isSearching = false; - StateHasChanged(); - } - } - - private async Task SearchTracksByEntity(string entityId, string title, TrackSearchType entityType) - { - // Переключаем тип и ищем по ID - _searchType = entityType; - _searchQuery = title; - await SearchTracks(byId: true, forcedQuery: entityId); - } - - private async Task ToggleTrack(YandexTrack track) - { - if (ExistingTrackIds.Contains(track.TrackId)) - await RemoveTrack(track); - else - await AddTrack(track); - } - - private async Task RemoveTrack(YandexTrack track) - { - if (!ExistingTrackIds.Remove(track.TrackId)) return; - - try - { - await RemoveTrackById(track.TrackId); - await OnTrackRemoved.InvokeAsync(); - Snackbar.Add($"Трек \"{track.Title}\" удален", Severity.Success, c => c.SnackbarVariant = Variant.Outlined); - } - catch (Exception ex) - { - Snackbar.Add($"{ex.Message}", Severity.Error); - ExistingTrackIds.Add(track.TrackId); - } - finally - { - StateHasChanged(); - } - } - - private async Task RemoveTrackById(string trackId) - { - var request = new UpdateTrackListRequest { TrackIds = new List { trackId } }; - var response = await Http.PostAsJsonAsync($"/api/sharedplaylist/{ShareToken}/remove-tracks", request); - if (response.IsSuccessStatusCode) - { - await OnTrackAdded.InvokeAsync(); - } - else - { - var error = await response.Content.ReadFromJsonAsync>(); - throw new Exception(error?.Error?.Message ?? "Ошибка удаления трека"); - } - } - - private async Task AddTrack(YandexTrack track) - { - if (ExistingTrackIds.Contains(track.TrackId)) return; - ExistingTrackIds.Add(track.TrackId); - - try - { - await AddTrackById(track.TrackId); - await OnTrackAdded.InvokeAsync(); - Snackbar.Add($"Трек \"{track.Title}\" добавлен", Severity.Success, c => c.SnackbarVariant = Variant.Outlined); - } - catch (Exception ex) - { - Snackbar.Add($"{ex.Message}", Severity.Error); - ExistingTrackIds.Remove(track.TrackId); - } - finally - { - StateHasChanged(); - } - } - - private async Task AddTrackById(string trackId) - { - var request = new UpdateTrackListRequest { TrackIds = new List { trackId } }; - var response = await Http.PostAsJsonAsync($"/api/sharedplaylist/{ShareToken}/add-tracks", request); - if (response.IsSuccessStatusCode) - { - await OnTrackAdded.InvokeAsync(); - } - else - { - var error = await response.Content.ReadFromJsonAsync>(); - throw new Exception(error?.Error?.Message ?? "Ошибка добавления трека"); - } - } - - private static (TrackSearchType Type, string Id) ParseYandexMusicUrl(Uri uri) - { - var segments = uri.AbsolutePath.Split('/', StringSplitOptions.RemoveEmptyEntries); - - var dataMap = segments - .Select((val, idx) => new { val, idx }) - .GroupBy(x => x.idx / 2) - .ToDictionary( - g => g.First().val, - g => g.ElementAtOrDefault(1)?.val - ); - - if (dataMap.TryGetValue("track", out var trackId) && !string.IsNullOrEmpty(trackId)) - return (TrackSearchType.Track, trackId); - if (dataMap.TryGetValue("album", out var albumId) && !string.IsNullOrEmpty(albumId)) - return (TrackSearchType.Album, albumId); - if (dataMap.TryGetValue("playlist", out var playlistId) && !string.IsNullOrEmpty(playlistId)) - return (TrackSearchType.Playlist, playlistId); - if (dataMap.TryGetValue("artist", out var artistId) && !string.IsNullOrEmpty(artistId)) - return (TrackSearchType.Artist, artistId); - - throw new ArgumentException("Unsupported URL pattern"); - } -} \ No newline at end of file diff --git a/PlaylistShared.Pwa/Pages/SharedPlaylistView.razor b/PlaylistShared.Pwa/Pages/SharedPlaylistView.razor index e27f7d7..4085c81 100644 --- a/PlaylistShared.Pwa/Pages/SharedPlaylistView.razor +++ b/PlaylistShared.Pwa/Pages/SharedPlaylistView.razor @@ -3,6 +3,7 @@ @using PlaylistShared.Pwa.Components.Common @using PlaylistShared.Pwa.Components.SharedPlaylist +@using PlaylistShared.Pwa.Components.SharedPlaylist.Cards @using PlaylistShared.Shared.DTO @using PlaylistShared.Shared.Enums @using PlaylistShared.Pwa.Services @@ -27,7 +28,7 @@ { @* --- ВЕРСИЯ ДЛЯ ПК (сетка) --- *@ - + @PlaylistCardContent @@ -79,7 +80,26 @@ } + + @code { + /// Токен расшаренного плейлиста [Parameter] public string Token { get; set; } private RenderFragment PlaylistCardContent => __builder => @@ -97,13 +117,13 @@ - + @if (_canRemove) { - - + + } @@ -114,37 +134,159 @@ private RenderFragment AddTrackCardContent => __builder => { - - Добавление треков - - - - + + + + Добавление треков + + + + + + + + + + + + + + @if (_isSearching) + { + + } + else if (_searchResult != null) + { + @* Секция исполнителей *@ + @if (_searchResult?.Artists?.Any() == true) + { + Исполнители +
+ @foreach (var artist in _searchResult.Artists) + { +
+ +
+ } +
+ } + + @* Секция альбомов *@ + @if (_searchResult?.Albums?.Any() == true) + { + Альбомы +
+ @foreach (var album in _searchResult.Albums) + { +
+ +
+ } +
+ } + + @* Секция плейлистов *@ + @if (_searchResult?.Playlists?.Any() == true) + { + Плейлисты +
+ @foreach (var playlist in _searchResult.Playlists) + { +
+ +
+ } +
+ } + + @* Секция треков *@ + @if (_searchResult?.Tracks != null) + { + Треки + + + + + + + + + + + } + } +
+
}; + /// Активная вкладка для моб.версии (0 = плейлист, 1 = добавление треков) private int _activeMobileTab = 0; + /// + /// Список ID треков, уже добавленных в плейлист. + /// Используется для быстрого определения, добавлен трек или нет, при отображении результатов поиска. + /// private HashSet _existingTrackIds = new(); - private bool _firstLoadExistingTrackIds; + /// + /// Список добавленных в плейлист треков. + /// private List _tracks = new(); + /// Свойства плейлиста. private SharedPlaylistDto? _playlist; - private bool _loading = true; - private bool _isAuthenticated; + /// Пользователь является создателем плейлиста. private bool _isCreator; + /// Пользователь имеет право воспроизведения треков. private bool _canPlay; + /// Пользователь имеет право добавления треков в плейлист. private bool _canAdd; + /// Пользователь имеет право удаления треков из плейлиста. private bool _canRemove; - private UpdatePermissionsDto _editPermissions = new(); - private bool _savingPermissions; + /// Пользователь авторизован из под учетной записи. + private bool _isAuthenticated; + /// Текущий ID пользователя, если авторизован. private string? _currentUserId; - - private bool _isFavorite = false; - private bool _favoriteLoading = false; + + /// Обновление прав доступа к плейлисту, если создатель. + private UpdatePermissionsDto _editPermissions = new(); + /// Состояние: Происходит загрузка плейлиста. + private bool _loading = true; + /// Состояние: Происходит загрузка треков плейлиста. private bool _tracksLoading; - private string _trackLink = ""; - private bool _addingTrack; + /******************************** + * Вкладка добавления треков + *********************************/ + /// Строка запроса поиска. + private string _searchQuery = ""; + /// Тип поиска. + private TrackSearchType _searchType = TrackSearchType.All; + /// Состояние: Происходит поиск. + private bool _isSearching = false; + + /// Результат поиска. + private YandexSearchResult? _searchResult = null; protected override async Task OnInitializedAsync() { @@ -155,6 +297,7 @@ await LoadTracks(); } + /// Установка разрешений. private async Task ConfigurePermissions() { if (_playlist is null) @@ -193,7 +336,8 @@ } } - + + /// Загрузка плейлиста. private async Task LoadPlaylist() { try @@ -221,7 +365,8 @@ StateHasChanged(); } } - + + /// Загрузка треков. private async Task LoadTracks() { if (_playlist == null) return; @@ -234,7 +379,7 @@ if (response?.Success == true && response.Data != null) { _tracks = response.Data.Tracks; - _existingTrackIds = _tracks.Select(t => t.TrackId).ToHashSet(); + await GenerateUniqTrackIds(); } else { @@ -253,7 +398,13 @@ } } - private async Task RemoveTrack(YandexTrack track) + private async Task GenerateUniqTrackIds() + { + _existingTrackIds = _tracks.Select(t => t.TrackId).ToHashSet(); + } + + /// Удаление трека из списка плейлиста + private async Task OnRemoveTrack(YandexTrack track) { var confirmed = await DialogService.ShowMessageBoxAsync( "Подтверждение удаления", @@ -262,6 +413,46 @@ if (confirmed != true) return; + await RemoveTrack(track); + } + + + + #region Добавление/удаление трека + /// Добавление трека. + private async Task AddTrack(YandexTrack track) + { + if (_existingTrackIds.Contains(track.TrackId)) return; + + try + { + var request = new UpdateTrackListRequest { TrackIds = new List { track.TrackId } }; + var response = await Http.PostAsJsonAsync($"/api/sharedplaylist/{Token}/add-tracks", request); + if (response.IsSuccessStatusCode) + { + Snackbar.Add($"Трек \"{track.Title}\" добавлен", Severity.Success, c => c.SnackbarVariant = Variant.Outlined); + _tracks.Add(track); + await GenerateUniqTrackIds(); + } + else + { + var error = await response.Content.ReadFromJsonAsync>(); + Snackbar.Add(error?.Error?.Message ?? "Ошибка добавления трека", Severity.Error); + } + + } + catch (Exception ex) + { + Snackbar.Add($"Ошибка: {ex.Message}", Severity.Error); + } + StateHasChanged(); + } + + /// Удаление трека. + private async Task RemoveTrack(YandexTrack track) + { + if (!_existingTrackIds.Contains(track.TrackId)) return; + try { var request = new UpdateTrackListRequest { TrackIds = new List { track.TrackId } }; @@ -269,7 +460,8 @@ if (response.IsSuccessStatusCode) { Snackbar.Add("Трек удалён", Severity.Success); - await LoadTracks(); + _tracks.Remove(track); + await GenerateUniqTrackIds(); } else { @@ -281,5 +473,129 @@ { Snackbar.Add($"Ошибка: {ex.Message}", Severity.Error); } + StateHasChanged(); } + #endregion + + #region Вкладка Добавление треков + private async Task OnSearchQueryChanged() + { + await SearchTracks(byId: false); + } + + private async Task OnSearchTypeChanged() + { + await SearchTracks(byId: false); + } + + /// + /// Поиск трека + /// + /// По ID + /// Принудительный запрос + /// + private async Task SearchTracks(bool byId = false, string? forcedQuery = null) + { + var query = forcedQuery ?? _searchQuery; + if (string.IsNullOrWhiteSpace(query)) + { + _searchResult = null; + return; + } + + var type = _searchType; + + // Распознавание ссылки Яндекс.Музыки + if (!byId && Uri.TryCreate(query, UriKind.Absolute, out var uri) && uri.Host == "music.yandex.ru") + { + try + { + (type, query) = ParseYandexMusicUrl(uri); + byId = true; + } + catch (Exception ex) + { + Snackbar.Add($"Ошибка распознавания URL: {ex.Message}", Severity.Error); + return; + } + } + + _isSearching = true; + _searchResult = null; + StateHasChanged(); + + try + { + var url = $"/api/yandexsearch/search?query={Uri.EscapeDataString(query)}&searchType={Uri.EscapeDataString(type.ToString())}&limit=20"; + if (byId) + url += "&byId=true"; + if (!string.IsNullOrEmpty(Token)) + url += $"&shared_id={Uri.EscapeDataString(Token)}"; + + var response = await Http.GetFromJsonAsync>(url); + if (response?.Success == true) + { + _searchResult = response.Data ?? new YandexSearchResult(); + } + else + { + Snackbar.Add(response?.Error?.Message ?? "Ошибка поиска", Severity.Error); + _searchResult = null; + } + } + catch (Exception ex) + { + Snackbar.Add($"Ошибка: {ex.Message}", Severity.Error); + _searchResult = null; + } + finally + { + _isSearching = false; + StateHasChanged(); + } + } + + /// Поиск по определенному типу результата. + private async Task SearchTracksByEntity(string entityId, string title, TrackSearchType entityType) + { + // Переключаем тип и ищем по ID + _searchType = entityType; + _searchQuery = title; + await SearchTracks(byId: true, forcedQuery: entityId); + } + + /// Переключатель "Добавление/Удаление" трека + private async Task ToggleTrack(YandexTrack track) + { + if (_existingTrackIds.Contains(track.TrackId)) + await RemoveTrack(track); + else + await AddTrack(track); + } + + /// Парсинг ссылки на яндекс трек + private static (TrackSearchType Type, string Id) ParseYandexMusicUrl(Uri uri) + { + var segments = uri.AbsolutePath.Split('/', StringSplitOptions.RemoveEmptyEntries); + + var dataMap = segments + .Select((val, idx) => new { val, idx }) + .GroupBy(x => x.idx / 2) + .ToDictionary( + g => g.First().val, + g => g.ElementAtOrDefault(1)?.val + ); + + if (dataMap.TryGetValue("track", out var trackId) && !string.IsNullOrEmpty(trackId)) + return (TrackSearchType.Track, trackId); + if (dataMap.TryGetValue("album", out var albumId) && !string.IsNullOrEmpty(albumId)) + return (TrackSearchType.Album, albumId); + if (dataMap.TryGetValue("playlist", out var playlistId) && !string.IsNullOrEmpty(playlistId)) + return (TrackSearchType.Playlist, playlistId); + if (dataMap.TryGetValue("artist", out var artistId) && !string.IsNullOrEmpty(artistId)) + return (TrackSearchType.Artist, artistId); + + throw new ArgumentException("Unsupported URL pattern"); + } + #endregion } \ No newline at end of file