Изменено отображение треков

This commit is contained in:
FrigaT
2026-04-15 15:56:43 +03:00
parent e00b7a735c
commit 76c9b11a68
7 changed files with 118 additions and 253 deletions

View File

@@ -66,7 +66,6 @@
} }
_shareUrl = ShareUrl; _shareUrl = ShareUrl;
_popoverOpen = true; _popoverOpen = true;
await CopyLink();
} }
} }

View File

@@ -1,8 +1,7 @@
@using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web
@inject IAudioPlayerService AudioPlayerService @inject IAudioPlayerService AudioPlayerService
<MudPaper Elevation="0" <MudItem @onmouseenter="HandleMouseEnter"
@onmouseenter="HandleMouseEnter"
@onmouseleave="HandleMouseLeave" @onmouseleave="HandleMouseLeave"
style="position: relative; display: inline-block; cursor: pointer; border-radius: 4px; overflow: hidden;"> style="position: relative; display: inline-block; cursor: pointer; border-radius: 4px; overflow: hidden;">
@@ -10,15 +9,15 @@
@if (_isHovered || IsCurrentTrackPlaying) @if (_isHovered || IsCurrentTrackPlaying)
{ {
<MudPaper class="play-overlay" <MudItem class="play-overlay"
style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; border-radius: 4px;"> style="position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; border-radius: 4px;">
<MudIconButton Icon="@(IsCurrentTrackPlaying? Icons.Material.Filled.Pause : Icons.Material.Filled.PlayArrow)" <MudIconButton Icon="@(IsCurrentTrackPlaying? Icons.Material.Filled.Pause : Icons.Material.Filled.PlayArrow)"
Color="Color.Inherit" Color="Color.Inherit"
Size="Size.Large" Size="Size.Large"
OnClick="OnPlayClick" /> OnClick="OnPlayClick" />
</MudPaper> </MudItem>
} }
</MudPaper> </MudItem>
@code { @code {
[Parameter] public string CoverUrl { get; set; } = string.Empty; [Parameter] public string CoverUrl { get; set; } = string.Empty;

View File

@@ -0,0 +1,34 @@
@using PlaylistShared.Shared.DTO
@using PlaylistShared.Pwa.Components.Common
@using PlaylistShared.Pwa.Extensions
<MudStack Row AlignItems="AlignItems.Center">
<!-- Обложка с фиксированной шириной -->
<MudItem>
<TrackCoverWithPlay CoverUrl="@Track.CoverUri"
TrackId="@Track.TrackId"
TrackTitle="@Track.Title"
PlaylistShareToken="@PlaylistShareToken"
Width="40" Height="40" />
</MudItem>
<!-- Информация о треке (занимает всё доступное место) -->
<MudItem>
<MudStack Spacing="0">
<MudText Typo="Typo.body1" Color="Color.Secondary">@Track.Title</MudText>
<MudText Typo="Typo.body2" >@string.Join(", ", Track.Artists)</MudText>
</MudStack>
</MudItem>
<MudSpacer />
<!-- Длительность (фиксированная ширина по содержимому) -->
<MudItem>
<MudText Typo="Typo.body2">@Track.DurationMs.FormatDuration()</MudText>
</MudItem>
</MudStack>
@code {
[Parameter] public YandexTrack Track { get; set; } = null!;
[Parameter] public string PlaylistShareToken { get; set; } = string.Empty;
}

View File

@@ -1,113 +0,0 @@
@using PlaylistShared.Pwa.Components.Common
@using PlaylistShared.Shared.DTO
@inject HttpClient Http
@inject ISnackbar Snackbar
<MudStack AlignItems="AlignItems.Center">
<MudTextField @bind-Value="_searchQuery"
Label="Название трека или исполнитель"
Variant="Variant.Outlined"
Disabled="@_isSearching"
FullWidth="true"
OnKeyUp="@(async (e) => { if (e.Key == "Enter") await SearchTracks(); })"
Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Filled.Search" AdornmentColor="Color.Secondary"
/>
@if (_isSearching)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="my-7" />
}
else if (_searchResults.Any())
{
<div style="max-height: 400px; overflow-y: auto;">
@foreach (var track in _searchResults)
{
<div style="display: flex; align-items: center; gap: 12px; padding: 8px; border-bottom: 1px solid rgba(0,0,0,0.1);">
<div style="width: 40px; height: 40px; flex-shrink: 0;">
<TrackCoverWithPlay CoverUrl="@track.CoverUri"
TrackId="@track.TrackId"
TrackTitle="@track.Title"
PlaylistShareToken="@ShareToken"
Width="40" Height="40"/>
</div>
<div style="flex: 1; min-width: 0;">
<MudText Typo="Typo.body1" Style="font-weight: 500;">@track.Title</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">@string.Join(", ", track.Artists)</MudText>
</div>
<div style="flex-shrink: 0;">
<MudText Typo="Typo.body2">@track.DurationMs.FormatDuration()</MudText>
</div>
<div style="flex-shrink: 0;">
<MudIconButton Icon="@Icons.Material.Filled.AddCircle"
Color="Color.Primary"
OnClick="() => AddTrack(track)"
Disabled="_addingTrackIds.Contains(track.TrackId)"
Title="Добавить в плейлист" />
</div>
</div>
}
</div>
}
else if (!_isFirstSearch)
{
<MudAlert Severity="Severity.Info">Ничего не найдено. Попробуйте изменить запрос.</MudAlert>
}
</MudStack>
@code {
[Parameter] public EventCallback<string> OnAddTrack { get; set; }
[Parameter] public string ShareToken { get; set; } = string.Empty;
private List<YandexTrack> _searchResults = new();
private bool _isSearching;
private bool _isFirstSearch = true;
private HashSet<string> _addingTrackIds = new();
private string _searchQuery = string.Empty;
private async Task SearchTracks()
{
_isFirstSearch = false;
_isSearching = true;
try
{
var url = $"/api/yandexsearch/query?query={Uri.EscapeDataString(_searchQuery)}&limit=20";
if (!string.IsNullOrEmpty(ShareToken))
url += $"&shared_id={Uri.EscapeDataString(ShareToken)}";
var response = await Http.GetFromJsonAsync<ApiResponse<List<YandexTrack>>>(url);
if (response?.Success == true)
_searchResults = response.Data ?? new();
else
Snackbar.Add(response?.Error?.Message ?? "Ошибка поиска", Severity.Error);
}
catch (Exception ex)
{
Snackbar.Add($"Ошибка: {ex.Message}", Severity.Error);
}
finally
{
_isSearching = false;
StateHasChanged();
}
}
private async Task AddTrack(YandexTrack track)
{
if (_addingTrackIds.Contains(track.TrackId)) return;
_addingTrackIds.Add(track.TrackId);
try
{
await OnAddTrack.InvokeAsync(track.TrackId);
Snackbar.Add($"Трек \"{track.Title}\" добавлен", Severity.Success);
}
catch (Exception ex)
{
Snackbar.Add($"Ошибка добавления: {ex.Message}", Severity.Error);
}
finally
{
_addingTrackIds.Remove(track.TrackId);
StateHasChanged();
}
}
}

View File

@@ -5,8 +5,7 @@
@inject HttpClient Http @inject HttpClient Http
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
<MudPaper Class="mb-4" Elevation="0" Style="background-color: rgba(0,0,0,0.05); border-radius: 8px;"> <MudStack AlignItems="AlignItems.Stretch">
<MudStack AlignItems="AlignItems.Start">
<MudTextField @bind-Value="_searchQuery" <MudTextField @bind-Value="_searchQuery"
@bind-Value:after="SearchTracks" @bind-Value:after="SearchTracks"
Variant="Variant.Outlined" Variant="Variant.Outlined"
@@ -29,48 +28,27 @@
<MudToggleItem Value="TrackSearchType.Artist" Text="Исполнитель" /> <MudToggleItem Value="TrackSearchType.Artist" Text="Исполнитель" />
</MudToggleGroup> </MudToggleGroup>
@if (_isSearching) <MudTable Items="@_searchResults"
{ Virtualize="@true"
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="my-7" /> Height="400px"
} Hover="true"
else if (_searchResults.Any()) Breakpoint="Breakpoint.Sm"
{ Loading="@_isSearching">
<div style="max-height: 400px; overflow-y: auto;"> <RowTemplate>
@foreach (var track in _searchResults) <MudTd Style="width: 100%;">
{ <TrackItem Track="@context" PlaylistShareToken="@ShareToken" />
<div style="display: flex; align-items: center; gap: 12px; padding: 8px; border-bottom: 1px solid rgba(0,0,0,0.1);"> </MudTd>
<div style="width: 40px; height: 40px; flex-shrink: 0;"> <MudTd>
<TrackCoverWithPlay CoverUrl="@track.CoverUri" <MudToggleIconButton Toggled="_addingTrackIds.Contains(context.TrackId)"
TrackId="@track.TrackId"
TrackTitle="@track.Title"
PlaylistShareToken="@ShareToken"
Width="40" Height="40" />
</div>
<div style="flex: 1; min-width: 0;">
<MudText Typo="Typo.body1" Style="font-weight: 500;">@track.Title</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">@string.Join(", ", track.Artists)</MudText>
</div>
<div style="flex-shrink: 0;">
<MudText Typo="Typo.body2">@track.DurationMs.FormatDuration()</MudText>
</div>
<div style="flex-shrink: 0;">
<MudToggleIconButton Toggled="_addingTrackIds.Contains(track.TrackId)"
Icon="@Icons.Material.Filled.AddCircle" Icon="@Icons.Material.Filled.AddCircle"
Color="@Color.Primary" Color="@Color.Primary"
ToggledIcon="@Icons.Material.Filled.RemoveCircle" ToggledIcon="@Icons.Material.Filled.RemoveCircle"
ToggledColor="@Color.Error" ToggledColor="@Color.Error"
ToggledChanged="() => ToggleTrack(track)" /> ToggledChanged="() => ToggleTrack(context)" />
</div> </MudTd>
</div> </RowTemplate>
} </MudTable>
</div>
}
else if (!_isFirstSearch)
{
<MudAlert Severity="Severity.Info">Ничего не найдено. Попробуйте изменить запрос.</MudAlert>
}
</MudStack> </MudStack>
</MudPaper>
@code { @code {
[Parameter] public string ShareToken { get; set; } = string.Empty; [Parameter] public string ShareToken { get; set; } = string.Empty;
@@ -165,7 +143,7 @@
{ {
await RemoveTrackById(track.TrackId); await RemoveTrackById(track.TrackId);
await OnTrackRemoved.InvokeAsync(); await OnTrackRemoved.InvokeAsync();
Snackbar.Add($"Трек \"{track.Title}\" добавлен", Severity.Success); Snackbar.Add($"Трек \"{track.Title}\" удален", Severity.Success);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -208,7 +186,6 @@
var response = await Http.PostAsJsonAsync($"/api/sharedplaylist/{ShareToken}/add-tracks", request); var response = await Http.PostAsJsonAsync($"/api/sharedplaylist/{ShareToken}/add-tracks", request);
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
Snackbar.Add("Трек успешно добавлен", Severity.Success);
await OnTrackAdded.InvokeAsync(); // уведомляем родителя, что список треков изменился await OnTrackAdded.InvokeAsync(); // уведомляем родителя, что список треков изменился
} }
else else
@@ -235,7 +212,6 @@
var response = await Http.PostAsJsonAsync($"/api/sharedplaylist/{ShareToken}/remove-tracks", request); var response = await Http.PostAsJsonAsync($"/api/sharedplaylist/{ShareToken}/remove-tracks", request);
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
Snackbar.Add("Трек успешно удален", Severity.Success);
await OnTrackAdded.InvokeAsync(); // уведомляем родителя, что список треков изменился await OnTrackAdded.InvokeAsync(); // уведомляем родителя, что список треков изменился
} }
else else

View File

@@ -5,48 +5,14 @@
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject IDialogService DialogService @inject IDialogService DialogService
<MudTable Items="@_tracks" Hover="true" Breakpoint="Breakpoint.Sm" Loading="_loading"> <MudTable Items="@_tracks" Hover="true" Breakpoint="Breakpoint.Sm" Loading="@_loading">
<HeaderContent>
<MudTh>#</MudTh>
<MudTh>Обложка</MudTh>
<MudTh>Название</MudTh>
<MudTh>Исполнитель</MudTh>
<MudTh>Длительность</MudTh>
@if (CanRemove)
{
<MudTh></MudTh>
}
</HeaderContent>
<RowTemplate> <RowTemplate>
<MudTd DataLabel="#" Style="font-weight: normal;">@context.Index</MudTd> <MudTd Style="width: 100%;">
<MudTd DataLabel="Обложка"> <TrackItem Track="@context" PlaylistShareToken="@ShareToken" />
@if (!string.IsNullOrEmpty(context.CoverUri))
{
@if (CanPlay)
{
<TrackCoverWithPlay CoverUrl="@context.CoverUri"
TrackId="@context.TrackId"
TrackTitle="@context.Title"
PlaylistShareToken="@ShareToken"
Width="50" Height="50"/>
}
else
{
<MudImage Src="@context.CoverUri.FormatCoverUrl(50, 50)" Height="50" Width="50" Class="rounded" />
}
}
</MudTd> </MudTd>
<MudTd DataLabel="Название">
<MudLink Href="@($"https://music.yandex.ru/track/{context.TrackId}")" 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="Длительность">@context.DurationMs.FormatDuration()</MudTd>
@if (CanRemove) @if (CanRemove)
{ {
<MudTd DataLabel=""> <MudTd>
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="() => RemoveTrack(context)" /> <MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="() => RemoveTrack(context)" />
</MudTd> </MudTd>
} }
@@ -57,7 +23,6 @@
[Parameter] public string ShareToken { get; set; } = string.Empty; [Parameter] public string ShareToken { get; set; } = string.Empty;
[Parameter] public bool CanPlay { get; set; } [Parameter] public bool CanPlay { get; set; }
[Parameter] public bool CanRemove { get; set; } [Parameter] public bool CanRemove { get; set; }
[Parameter] public string? CurrentPlayingTrackId { get; set; }
[Parameter] public bool IsPlaying { get; set; } [Parameter] public bool IsPlaying { get; set; }
[Parameter] public EventCallback<string> OnPlayTrack { get; set; } [Parameter] public EventCallback<string> OnPlayTrack { get; set; }

View File

@@ -10,7 +10,7 @@
@inject AuthenticationStateProvider AuthProvider @inject AuthenticationStateProvider AuthProvider
@inject IDialogService DialogService @inject IDialogService DialogService
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="mt-8">
@if (_loading) @if (_loading)
{ {
<MudProgressCircular Indeterminate /> <MudProgressCircular Indeterminate />
@@ -21,6 +21,8 @@
} }
else else
{ {
<MudSplitPanel>
<FirstPanel>
<MudCard> <MudCard>
<!-- Заголовок с обложкой --> <!-- Заголовок с обложкой -->
<MudCardHeader> <MudCardHeader>
@@ -30,28 +32,31 @@
</MudCardHeader> </MudCardHeader>
<MudCardContent> <MudCardContent>
@if (_canAdd)
{
<AddTrackSection ShareToken="@Token"
OnTrackAdded="LoadTracks"
OnTrackRemoved="LoadTracks"
/>
}
<!-- Список треков -->
<MudStack Row Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mb-4">
<MudText Typo="Typo.h6">Треки</MudText>
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="LoadTracks" Disabled="_tracksLoading" Size="Size.Medium" /> <MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="LoadTracks" Disabled="_tracksLoading" Size="Size.Medium" />
</MudStack>
<TracksTable @ref="_tracksTableRef" <TracksTable @ref="_tracksTableRef"
ShareToken="@Token" ShareToken="@Token"
CanPlay="@_canPlay" CanPlay="@_canPlay"
CanRemove="@_canRemove" CanRemove="@_canRemove"
CurrentPlayingTrackId="_currentTrackId"
/> />
</MudCardContent> </MudCardContent>
</MudCard> </MudCard>
</FirstPanel>
<SecondPanel>
@if (_canAdd)
{
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h5" Color="Color.Primary">Добавление треков</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<AddTrackSection ShareToken="@Token" OnTrackAdded="LoadTracks" OnTrackRemoved="LoadTracks" />
</MudCardContent>
</MudCard>
}
</SecondPanel>
</MudSplitPanel>
} }
</MudContainer> </MudContainer>