Добавьте файлы проекта.

This commit is contained in:
FrigaT
2026-04-13 14:16:44 +03:00
parent b2b5a3945a
commit 37c997dbe0
120 changed files with 5364 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
@page "/auth-callback"
@using PlaylistShared.Pwa.Services
@inject NavigationManager Navigation
@inject AuthStateProvider AuthProvider
@inject ISnackbar Snackbar
@code {
[Parameter] public string? Token { get; set; }
[Parameter] public string? RefreshToken { get; set; }
protected override async Task OnInitializedAsync()
{
if (!string.IsNullOrEmpty(Token) && !string.IsNullOrEmpty(RefreshToken))
{
await AuthProvider.MarkUserAsAuthenticated(Token, RefreshToken);
Navigation.NavigateTo("/");
}
else
{
Snackbar.Add("Ошибка аутентификации через Яндекс", Severity.Error);
Navigation.NavigateTo("/login");
}
}
}

View File

@@ -0,0 +1,18 @@
@page "/"
<PageTitle>Home</PageTitle>
<MudText Typo="Typo.h3" GutterBottom="true">Hello, world!</MudText>
<MudText Class="mb-8">Welcome to your new app, powered by MudBlazor and the .NET 10 Template!</MudText>
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined" Dense="true" Class="mb-6">
Before authentication will function correctly, you must configure your provider details in <code>Program.cs</code>.
</MudAlert>
<MudAlert Severity="Severity.Normal" ContentAlignment="HorizontalAlignment.Start">
You can find documentation and examples on our website here:
<MudLink Href="https://mudblazor.com" Target="_blank" Typo="Typo.body2" Color="Color.Primary">
<b>www.mudblazor.com</b>
</MudLink>
</MudAlert>

View File

@@ -0,0 +1,75 @@
@page "/login"
@using PlaylistShared.Shared.DTO
@using PlaylistShared.Pwa.Services
@inject HttpClient Http
@inject AuthStateProvider AuthProvider
@inject NavigationManager Navigation
@inject ISnackbar Snackbar
<MudContainer MaxWidth="MaxWidth.Small" Class="mt-16">
<MudCard>
<MudCardContent Class="text-center">
<MudText Typo="Typo.h5" Class="mb-4">Вход в PlaylistShared</MudText>
<MudText Typo="Typo.body2" Class="mb-6">
Войдите через учётную запись Keycloak или используйте локальный аккаунт.
</MudText>
<!-- Кнопка входа через Keycloak -->
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="LoginWithKeycloak" StartIcon="@Icons.Material.Filled.Login" FullWidth="true" Class="mb-4">
Войти через Keycloak
</MudButton>
<MudDivider Class="my-4">или</MudDivider>
<!-- Локальная форма входа -->
<MudTextField @bind-Value="_loginModel.Username" Label="Имя пользователя" Variant="Variant.Outlined" FullWidth="true" Class="mb-3" />
<MudTextField @bind-Value="_loginModel.Password" Label="Пароль" Variant="Variant.Outlined" FullWidth="true" Type="InputType.Password" />
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" OnClick="LocalLogin" FullWidth="true" Class="mt-4">
Войти (локально)
</MudButton>
<MudText Class="mt-4" Typo="Typo.body2">
Нет аккаунта? <MudLink Href="/register">Зарегистрироваться</MudLink>
</MudText>
</MudCardContent>
</MudCard>
</MudContainer>
@code {
private LoginModel _loginModel = new();
private void LoginWithKeycloak()
{
Navigation.NavigateTo("/api/openid/login", true);
}
private async Task LocalLogin()
{
var response = await Http.PostAsJsonAsync("/api/account/login", _loginModel);
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadFromJsonAsync<ApiResponse<LoginResponse>>();
if (result?.Success == true && result.Data != null)
{
await AuthProvider.MarkUserAsAuthenticated(result.Data.Token, result.Data.RefreshToken);
Navigation.NavigateTo("/");
}
else
{
Snackbar.Add(result?.Error?.Message ?? "Ошибка входа", Severity.Error);
}
}
else
{
Snackbar.Add("Неверное имя пользователя или пароль", Severity.Error);
}
}
public class LoginModel
{
public string Username { get; set; } = "";
public string Password { get; set; } = "";
}
}

View File

@@ -0,0 +1,12 @@
@page "/logout"
@using PlaylistShared.Pwa.Services
@inject AuthStateProvider AuthProvider
@inject NavigationManager Navigation
@code {
protected override async Task OnInitializedAsync()
{
await AuthProvider.MarkUserAsLoggedOut();
Navigation.NavigateTo("/");
}
}

