diff --git a/PlaylistShared.Api/Controllers/OgController.cs b/PlaylistShared.Api/Controllers/OgController.cs new file mode 100644 index 0000000..801ef25 --- /dev/null +++ b/PlaylistShared.Api/Controllers/OgController.cs @@ -0,0 +1,67 @@ +using Microsoft.AspNetCore.Mvc; +using PlaylistShared.Api.Services; + +namespace PlaylistShared.Api.Controllers; + +[ApiController] +[Route("shared")] +public class OgController : ControllerBase +{ + private readonly SharedPlaylistService _sharedService; + private readonly string _clientBaseUrl; + + public OgController(SharedPlaylistService sharedService, IConfiguration configuration) + { + _sharedService = sharedService; + _clientBaseUrl = configuration["Client:BaseUrl"]?.TrimEnd('/') ?? ""; + } + + [HttpGet("{token}")] + [Produces("text/html")] + public async Task GetOgPage(string token) + { + var entity = await _sharedService.GetEntityByTokenAsync(token); + var pwaUrl = $"{_clientBaseUrl}/shared/{token}"; + + string title = entity?.Title ?? "Playlist Share"; + string description = entity != null + ? $"Слушайте плейлист «{entity.Title}» на Playlist Share" + : "Совместные плейлисты Яндекс.Музыки"; + string imageUrl = !string.IsNullOrEmpty(entity?.CoverUrl) ? entity.CoverUrl : ""; + + var html = $""" + + + + + + {HtmlEncode(title)} + + + + + + {(string.IsNullOrEmpty(imageUrl) ? "" : $"""""")} + + + + {(string.IsNullOrEmpty(imageUrl) ? "" : $"""""")} + + + + + +

Перенаправление на {HtmlEncode(title)}

+ + + """; + + return Content(html, "text/html; charset=utf-8"); + } + + private static string HtmlEncode(string s) => + System.Web.HttpUtility.HtmlAttributeEncode(s); + + private static string JsEncode(string s) => + s.Replace("\\", "\\\\").Replace("'", "\\'").Replace("\"", "\\\""); +} diff --git a/PlaylistShared.Api/Program.cs b/PlaylistShared.Api/Program.cs index c8151bd..1098a2d 100644 --- a/PlaylistShared.Api/Program.cs +++ b/PlaylistShared.Api/Program.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; @@ -51,6 +52,14 @@ public class Program options.Cookie.HttpOnly = true; options.Cookie.IsEssential = true; options.Cookie.SameSite = SameSiteMode.Lax; + options.Cookie.MaxAge = TimeSpan.FromDays(30); // persistent across browser restarts + }); + + builder.Services.Configure(options => + { + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + options.KnownNetworks.Clear(); // trust all proxies in Docker network + options.KnownProxies.Clear(); }); // JWT @@ -129,6 +138,7 @@ public class Program app.MapOpenApi(); + app.UseForwardedHeaders(); app.UseCors("Production"); if (!app.Environment.IsDevelopment()) diff --git a/PlaylistShared.Api/Services/Yandex/YandexAuthService.cs b/PlaylistShared.Api/Services/Yandex/YandexAuthService.cs index 8e3ef42..26f3301 100644 --- a/PlaylistShared.Api/Services/Yandex/YandexAuthService.cs +++ b/PlaylistShared.Api/Services/Yandex/YandexAuthService.cs @@ -44,10 +44,10 @@ public class YandexAuthService internal async Task GenerateQrAsync(ApplicationUser user) { var qr = await Api.Passport.GetAuthQRLinkAsync(); - var trackId = Service.Client.AuthStorage.AuthToken.TrackId; - var csrfToken = Service.Client.AuthStorage.AuthToken.CsfrToken; - var headerProcessUuid = Service.Client.AuthStorage.HeaderToken.ProcessUuid; - var headerCsrfToken = Service.Client.AuthStorage.HeaderToken.CsfrToken; + var trackId = Service.Client.AuthStorage.AuthToken?.TrackId; + var csrfToken = Service.Client.AuthStorage.AuthToken?.CsfrToken; + var headerProcessUuid = Service.Client.AuthStorage.HeaderToken?.ProcessUuid; + var headerCsrfToken = Service.Client.AuthStorage.HeaderToken?.CsfrToken; if (string.IsNullOrEmpty(qr)) throw new Exception("Не удалось получить QR-ссылку"); @@ -93,6 +93,8 @@ public class YandexAuthService Service.Client.AuthStorage.AuthToken.CsfrToken = session.CsrfToken ?? ""; Service.Client.AuthStorage.AuthToken.TrackId = session.TrackId ?? ""; + if (Service.Client.AuthStorage.HeaderToken is null) + Service.Client.AuthStorage.HeaderToken = new(); Service.Client.AuthStorage.HeaderToken.CsfrToken = session.HeaderCsrfToken ?? ""; Service.Client.AuthStorage.HeaderToken.ProcessUuid = session.HeaderProcessId ?? ""; @@ -131,15 +133,16 @@ public class YandexAuthService private void RestoreCookies(CookieContainer container, string serializedCookies) { var cookies = JsonSerializer.Deserialize>(serializedCookies); + if (cookies == null) return; foreach (var c in cookies) container.Add(new Cookie(c.Name, c.Value, c.Path, c.Domain)); } private class CookieData { - public string Name { get; set; } - public string Value { get; set; } - public string Domain { get; set; } - public string Path { get; set; } + public string Name { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; + public string Domain { get; set; } = string.Empty; + public string Path { get; set; } = string.Empty; } } diff --git a/PlaylistShared.Api/ruvector.db b/PlaylistShared.Api/ruvector.db new file mode 100644 index 0000000..f374a05 Binary files /dev/null and b/PlaylistShared.Api/ruvector.db differ diff --git a/PlaylistShared.Pwa/Components/Common/ShareButton.razor b/PlaylistShared.Pwa/Components/Common/ShareButton.razor index eee5d15..7115315 100644 --- a/PlaylistShared.Pwa/Components/Common/ShareButton.razor +++ b/PlaylistShared.Pwa/Components/Common/ShareButton.razor @@ -14,7 +14,7 @@ Paper="true"> Ссылка для приглашения: - + - + - - @if (CanPlay && (_isHovered || IsCurrentTrackPlaying)) + + @if (CanPlay) { - - + - + } diff --git a/PlaylistShared.Pwa/Components/Common/TrackItem.razor b/PlaylistShared.Pwa/Components/Common/TrackItem.razor index 3c97eee..8ca2f07 100644 --- a/PlaylistShared.Pwa/Components/Common/TrackItem.razor +++ b/PlaylistShared.Pwa/Components/Common/TrackItem.razor @@ -3,7 +3,7 @@ @using PlaylistShared.Pwa.Extensions @using PlaylistShared.Shared.Yandex - + - @Track.DurationMs.FormatDuration() + @if (IsCurrentTrack) + { + + } + @Track.DurationMs.FormatDuration() + + @code { [Parameter] public YandexTrack Track { get; set; } = null!; [Parameter] public string PlaylistShareToken { get; set; } = string.Empty; [Parameter] public bool CanPlay { get; set; } = true; [Parameter] public string? AddedByName { get; set; } + [Parameter] public bool IsCurrentTrack { get; set; } } \ No newline at end of file diff --git a/PlaylistShared.Pwa/Components/Global/AudioPlayer.razor b/PlaylistShared.Pwa/Components/Global/AudioPlayer.razor index 0f3c98a..c8de414 100644 --- a/PlaylistShared.Pwa/Components/Global/AudioPlayer.razor +++ b/PlaylistShared.Pwa/Components/Global/AudioPlayer.razor @@ -9,7 +9,7 @@ @implements IDisposable @implements IAsyncDisposable - + - @if (!string.IsNullOrEmpty(AudioPlayerService.CurrentTrack?.CoverUri)) @@ -125,7 +123,6 @@ private double _volumeBeforeMute; private double _bufferSecond; - private bool _isPlayHovered; protected override async Task OnInitializedAsync() { @@ -179,6 +176,29 @@ { _bufferSecond = second; } + + [JSInvokable] + public async Task OnKeyboardTogglePlay() + { + if (AudioPlayerService.IsPlaying) + await AudioPlayerService.PauseAsync(); + else + await AudioPlayerService.PlayAsync(); + } + + [JSInvokable] + public async Task OnKeyboardSeek(double delta) + { + var newTime = Math.Max(0, AudioPlayerService.CurrentTime + delta); + await AudioPlayerService.SeekToAsync(newTime); + } + + [JSInvokable] + public async Task OnKeyboardVolumeChange(double delta) + { + var newVol = Math.Clamp(AudioPlayerService.CurrentVolume + delta, 0, 100); + await AudioPlayerService.SetVolumeAsync(newVol); + } #endregion #region Обработка сервиса @@ -307,7 +327,10 @@ AudioPlayerService.OnStateChanged -= OnServiceStateChanged; if (_audioElement != null) + { + try { await _audioElement.InvokeVoidAsync("destroy"); } catch { } await _audioElement.DisposeAsync(); + } if (_audioModule != null) await _audioModule.DisposeAsync(); } diff --git a/PlaylistShared.Pwa/DarkModeToggle())` b/PlaylistShared.Pwa/DarkModeToggle())` new file mode 100644 index 0000000..e69de29 diff --git a/PlaylistShared.Pwa/Layout/MainLayout.razor b/PlaylistShared.Pwa/Layout/MainLayout.razor index 3e7b50d..cee5c45 100644 --- a/PlaylistShared.Pwa/Layout/MainLayout.razor +++ b/PlaylistShared.Pwa/Layout/MainLayout.razor @@ -3,9 +3,11 @@ @inherits LayoutComponentBase @inject PwaUpdateService PwaUpdateService @inject IJSRuntime JSRuntime +@inject NavigationManager NavigationManager @inject ContextualActionBarService ContextualActionBarService @inject IBrowserViewportService BrowserViewportService @implements IBrowserViewportObserver +@implements IDisposable @@ -23,7 +25,7 @@ { - + Git @@ -68,6 +70,22 @@ }; ContextualActionBarService.OnChanged += OnContextualChangedHandler; + NavigationManager.LocationChanged += OnLocationChanged; + } + + private void OnLocationChanged(object? sender, Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs e) + { + if (_isMobile) + { + _drawerOpen = false; + InvokeAsync(StateHasChanged); + } + } + + public void Dispose() + { + NavigationManager.LocationChanged -= OnLocationChanged; + ContextualActionBarService.OnChanged -= OnContextualChangedHandler; } protected override async Task OnAfterRenderAsync(bool firstRender) @@ -77,6 +95,13 @@ _dotNetRef = DotNetObjectReference.Create(PwaUpdateService); await JSRuntime.InvokeVoidAsync("registerSWMessageHandler", _dotNetRef); await BrowserViewportService.SubscribeAsync(this, fireImmediately: true); + + var savedTheme = await JSRuntime.InvokeAsync("localStorage.getItem", "theme"); + if (savedTheme != null) + { + _isDarkMode = savedTheme != "light"; + StateHasChanged(); + } } } @@ -92,9 +117,10 @@ _drawerOpen = !_drawerOpen; } - private void DarkModeToggle() + private async Task DarkModeToggle() { _isDarkMode = !_isDarkMode; + await JSRuntime.InvokeVoidAsync("localStorage.setItem", "theme", _isDarkMode ? "dark" : "light"); } private readonly PaletteLight _lightPalette = new() @@ -154,8 +180,14 @@ Task IBrowserViewportObserver.NotifyBrowserViewportChangeAsync(BrowserViewportEventArgs browserViewportEventArgs) { + var wasMobile = _isMobile; _isMobile = browserViewportEventArgs.Breakpoint <= Breakpoint.Sm; + if (!wasMobile && _isMobile) + _drawerOpen = false; + else if (wasMobile && !_isMobile) + _drawerOpen = true; + return InvokeAsync(StateHasChanged); } } diff --git a/PlaylistShared.Pwa/Pages/Favorites.razor b/PlaylistShared.Pwa/Pages/Favorites.razor index 5481499..60cbc8c 100644 --- a/PlaylistShared.Pwa/Pages/Favorites.razor +++ b/PlaylistShared.Pwa/Pages/Favorites.razor @@ -47,8 +47,7 @@ + OnClick="() => RemoveFromFavorites(context)" /> diff --git a/PlaylistShared.Pwa/Pages/Home.razor b/PlaylistShared.Pwa/Pages/Home.razor index 67f436e..277c1d9 100644 --- a/PlaylistShared.Pwa/Pages/Home.razor +++ b/PlaylistShared.Pwa/Pages/Home.razor @@ -3,7 +3,7 @@ @inject NavigationManager Navigation @inject AuthenticationStateProvider AuthProvider - + @@ -26,7 +26,7 @@
- + Чтобы расшаривать плейлисты, необходимо зарегистрироваться @@ -45,19 +45,19 @@ - + Создавайте ссылки-приглашения - + Совместное управление треками - + Гибкие настройки доступа diff --git a/PlaylistShared.Pwa/Pages/MyPlaylists.razor b/PlaylistShared.Pwa/Pages/MyPlaylists.razor index 6c73326..7b4642f 100644 --- a/PlaylistShared.Pwa/Pages/MyPlaylists.razor +++ b/PlaylistShared.Pwa/Pages/MyPlaylists.razor @@ -17,7 +17,7 @@ - + @@ -71,11 +71,11 @@ @code { - private List _playlists; + private List _playlists = new(); private bool _loading = true; private bool _showOnlyShared = false; - private List FilteredPlaylists => _showOnlyShared ? _playlists?.Where(p => p.IsShared).ToList() : _playlists; + private List FilteredPlaylists => _showOnlyShared ? _playlists.Where(p => p.IsShared).ToList() : _playlists; protected override async Task OnInitializedAsync() { @@ -89,7 +89,7 @@ { var response = await Http.GetFromJsonAsync>>("/api/playlists"); if (response?.Success == true) - _playlists = response.Data; + _playlists = response.Data ?? new(); else Snackbar.Add("Ошибка загрузки плейлистов", Severity.Error); } diff --git a/PlaylistShared.Pwa/Pages/Profile.razor b/PlaylistShared.Pwa/Pages/Profile.razor index d7e5da3..0ec378c 100644 --- a/PlaylistShared.Pwa/Pages/Profile.razor +++ b/PlaylistShared.Pwa/Pages/Profile.razor @@ -48,7 +48,6 @@ @code { - private string _email = "user@example.com"; private string _statusText = "Загрузка..."; private bool _hasToken; diff --git a/PlaylistShared.Pwa/Pages/Register.razor b/PlaylistShared.Pwa/Pages/Register.razor index cc1d90b..555ca8d 100644 --- a/PlaylistShared.Pwa/Pages/Register.razor +++ b/PlaylistShared.Pwa/Pages/Register.razor @@ -43,7 +43,7 @@ var result = await response.Content.ReadFromJsonAsync>(); if (result?.Success == true) { - await AuthProvider.MarkUserAsAuthenticated(result.Data.Token, result.Data.RefreshToken); + await AuthProvider.MarkUserAsAuthenticated(result.Data!.Token, result.Data.RefreshToken); Navigation.NavigateTo("/"); } else @@ -60,9 +60,9 @@ public class RegisterModel { - public string Username { get; set; } - public string Email { get; set; } - public string Password { get; set; } - public string ConfirmPassword { get; set; } + public string Username { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public string ConfirmPassword { get; set; } = string.Empty; } } \ No newline at end of file diff --git a/PlaylistShared.Pwa/Pages/SharedPlaylistView.razor b/PlaylistShared.Pwa/Pages/SharedPlaylistView.razor index eccefb5..9fc1bcf 100644 --- a/PlaylistShared.Pwa/Pages/SharedPlaylistView.razor +++ b/PlaylistShared.Pwa/Pages/SharedPlaylistView.razor @@ -249,14 +249,16 @@ } else { - @if (_canRemove) @@ -508,7 +510,7 @@ /// Состояние: Происходит поиск. private bool _isSearching = false; /// Ссылка на поле ввода - private MudTextField _searchField; + private MudTextField _searchField = null!; /// Результат поиска. private YandexSearchResult? _searchResult = null; /// Текст фильтра для результатов поиска diff --git a/PlaylistShared.Pwa/nginx.conf b/PlaylistShared.Pwa/nginx.conf index b0507ec..cc0d615 100644 --- a/PlaylistShared.Pwa/nginx.conf +++ b/PlaylistShared.Pwa/nginx.conf @@ -5,7 +5,7 @@ events { http { include /etc/nginx/mime.types; default_type application/octet-stream; - + # Не раскрывайть версию Nginx в ответах. server_tokens off; @@ -18,6 +18,29 @@ http { gzip_min_length 1024; gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/wasm application/json; + # Бэкенд API (для OG-тегов) + upstream api_backend { + server playlistshared_api:80; + } + + # Определяем, является ли запрос ботом-краулером + map $http_user_agent $is_crawler { + default 0; + ~*Slackbot 1; + ~*facebookexternalhit 1; + ~*Twitterbot 1; + ~*TelegramBot 1; + ~*WhatsApp 1; + ~*LinkedInBot 1; + ~*vkShare 1; + ~*Pinterest 1; + ~*Googlebot 1; + ~*YandexBot 1; + ~*Discordbot 1; + ~*Applebot 1; + ~*DuckDuckBot 1; + } + server { listen 80; server_name localhost; @@ -54,6 +77,18 @@ http { try_files $uri =404; } + # OG-теги: краулеры получают HTML с meta-тегами от API + location ~ ^/shared/(.+)$ { + if ($is_crawler) { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $host; + proxy_pass http://api_backend; + } + try_files $uri $uri/ /index.html?$args; + } + # Основной SPA fallback: все неизвестные пути отдаём через index.html location / { try_files $uri $uri/ /index.html?$args; diff --git a/PlaylistShared.Pwa/wwwroot/css/app.css b/PlaylistShared.Pwa/wwwroot/css/app.css index 516cfba..7b9dbd2 100644 --- a/PlaylistShared.Pwa/wwwroot/css/app.css +++ b/PlaylistShared.Pwa/wwwroot/css/app.css @@ -113,15 +113,108 @@ code { .horizontal-scroll { overflow-x: auto; scroll-snap-type: x mandatory; - overflow-y: hidden; /* отключаем вертикальный скролл */ + overflow-y: hidden; cursor: grab; } .horizontal-scroll:active { cursor: grabbing; } -/* Для WebKit (Chrome, Edge, Safari) можно включить горизонтальный скролл мышью */ + .horizontal-scroll { scrollbar-width: thin; -webkit-overflow-scrolling: touch; +} + +/* ===== Animations ===== */ + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes slideUpFade { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes equalizerPulse { + 0%, 100% { transform: scaleY(0.5) translateY(3px); } + 50% { transform: scaleY(1) translateY(0); } +} + +@keyframes playerSlideUp { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +/* Fade-in for content that loads */ +.content-fade-in { + animation: fadeIn 0.3s ease-out; +} + +/* Track items slide up when list loads */ +.tracks-slide-in { + animation: slideUpFade 0.25s ease-out; +} + +/* Equalizer icon bounce */ +.eq-animate { + display: inline-block; + transform-origin: bottom center; + animation: equalizerPulse 0.55s ease-in-out infinite alternate; +} + +/* Feature cards on home page */ +.feature-card { + transition: transform 0.2s ease, box-shadow 0.2s ease !important; + cursor: default; +} + +.feature-card:hover { + transform: translateY(-3px) !important; + box-shadow: 0 6px 20px rgba(126, 111, 255, 0.18) !important; +} + +/* Audio player entrance */ +.player-enter { + animation: playerSlideUp 0.3s ease-out; +} + +/* Play overlay — opacity-based show/hide */ +.play-overlay { + 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; + opacity: 0; + transition: opacity 0.2s ease; + cursor: pointer; +} + +.play-overlay.play-overlay--visible { + opacity: 1; +} + +/* Touch devices: play overlay always visible */ +@media (hover: none) { + .play-overlay { + opacity: 1; + } +} + +/* Current track — smooth highlight transition */ +.current-track { + transition: background 0.35s ease; +} + +/* Mobile padding tightening */ +@media (max-width: 599px) { + .mud-container { + padding-left: 6px !important; + padding-right: 6px !important; + } } \ No newline at end of file diff --git a/PlaylistShared.Pwa/wwwroot/js/AudioPlayer.js b/PlaylistShared.Pwa/wwwroot/js/AudioPlayer.js index 9f97393..81a006f 100644 --- a/PlaylistShared.Pwa/wwwroot/js/AudioPlayer.js +++ b/PlaylistShared.Pwa/wwwroot/js/AudioPlayer.js @@ -58,6 +58,36 @@ } }); - // Возвращаем все методы, которые будут вызываться из C# - return { loadAndPlay, play, pause, stop, setVolume, setCurrentTime }; + const handleKeyDown = (e) => { + const tag = e.target.tagName.toLowerCase(); + if (tag === 'input' || tag === 'textarea' || e.target.isContentEditable) return; + + switch (e.key) { + case ' ': + e.preventDefault(); + dotNetHelper.invokeMethodAsync('OnKeyboardTogglePlay'); + break; + case 'ArrowLeft': + e.preventDefault(); + dotNetHelper.invokeMethodAsync('OnKeyboardSeek', -10); + break; + case 'ArrowRight': + e.preventDefault(); + dotNetHelper.invokeMethodAsync('OnKeyboardSeek', 10); + break; + case 'ArrowUp': + e.preventDefault(); + dotNetHelper.invokeMethodAsync('OnKeyboardVolumeChange', 5); + break; + case 'ArrowDown': + e.preventDefault(); + dotNetHelper.invokeMethodAsync('OnKeyboardVolumeChange', -5); + break; + } + }; + window.addEventListener('keydown', handleKeyDown); + + const destroy = () => window.removeEventListener('keydown', handleKeyDown); + + return { loadAndPlay, play, pause, stop, setVolume, setCurrentTime, destroy }; } \ No newline at end of file diff --git a/PlaylistShared.Shared/Profile/SetYandexTokenRequest.cs b/PlaylistShared.Shared/Profile/SetYandexTokenRequest.cs index 8e0facb..637b1c9 100644 --- a/PlaylistShared.Shared/Profile/SetYandexTokenRequest.cs +++ b/PlaylistShared.Shared/Profile/SetYandexTokenRequest.cs @@ -2,5 +2,5 @@ public class SetYandexTokenRequest { - public string Token { get; set; } + public string Token { get; set; } = string.Empty; } diff --git a/PlaylistShared.Shared/SharedPlaylist/AddTrackByLinkRequest.cs b/PlaylistShared.Shared/SharedPlaylist/AddTrackByLinkRequest.cs index dd5adc1..caf2d03 100644 --- a/PlaylistShared.Shared/SharedPlaylist/AddTrackByLinkRequest.cs +++ b/PlaylistShared.Shared/SharedPlaylist/AddTrackByLinkRequest.cs @@ -2,5 +2,5 @@ public class AddTrackByLinkRequest { - public string Link { get; set; } + public string Link { get; set; } = string.Empty; } \ No newline at end of file diff --git a/PlaylistShared.Shared/SharedPlaylist/SharePlaylistDto.cs b/PlaylistShared.Shared/SharedPlaylist/SharePlaylistDto.cs index 758bdd0..31bd413 100644 --- a/PlaylistShared.Shared/SharedPlaylist/SharePlaylistDto.cs +++ b/PlaylistShared.Shared/SharedPlaylist/SharePlaylistDto.cs @@ -36,7 +36,7 @@ public class SharePlaylistDto /// Токен для расшаривания плейлиста. [JsonPropertyName("shareToken")] - public string ShareToken { get; set; } + public string ShareToken { get; set; } = string.Empty; /// Права на просмотр. [JsonPropertyName("viewPermission")] diff --git a/PlaylistShared.Shared/SharedPlaylist/SharePlaylistRequest.cs b/PlaylistShared.Shared/SharedPlaylist/SharePlaylistRequest.cs index 6f440b2..6c69e8e 100644 --- a/PlaylistShared.Shared/SharedPlaylist/SharePlaylistRequest.cs +++ b/PlaylistShared.Shared/SharedPlaylist/SharePlaylistRequest.cs @@ -2,6 +2,6 @@ public class SharePlaylistRequest { - public string Kind { get; set; } - public string OwnerUid { get; set; } + public string Kind { get; set; } = string.Empty; + public string OwnerUid { get; set; } = string.Empty; } \ No newline at end of file