Files
PlaylistShared/PlaylistShared.Pwa/Pages/SharedPlaylistView.razor

510 lines
20 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@page "/shared/{token}"
@using PlaylistShared.Shared.DTO
@using PlaylistShared.Shared.Enums
@using PlaylistShared.Pwa.Services
@using PlaylistShared.Shared.Shared
@inject HttpClient Http
@inject ISnackbar Snackbar
@inject NavigationManager Navigation
@inject AuthenticationStateProvider AuthProvider
@inject IDialogService DialogService
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
@if (_loading)
{
<MudProgressCircular Indeterminate />
}
else if (_playlist == null)
{
<MudAlert Severity="Severity.Error">Плейлист не найден или у вас нет доступа</MudAlert>
}
else
{
<MudCard>
<!-- Заголовок с обложкой -->
<MudCardHeader>
<CardHeaderContent>
<div style="display: flex; gap: 16px; align-items: center;">
@if (!string.IsNullOrEmpty(_playlist.CoverUrl))
{
<MudImage Src="@FormatCoverUrl(_playlist.CoverUrl, "80x80")" Height="80" Width="80" Class="rounded" />
}
<div>
<div style="display: flex; align-items: center; gap: 8px;">
<MudLink Href="@($"https://music.yandex.ru/playlists/{_playlist.YandexPlaylistUuid}")" Typo="Typo.h5" Target="_blank" Underline="Underline.Hover">
@_playlist.Title
<MudIcon Icon="@Icons.Material.Filled.OpenInNew" Size="Size.Small" Class="ml-1" />
</MudLink>
<ShareButton />
<MudIconButton Icon="@(_isFavorite? Icons.Material.Filled.Star : Icons.Material.Outlined.StarBorder)"
Color="Color.Warning"
OnClick="ToggleFavorite"
Disabled="_favoriteLoading"
Size="Size.Medium" />
@if (_isCreator && _isAuthenticated)
{
<MudIconButton Icon="@Icons.Material.Filled.Settings"
Color="Color.Default"
OnClick="OpenPermissionsDialog"
Title="Настройки доступа"
Size="Size.Medium" />
}
</div>
<MudText Typo="Typo.body2" Color="Color.Secondary">Владелец: @_playlist.Creator?.UserName</MudText>
</div>
</div>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<!-- Блок добавления трека (только для авторизованных с правом добавления) -->
@if (_canAdd)
{
<MudPaper Class="pa-4 mb-4" Elevation="0" Style="background-color: rgba(0,0,0,0.05); border-radius: 8px;">
<MudText Typo="Typo.h6" GutterBottom>Добавить трек</MudText>
<MudGrid>
<MudItem xs="12" sm="10">
<MudTextField @bind-Value="_trackLink" Label="Ссылка на трек Яндекс.Музыки"
Variant="Variant.Outlined" FullWidth="true"
Placeholder="https://music.yandex.ru/album/2488464/track/21696942" />
</MudItem>
<MudItem xs="12" sm="2">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="AddTrack"
Disabled="_addingTrack" FullWidth="true" Style="height: 100%;">
@if (_addingTrack)
{
<MudProgressCircular Size="Size.Small" Indeterminate />
}
else
{
<span>Добавить</span>
}
</MudButton>
</MudItem>
</MudGrid>
<MudText Typo="Typo.body2" Class="mt-2" Color="Color.Secondary">
Поддерживаются ссылки вида: https://music.yandex.ru/album/12345/track/67890
</MudText>
</MudPaper>
}
<!-- Список треков -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<MudText Typo="Typo.h6">Треки</MudText>
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="LoadTracks" Disabled="_tracksLoading" Size="Size.Medium" />
</div>
@if (_tracksLoading)
{
<MudProgressCircular Indeterminate />
}
else if (_tracks == null || !_tracks.Any())
{
<MudAlert Severity="Severity.Info">В плейлисте пока нет треков</MudAlert>
}
else
{
<MudTable Items="@_tracks">
<HeaderContent>
<MudTh>#</MudTh>
<MudTh>Обложка</MudTh>
<MudTh>Название</MudTh>
<MudTh>Исполнитель</MudTh>
<MudTh>Длительность</MudTh>
@if (_canRemove)
{
<MudTh></MudTh>
}
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="#" Style="font-weight: normal;">@context.Index</MudTd>
<MudTd DataLabel="Обложка">
@if (!string.IsNullOrEmpty(context.CoverUri))
{
@if (@_canPlay)
{
<TrackCoverWithPlay CoverUrl="@context.CoverUri"
TrackId="@context.Id"
Width="50" Height="50"
IsPlaying="@(_currentTrackId == context.Id && _isPlaying)"
OnPlay="PlayTrack" />
}
else
{
<MudImage Src="@FormatCoverUrl(context.CoverUri, "50x50")" Height="50" Width="50" Class="rounded" Style="display: block;" />
}
}
</MudTd>
<MudTd DataLabel="Название">
<MudLink Href="@($"https://music.yandex.ru/track/{context.Id}")" Target="_blank" Underline="Underline.Hover">
@context.Title
<MudIcon Icon="@Icons.Material.Filled.OpenInNew" Size="Size.Small" Class="ml-1" />
</MudLink>
</MudTd>
<MudTd DataLabel="Исполнитель">@string.Join(", ", context.Artists)</MudTd>
<MudTd DataLabel="Длительность">@FormatDuration(context.DurationMs)</MudTd>
@if (_canRemove)
{
<MudTd DataLabel="">
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="() => RemoveTrack(context)" />
</MudTd>
}
</RowTemplate>
</MudTable>
}
</MudCardContent>
</MudCard>
}
<!-- Фиксированный плеер внизу -->
<div class="fixed-player" style="display: @(_isPlayerVisible ? "block" : "none");">
<AudioPlayer @ref="_audioPlayer" OnTrackEnded="OnTrackEnded" RequireAuth="false" SharedPlaylistId="@Token"/>
</div>
</MudContainer>
@code {
[Parameter] public string Token { get; set; }
private AudioPlayer? _audioPlayer;
private string? _currentTrackId { get; set; }
private bool _isPlaying = false;
private bool _isPlayerVisible = false;
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 string? _currentUserId;
private bool _isFavorite = false;
private bool _favoriteLoading = false;
private List<YandexTrackDisplay> _tracks = new();
private bool _tracksLoading;
private string _trackLink = "";
private bool _addingTrack;
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();
}
private async Task CheckFavoriteStatus()
{
if (!_isAuthenticated || _playlist == null) return;
try
{
var response = await Http.GetFromJsonAsync<ApiResponse<bool>>($"/api/favorites/{Token}/check");
if (response?.Success == true)
_isFavorite = response.Data;
}
catch { }
}
private async Task ToggleFavorite()
{
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();
}
}
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);
if (_isCreator && _isAuthenticated)
{
_editPermissions = new UpdatePermissionsDto
{
ViewPermission = _playlist.ViewPermission,
AddPermission = _playlist.AddPermission,
RemovePermission = _playlist.RemovePermission,
PlayPermission = _playlist.PlayPermission,
};
}
}
}
private async Task LoadPlaylist()
{
try
{
var response = await Http.GetFromJsonAsync<ApiResponse<SharedPlaylistDto>>($"/api/sharedplaylist/{Token}");
if (response?.Success == true)
{
_playlist = response.Data;
await ConfigurePermissions();
await LoadTracks();
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 url = $"/api/sharedplaylist/{Token}/tracks";
var response = await Http.GetFromJsonAsync<ApiResponse<YandexPlaylistData>>(url);
if (response?.Success == true && response.Data != null)
{
_tracks = response.Data.Tracks.Select((t, idx) => new YandexTrackDisplay
{
Id = t.Id,
Title = t.Title,
Artists = t.Artists,
DurationMs = t.DurationMs,
CoverUri = t.CoverUri,
Index = idx + 1
}).ToList();
}
else
{
Snackbar.Add(response?.Error?.Message ?? "Не удалось загрузить треки", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"Ошибка загрузки треков: {ex.Message}", Severity.Error);
}
finally
{
_tracksLoading = false;
StateHasChanged();
}
}
private async Task AddTrack()
{
if (string.IsNullOrWhiteSpace(_trackLink))
{
Snackbar.Add("Введите ссылку на трек", Severity.Warning);
return;
}
_addingTrack = true;
try
{
var request = new AddTrackByLinkRequest { Link = _trackLink };
var response = await Http.PostAsJsonAsync($"/api/sharedplaylist/{Token}/add-track-by-link", request);
if (response.IsSuccessStatusCode)
{
Snackbar.Add("Трек успешно добавлен", Severity.Success);
_trackLink = "";
await LoadTracks();
}
else
{
var error = await response.Content.ReadFromJsonAsync<ApiResponse<object>>();
Snackbar.Add(error?.Error?.Message ?? "Ошибка добавления трека", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"Ошибка: {ex.Message}", Severity.Error);
}
finally
{
_addingTrack = false;
}
}
private async Task RemoveTrack(YandexTrackDisplay track)
{
var confirmed = await DialogService.ShowMessageBoxAsync(
"Подтверждение удаления",
$"Вы уверены, что хотите удалить трек \"{track.Title}\"?",
yesText: "Удалить", cancelText: "Отмена");
if (confirmed != true) return;
try
{
var request = new RemoveTracksRequest { TrackIds = new List<string> { track.Id } };
var response = await Http.PostAsJsonAsync($"/api/sharedplaylist/{Token}/remove-tracks", request);
if (response.IsSuccessStatusCode)
{
Snackbar.Add("Трек удалён", Severity.Success);
await LoadTracks();
}
else
{
var error = await response.Content.ReadFromJsonAsync<ApiResponse<object>>();
Snackbar.Add(error?.Error?.Message ?? "Ошибка удаления", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"Ошибка: {ex.Message}", Severity.Error);
}
}
private async Task OpenPermissionsDialog()
{
var parameters = new DialogParameters
{
{ nameof(PermissionsDialog.ShareToken), Token },
{ nameof(PermissionsDialog.InitialPermissions), _editPermissions }
};
var dialog = await DialogService.ShowAsync<PermissionsDialog>("Настройки доступа", parameters);
var result = await dialog.Result;
if (!result.Canceled && result.Data is UpdatePermissionsDto updatedPermissions)
{
// Обновляем локальные права и перезагружаем плейлист
_editPermissions = updatedPermissions;
await LoadPlaylist(); // перезагружаем, чтобы обновить _playlist и права доступа
await LoadTracks(); // возможно, треки тоже нужно перезагрузить
StateHasChanged();
}
}
private string FormatDuration(int ms)
{
var seconds = ms / 1000;
var mins = seconds / 60;
var secs = seconds % 60;
return $"{mins}:{secs:D2}";
}
private string FormatCoverUrl(string? url, string size = "200x200")
{
if (string.IsNullOrEmpty(url)) return "";
return "https://" + url.Replace("%%", size);
}
private class YandexTrackDisplay : YandexTrack
{
public int Index { get; set; }
}
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();
}
}