View File

@@ -0,0 +1,125 @@
@page "/my-playlists"
@attribute [Authorize]
@using PlaylistShared.Shared.DTO
@inject HttpClient Http
@inject ISnackbar Snackbar
@inject NavigationManager Navigation
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h5">Мои плейлисты</MudText>
</CardHeaderContent>
<CardHeaderActions>
<!-- Явно указываем T="bool" для MudSwitch -->
<MudSwitch T="bool" @bind-Checked="_showOnlyShared" Color="Color.Primary" Label="Только расшаренные" />
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="LoadPlaylists" />
</CardHeaderActions>
</MudCardHeader>
<MudCardContent>
@if (_loading)
{
<MudProgressCircular Indeterminate />
}
else if (_playlists == null || !_playlists.Any())
{
<MudText>Плейлисты не найдены. Убедитесь, что вы сохранили корректный токен Яндекс.Музыки.</MudText>
}
else
{
<MudTable Items="@FilteredPlaylists" Hover="true" Breakpoint="Breakpoint.Sm">
<HeaderContent>
<MudTh>Название</MudTh>
<MudTh>Треков</MudTh>
<MudTh>Статус</MudTh>
<MudTh></MudTh>
</HeaderContent>
<RowTemplate>
<MudTd><MudText>@context.Title</MudText></MudTd>
<MudTd>@context.TrackCount</MudTd>
<MudTd>
<!-- Явно указываем T="string" для MudChip -->
@if (context.IsShared)
{
<MudChip T="string" Size="Size.Small" Color="Color.Success">Расшарен</MudChip>
}
else
{
<MudChip T="string" Size="Size.Small" Color="Color.Default">Не расшарен</MudChip>
}
</MudTd>
<MudTd>
@if (!context.IsShared)
{
<MudButton Variant="Variant.Text" Color="Color.Primary" OnClick="() => SharePlaylist(context)">Поделиться</MudButton>
}
else
{
<MudButton Variant="Variant.Text" Color="Color.Secondary" OnClick="() => GoToShared(context)">Управлять</MudButton>
}
</MudTd>
</RowTemplate>
</MudTable>
}
</MudCardContent>
</MudCard>
</MudContainer>
@code {
private List<YandexPlaylistInfo> _playlists;
private bool _loading = true;
private bool _showOnlyShared = false;
private List<YandexPlaylistInfo> FilteredPlaylists => _showOnlyShared ? _playlists?.Where(p => p.IsShared).ToList() : _playlists;
protected override async Task OnInitializedAsync()
{
await LoadPlaylists();
}
private async Task LoadPlaylists()
{
_loading = true;
try
{
var response = await Http.GetFromJsonAsync<ApiResponse<List<YandexPlaylistInfo>>>("/api/playlist/my");
if (response?.Success == true)
_playlists = response.Data;
else
Snackbar.Add("Ошибка загрузки плейлистов", Severity.Error);
}
catch (Exception ex)
{
Snackbar.Add($"Ошибка: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
StateHasChanged();
}
}
private async Task SharePlaylist(YandexPlaylistInfo playlist)
{
var request = new SharePlaylistRequest { Kind = playlist.Kind, OwnerUid = playlist.OwnerUid };
var response = await Http.PostAsJsonAsync("/api/playlist/share", request);
if (response.IsSuccessStatusCode)
{
Snackbar.Add("Плейлист расшарен", Severity.Success);
await LoadPlaylists();
}
else
{
Snackbar.Add("Ошибка расшаривания", Severity.Error);
}
}
private void GoToShared(YandexPlaylistInfo playlist)
{
if (!string.IsNullOrEmpty(playlist.ShareToken))
Navigation.NavigateTo($"/shared/{playlist.ShareToken}");
else
Snackbar.Add("Ошибка: токен расшаривания не найден", Severity.Error);
}
}

View File

@@ -0,0 +1,9 @@
@page "/not-found"
@layout MainLayout
<PageTitle>Not Found</PageTitle>
<MudText Typo="Typo.h3" GutterBottom="true">404 - Page Not Found</MudText>
<MudText Class="mb-8">Sorry, the content you are looking for does not exist.</MudText>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Href="/">Go to Home</MudButton>

View File

@@ -0,0 +1,72 @@
@page "/profile"
@using Microsoft.AspNetCore.Authorization
@using PlaylistShared.Shared.DTO
@attribute [Authorize]
@inject HttpClient Http
@inject ISnackbar Snackbar
<MudContainer MaxWidth="MaxWidth.Small" Class="mt-8">
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h5">Личный кабинет</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<MudText Typo="Typo.body2" Class="mb-4">Здесь вы можете указать токен доступа к Яндекс.Музыке.</MudText>
<MudTextField @bind-Value="_token" Label="Токен Яндекс.Музыки" Variant="Variant.Outlined" FullWidth="true" />
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveToken" Class="mt-4" FullWidth="true">Сохранить токен</MudButton>
<MudText Class="mt-4" Typo="Typo.body2">Статус: @_statusText</MudText>
</MudCardContent>
</MudCard>
</MudContainer>
@code {
private string _token = "";
private string _statusText = "Загрузка...";
protected override async Task OnInitializedAsync()
{
await LoadStatus();
}
private async Task LoadStatus()
{
try
{
var response = await Http.GetFromJsonAsync<ApiResponse<YandexTokenStatus>>("/api/yandextoken/status");
if (response?.Success == true)
{
_statusText = response.Data.HasToken
? $"Токен установлен{(response.Data.IsValid ? "" : " (просрочен)")}"
: "Токен не установлен";
}
}
catch { _statusText = "Не удалось загрузить статус"; }
}
private async Task SaveToken()
{
if (string.IsNullOrWhiteSpace(_token))
{
Snackbar.Add("Введите токен", Severity.Warning);
return;
}
var request = new SetYandexTokenRequest { Token = _token };
var response = await Http.PostAsJsonAsync("/api/yandextoken/set", request);
if (response.IsSuccessStatusCode)
{
Snackbar.Add("Токен сохранён", Severity.Success);
await LoadStatus();
_token = "";
}
else
{
Snackbar.Add("Ошибка сохранения токена", Severity.Error);
}
}
public class YandexTokenStatus { public bool HasToken { get; set; } public bool IsValid { get; set; } }
public class SetYandexTokenRequest { public string Token { get; set; } }
}

View File

@@ -0,0 +1,67 @@
@page "/register"
@inject HttpClient Http
@inject AuthStateProvider AuthProvider
@inject NavigationManager Navigation
@inject ISnackbar Snackbar
<MudContainer MaxWidth="MaxWidth.Small" Class="mt-16">
<MudCard>
<MudCardContent Class="text-center">
<MudText Typo="Typo.h5" Class="mb-4">Регистрация</MudText>
<MudTextField @bind-Value="_model.Username" Label="Имя пользователя" Variant="Variant.Outlined" FullWidth="true" Class="mb-3" />
<MudTextField @bind-Value="_model.Email" Label="Email" Variant="Variant.Outlined" FullWidth="true" Class="mb-3" />
<MudTextField @bind-Value="_model.Password" Label="Пароль" Variant="Variant.Outlined" FullWidth="true" Type="InputType.Password" />
<MudTextField @bind-Value="_model.ConfirmPassword" Label="Подтверждение пароля" Variant="Variant.Outlined" FullWidth="true" Type="InputType.Password" />
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="OnRegister" FullWidth="true" Class="mt-4">
Зарегистрироваться
</MudButton>
<MudText Class="mt-4" Typo="Typo.body2">
Уже есть аккаунт? <MudLink Href="/login">Войти</MudLink>
</MudText>
</MudCardContent>
</MudCard>
</MudContainer>
@code {
private RegisterModel _model = new();
private async Task OnRegister()
{
if (_model.Password != _model.ConfirmPassword)
{
Snackbar.Add("Пароли не совпадают", Severity.Error);
return;
}
var response = await Http.PostAsJsonAsync("/api/account/register", _model);
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadFromJsonAsync<ApiResponse<LoginResponse>>();
if (result?.Success == true)
{
await AuthProvider.MarkUserAsAuthenticated(result.Data.Token, result.Data.RefreshToken);
Navigation.NavigateTo("/");
}
else
{
Snackbar.Add(result?.Error?.Message ?? "Ошибка регистрации", Severity.Error);
}
}
else
{
var error = await response.Content.ReadFromJsonAsync<ApiResponse<object>>();
Snackbar.Add(error?.Error?.Message ?? "Ошибка регистрации", Severity.Error);
}
}
public class RegisterModel
{
public string Username { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string ConfirmPassword { get; set; }
}
}

View File

@@ -0,0 +1,163 @@
@page "/shared/{token}"
@attribute [Authorize]
@using PlaylistShared.Shared.DTO
@using PlaylistShared.Shared.Enums
@using PlaylistShared.Pwa.Services
@using PlaylistShared.Shared.Models
@inject HttpClient Http
@inject ISnackbar Snackbar
@inject NavigationManager Navigation
@inject AuthenticationStateProvider AuthProvider
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
@if (_loading)
{
<MudProgressCircular Indeterminate />
}
else if (_playlist == null)
{
<MudAlert Severity="Severity.Error">Плейлист не найден или у вас нет доступа</MudAlert>
}
else
{
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h5">@_playlist.Title</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">Владелец: @_playlist.Creator?.UserName</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
@if (_isCreator)
{
<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="4">
<MudSelect T="ViewPermission" Label="Просмотр" @bind-Value="_editPermissions.ViewPermission" Variant="Variant.Outlined" FullWidth="true">
<MudSelectItem Value="ViewPermission.Everyone">Все</MudSelectItem>
<MudSelectItem Value="ViewPermission.AuthorizedOnly">Только авторизованные</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="4">
<MudSelect T="EditPermission" Label="Добавление треков" @bind-Value="_editPermissions.AddPermission" Variant="Variant.Outlined" FullWidth="true">
<MudSelectItem Value="EditPermission.Everyone">Все</MudSelectItem>
<MudSelectItem Value="EditPermission.AuthorizedOnly">Только авторизованные</MudSelectItem>
<MudSelectItem Value="EditPermission.AddedByUserOnly">Только добавивший</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12" sm="4">
<MudSelect T="EditPermission" Label="Удаление треков" @bind-Value="_editPermissions.RemovePermission" Variant="Variant.Outlined" FullWidth="true">
<MudSelectItem Value="EditPermission.Everyone">Все</MudSelectItem>
<MudSelectItem Value="EditPermission.AuthorizedOnly">Только авторизованные</MudSelectItem>
<MudSelectItem Value="EditPermission.AddedByUserOnly">Только добавивший</MudSelectItem>
</MudSelect>
</MudItem>
</MudGrid>
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SavePermissions" Disabled="_savingPermissions">
@if (_savingPermissions)
{
<MudProgressCircular Size="Size.Small" Indeterminate />
}
else
{
<span>Сохранить</span>
}
</MudButton>
</MudPaper>
}
<!-- Здесь будет отображение треков и управление -->
<MudText>Функционал управления треками в разработке</MudText>
</MudCardContent>
</MudCard>
}
</MudContainer>
@code {
[Parameter] public string Token { get; set; }
private SharedPlaylistDto? _playlist;
private bool _loading = true;
private bool _isCreator;
private UpdatePermissionsDto _editPermissions = new();
private bool _savingPermissions;
private string? _currentUserId;
protected override async Task OnInitializedAsync()
{
var authState = await AuthProvider.GetAuthenticationStateAsync();
_currentUserId = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
await LoadPlaylist();
}
private async Task LoadPlaylist()
{
try
{
var response = await Http.GetFromJsonAsync<ApiResponse<SharedPlaylistDto>>($"/api/sharedplaylist/{Token}");
if (response?.Success == true)
{
_playlist = response.Data;
_isCreator = _playlist.CreatorUserId.ToString() == _currentUserId;
if (_isCreator)
{
_editPermissions = new UpdatePermissionsDto
{
ViewPermission = _playlist.ViewPermission,
AddPermission = _playlist.AddPermission,
RemovePermission = _playlist.RemovePermission
};
}
}
else
{
Snackbar.Add(response?.Error?.Message ?? "Не удалось загрузить плейлист", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"Ошибка: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
StateHasChanged();
}
}
private async Task SavePermissions()
{
_savingPermissions = true;
try
{
var response = await Http.PutAsJsonAsync($"/api/sharedplaylist/{Token}/permissions", _editPermissions);
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadFromJsonAsync<ApiResponse<SharedPlaylistDto>>();
if (result?.Success == true)
{
_playlist = result.Data;
Snackbar.Add("Права доступа обновлены", Severity.Success);
}
else
{
Snackbar.Add(result?.Error?.Message ?? "Ошибка обновления", Severity.Error);
}
}
else
{
Snackbar.Add("Ошибка сохранения прав", Severity.Error);
}
}
catch (Exception ex)
{
Snackbar.Add($"Ошибка: {ex.Message}", Severity.Error);
}
finally
{
_savingPermissions = false;
}
}
}