@page "/shared/{token}" @_playlist?.Title - Playlist Share @using PlaylistShared.Pwa.Components.Common @using PlaylistShared.Pwa.Components.Global @using PlaylistShared.Pwa.Components.SharedPlaylist @using PlaylistShared.Pwa.Components.SharedPlaylist.Cards @using PlaylistShared.Shared.DTO @using PlaylistShared.Shared.Enums @using PlaylistShared.Pwa.Services @using PlaylistShared.Shared.Profile @using PlaylistShared.Shared.SharedPlaylist @using PlaylistShared.Shared.Yandex @inject HttpClient Http @inject ISnackbar Snackbar @inject NavigationManager Navigation @inject AuthenticationStateProvider AuthProvider @inject IDialogService DialogService @inject IAudioPlayerService AudioPlayerService @inject IJSRuntime JS @implements IDisposable @*Первый элемент растянется на всю высоту*@ @* --- ВЕРСИЯ ДЛЯ ПК (сетка) --- *@ @PlaylistCardHeaderContent @if (_isCreator && _isAuthenticated) { } @PlaylistCardBodyContent @if (_canAdd) { @AddTrackCardContent } @* --- ВЕРСИЯ ДЛЯ МОБИЛОК (вкладки внизу) --- *@
@* Область контента: оба компонента здесь всегда *@
@PlaylistCardHeaderContent @PlaylistCardBodyContent
@if (_canAdd) {
@AddTrackCardContent
}
@* Кастомная панель навигации внизу *@ @if (_isCreator && _isAuthenticated) { }
@*Второй элемент - плеер. Привязан к нижней части контейнера*@
@code { /// Токен расшаренного плейлиста [Parameter] public required string Token { get; set; } /// Элемент: заголовок плейлиста private RenderFragment PlaylistCardHeaderContent => __builder => { @if (_loading) { } else { @if (!string.IsNullOrEmpty(_playlist?.CoverUrl)) { } @_playlist?.Title } }; /// Элемент: треки плейлиста private RenderFragment PlaylistCardBodyContent => __builder => { @if (_loading || _tracksLoading) { } else { @if (_canRemove) { } } }; /// Элемент: блок добавления треков private RenderFragment AddTrackCardContent => __builder => { Добавление треков
@if (_isSearching) { @if (_searchType != TrackSearchType.Track || _searchType == TrackSearchType.All) { } @if (_searchType == TrackSearchType.All || _searchType == TrackSearchType.Track) { } } 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?.PersonalPlaylists?.Any() == true) { Плейлисты (персональные)
@foreach (var playlist in _searchResult.PersonalPlaylists) {
}
} @* Секция лайкнутых плейлистов *@ @if (_searchResult?.LikedPlaylists?.Any() == true) { Плейлисты (лайки)
@foreach (var playlist in _searchResult.LikedPlaylists) {
}
} @* Секция треков *@ @if (_searchResult?.Tracks != null) { Треки @if (_canAdd) { } } }
}; /// Активная вкладка для моб.версии (0 = плейлист, 1 = добавление треков) private int _activeMobileTab = 0; /// /// Список ID треков, уже добавленных в плейлист. /// Используется для быстрого определения, добавлен трек или нет, при отображении результатов поиска. /// private HashSet _existingTrackIds = new(); /// /// Список добавленных в плейлист треков. /// private List _tracks = new(); /// Свойства плейлиста. private SharedPlaylistDto? _playlist; /// Пользователь является создателем плейлиста. private bool _isCreator; /// Пользователь имеет право воспроизведения треков. private bool _canPlay; /// Пользователь имеет право добавления треков в плейлист. private bool _canAdd; /// Пользователь имеет право удаления треков из плейлиста. private bool _canRemove; /// Пользователь авторизован из под учетной записи. private bool _isAuthenticated; /// Текущий ID пользователя, если авторизован. private string? _currentUserId; /// Состояние: Происходит загрузка плейлиста. private bool _loading = true; /// Состояние: Происходит загрузка треков плейлиста. private bool _tracksLoading = true; /******************************** * Вкладка добавления треков *********************************/ /// Строка запроса поиска. private string _searchQuery = ""; /// Тип поиска. private TrackSearchType _searchType = TrackSearchType.All; /// Состояние: Происходит поиск. private bool _isSearching = false; /// Ссылка на поле ввода private MudTextField _searchField; /// Результат поиска. private YandexSearchResult? _searchResult = null; /******************************** * Вкладка добавления треков *********************************/ /// Признак, что альбом в фаворитах. private bool _isFavorite; /// Загрузка признака "фаворит". private bool _favoriteLoading; /******************************** * Поделиться ссылкой *********************************/ private IJSObjectReference? _shareModule; private bool _isWebShareSupported = false; protected override async Task OnInitializedAsync() { var authState = await AuthProvider.GetAuthenticationStateAsync(); _isAuthenticated = authState.User.Identity?.IsAuthenticated == true; _currentUserId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; await LoadPlaylist(); await LoadTracks(); AudioPlayerService.OnStartedTrack += OnPlayerStateChanged; AudioPlayerService.OnEndedTrack += OnPlayerStateChanged; } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { // Загружаем JS-модуль _shareModule = await JS.InvokeAsync("import", "/js/shareUtils.js"); // Проверяем поддержку Web Share API _isWebShareSupported = await _shareModule.InvokeAsync("isSupported"); StateHasChanged(); } } private void OnPlayerStateChanged() { InvokeAsync(StateHasChanged); } /// Установка разрешений. private async Task ConfigurePermissions() { if (_playlist is null) { _isCreator = false; _canAdd = false; _canRemove = false; _canPlay = false; } else { _isCreator = _playlist.CreatorUserId.ToString() == _currentUserId; _canAdd = _isCreator || _playlist.AddPermission == EditPermission.Everyone || (_playlist.AddPermission == EditPermission.AuthorizedOnly && _isAuthenticated); _canRemove = _isCreator || _playlist.RemovePermission == EditPermission.Everyone || (_playlist.RemovePermission == EditPermission.AuthorizedOnly && _isAuthenticated); _canPlay = _isCreator || _playlist.PlayPermission == ViewPermission.Everyone || (_playlist.PlayPermission == ViewPermission.AuthorizedOnly && _isAuthenticated); } } /// Загрузка плейлиста. private async Task LoadPlaylist() { try { var response = await Http.GetFromJsonAsync>($"/api/sharedplaylist/{Token}"); if (response?.Success == true) { _playlist = response.Data; _activeMobileTab = 0; await ConfigurePermissions(); await CheckFavoriteStatus(); } else { Snackbar.Add(response?.Error?.Message ?? "Не удалось загрузить плейлист", Severity.Error); } } catch (Exception ex) { Snackbar.Add($"Ошибка: {ex.Message}", Severity.Error); } finally { _loading = false; StateHasChanged(); } } /// Загрузка треков. private async Task LoadTracks() { if (_playlist == null) return; _tracksLoading = true; try { var response = await Http.GetFromJsonAsync>($"/api/sharedplaylist/{Token}/tracks"); if (response?.Success == true && response.Data != null) { _tracks = response.Data.Tracks; await GenerateUniqTrackIds(); } else { Snackbar.Add(response?.Error?.Message ?? "Не удалось загрузить треки", Severity.Error); } } catch (Exception ex) { Snackbar.Add($"Ошибка загрузки треков: {ex.Message}", Severity.Error); } finally { _tracksLoading = false; StateHasChanged(); } } private async Task GenerateUniqTrackIds() { _existingTrackIds = _tracks.Select(t => t.TrackId).ToHashSet(); } /// Удаление трека из списка плейлиста private async Task OnRemoveTrack(YandexTrack track) { var confirmed = await DialogService.ShowMessageBoxAsync( "Подтверждение удаления", $"Вы уверены, что хотите удалить трек \"{track.Title}\"?", yesText: "Удалить", cancelText: "Отмена"); 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.Insert(0, 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 } }; var response = await Http.PostAsJsonAsync($"/api/sharedplaylist/{Token}/remove-tracks", request); if (response.IsSuccessStatusCode) { Snackbar.Add("Трек удалён", Severity.Success); _tracks.Remove(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(); } #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; var type = _searchType; //Если поиск в моих плейлистах if (type == TrackSearchType.MyPlaylists) { var showMessage = true; if (_isAuthenticated) { var response = await Http.GetFromJsonAsync>("/api/yandexaccount/status"); if (response?.Success == true) { var hasToken = response?.Data?.HasToken ?? false; if (hasToken) showMessage = false; } } if (showMessage) { var response = await DialogService.ShowMessageBoxAsync(new() { Title = "Необходимо авторизация", Message = "Для использования \"Мои плейлисты\" необходима авторизация в яндекс музыке.", YesText = "Авторизоваться", CancelText = "Отмена", }); if (response == true) { Navigation.NavigateTo("/login"); } return; } } //Если обычный поиск, нужен текст else if (string.IsNullOrWhiteSpace(query)) { _searchResult = null; return; } // Распознавание ссылки Яндекс.Музыки if (!byId && Uri.TryCreate(query, UriKind.Absolute, out var uri) && uri.Host == "music.yandex.ru") { try { (type, query) = ParseYandexMusicUrl(uri); byId = true; _searchQuery = string.Empty; await _searchField.SetTextAsync(string.Empty); } 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())}"; 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 #region Избранное / Фаворит /// Установка галочки "избранное" private async Task CheckFavoriteStatus() { if (string.IsNullOrWhiteSpace(Token)) return; try { var response = await Http.GetFromJsonAsync>($"/api/favorites/{Token}/check"); if (response?.Success == true) _isFavorite = response.Data; } catch { } } private async Task ToggleFavorite() { if (string.IsNullOrWhiteSpace(Token)) return; if (!_isAuthenticated) { Snackbar.Add("Добавление в избранное только авторизованным пользователям", Severity.Warning); return; } _favoriteLoading = true; try { if (_isFavorite) { var response = await Http.DeleteAsync($"/api/favorites/{Token}"); if (response.IsSuccessStatusCode) { _isFavorite = false; Snackbar.Add("Плейлист удалён из избранного", Severity.Success); } else { Snackbar.Add("Ошибка удаления из избранного", Severity.Error); } } else { var response = await Http.PostAsync($"/api/favorites/{Token}", null); if (response.IsSuccessStatusCode) { _isFavorite = true; Snackbar.Add("Плейлист добавлен в избранное", Severity.Success); } else { Snackbar.Add("Ошибка добавления в избранное", Severity.Error); } } } catch (Exception ex) { Snackbar.Add($"Ошибка: {ex.Message}", Severity.Error); } finally { _favoriteLoading = false; StateHasChanged(); } } #endregion #region Настройка доступов к плейлисту private async Task OpenPermissionsDialog() { if (_playlist == null) return; var initialPermissions = new UpdatePermissionsDto { ViewPermission = _playlist.ViewPermission, PlayPermission = _playlist.PlayPermission, AddPermission = _playlist.AddPermission, RemovePermission = _playlist.RemovePermission }; var parameters = new DialogParameters { { nameof(PermissionsDialog.ShareToken), Token }, { nameof(PermissionsDialog.InitialPermissions), initialPermissions } }; var dialog = await DialogService.ShowAsync("Настройки доступа", parameters); var result = await dialog.Result; } #endregion #region Поделитьсы ссылкой /// Поделиться ссылкой private async Task SharePlaylist() { if (_shareModule == null) return; var shareUrl = Navigation.Uri; var shareTitle = "🎵 Поделиться плейлистом"; var shareText = _playlist?.Title != null ? $"Послушайте плейлист '{_playlist.Title}'!" : "Послушайте этот плейлист!"; if (_isWebShareSupported) { var result = await _shareModule.InvokeAsync("shareLink", shareTitle, shareText, shareUrl); if (result?.Success == false && !string.IsNullOrEmpty(result.Error) && !result.Cancelled) { Snackbar.Add($"Не удалось поделиться: {result.Error}", Severity.Warning); await ShowShareDialog(shareUrl); } } else { await ShowShareDialog(shareUrl); } } /// Модальное окно, чтобы поделиться ссылкой private async Task ShowShareDialog(string url) { var parameters = new DialogParameters { { x => x.ShareUrl, url } }; var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true }; await DialogService.ShowAsync("Поделиться", parameters, options); } // Вспомогательный класс для результата private class ShareResult { public bool Success { get; set; } public string? Error { get; set; } public bool Cancelled { get; set; } } #endregion public void Dispose() { AudioPlayerService.OnStartedTrack -= OnPlayerStateChanged; AudioPlayerService.OnEndedTrack -= OnPlayerStateChanged; _shareModule?.DisposeAsync(); } }