Добавлена подсветка воспроизводимой песни
This commit is contained in:
67
PlaylistShared.Api/Controllers/OgController.cs
Normal file
67
PlaylistShared.Api/Controllers/OgController.cs
Normal file
@@ -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<ContentResult> 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 = $"""
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>{HtmlEncode(title)}</title>
|
||||||
|
|
||||||
|
<meta property="og:type" content="music.playlist" />
|
||||||
|
<meta property="og:title" content="{HtmlEncode(title)}" />
|
||||||
|
<meta property="og:description" content="{HtmlEncode(description)}" />
|
||||||
|
<meta property="og:url" content="{HtmlEncode(pwaUrl)}" />
|
||||||
|
{(string.IsNullOrEmpty(imageUrl) ? "" : $"""<meta property="og:image" content="{HtmlEncode(imageUrl)}" />""")}
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:title" content="{HtmlEncode(title)}" />
|
||||||
|
<meta name="twitter:description" content="{HtmlEncode(description)}" />
|
||||||
|
{(string.IsNullOrEmpty(imageUrl) ? "" : $"""<meta name="twitter:image" content="{HtmlEncode(imageUrl)}" />""")}
|
||||||
|
|
||||||
|
<meta http-equiv="refresh" content="0;url={HtmlEncode(pwaUrl)}" />
|
||||||
|
<script>window.location.replace("{JsEncode(pwaUrl)}");</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>Перенаправление на <a href="{HtmlEncode(pwaUrl)}">{HtmlEncode(title)}</a>…</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
""";
|
||||||
|
|
||||||
|
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("\"", "\\\"");
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
using Microsoft.AspNetCore.DataProtection;
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
|
using Microsoft.AspNetCore.HttpOverrides;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
@@ -51,6 +52,14 @@ public class Program
|
|||||||
options.Cookie.HttpOnly = true;
|
options.Cookie.HttpOnly = true;
|
||||||
options.Cookie.IsEssential = true;
|
options.Cookie.IsEssential = true;
|
||||||
options.Cookie.SameSite = SameSiteMode.Lax;
|
options.Cookie.SameSite = SameSiteMode.Lax;
|
||||||
|
options.Cookie.MaxAge = TimeSpan.FromDays(30); // persistent across browser restarts
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||||
|
{
|
||||||
|
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
||||||
|
options.KnownNetworks.Clear(); // trust all proxies in Docker network
|
||||||
|
options.KnownProxies.Clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
// JWT
|
// JWT
|
||||||
@@ -129,6 +138,7 @@ public class Program
|
|||||||
|
|
||||||
app.MapOpenApi();
|
app.MapOpenApi();
|
||||||
|
|
||||||
|
app.UseForwardedHeaders();
|
||||||
app.UseCors("Production");
|
app.UseCors("Production");
|
||||||
|
|
||||||
if (!app.Environment.IsDevelopment())
|
if (!app.Environment.IsDevelopment())
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ public class YandexAuthService
|
|||||||
internal async Task<YandexAuthQr> GenerateQrAsync(ApplicationUser user)
|
internal async Task<YandexAuthQr> GenerateQrAsync(ApplicationUser user)
|
||||||
{
|
{
|
||||||
var qr = await Api.Passport.GetAuthQRLinkAsync();
|
var qr = await Api.Passport.GetAuthQRLinkAsync();
|
||||||
var trackId = Service.Client.AuthStorage.AuthToken.TrackId;
|
var trackId = Service.Client.AuthStorage.AuthToken?.TrackId;
|
||||||
var csrfToken = Service.Client.AuthStorage.AuthToken.CsfrToken;
|
var csrfToken = Service.Client.AuthStorage.AuthToken?.CsfrToken;
|
||||||
var headerProcessUuid = Service.Client.AuthStorage.HeaderToken.ProcessUuid;
|
var headerProcessUuid = Service.Client.AuthStorage.HeaderToken?.ProcessUuid;
|
||||||
var headerCsrfToken = Service.Client.AuthStorage.HeaderToken.CsfrToken;
|
var headerCsrfToken = Service.Client.AuthStorage.HeaderToken?.CsfrToken;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(qr))
|
if (string.IsNullOrEmpty(qr))
|
||||||
throw new Exception("Не удалось получить QR-ссылку");
|
throw new Exception("Не удалось получить QR-ссылку");
|
||||||
@@ -93,6 +93,8 @@ public class YandexAuthService
|
|||||||
|
|
||||||
Service.Client.AuthStorage.AuthToken.CsfrToken = session.CsrfToken ?? "";
|
Service.Client.AuthStorage.AuthToken.CsfrToken = session.CsrfToken ?? "";
|
||||||
Service.Client.AuthStorage.AuthToken.TrackId = session.TrackId ?? "";
|
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.CsfrToken = session.HeaderCsrfToken ?? "";
|
||||||
Service.Client.AuthStorage.HeaderToken.ProcessUuid = session.HeaderProcessId ?? "";
|
Service.Client.AuthStorage.HeaderToken.ProcessUuid = session.HeaderProcessId ?? "";
|
||||||
|
|
||||||
@@ -131,15 +133,16 @@ public class YandexAuthService
|
|||||||
private void RestoreCookies(CookieContainer container, string serializedCookies)
|
private void RestoreCookies(CookieContainer container, string serializedCookies)
|
||||||
{
|
{
|
||||||
var cookies = JsonSerializer.Deserialize<List<CookieData>>(serializedCookies);
|
var cookies = JsonSerializer.Deserialize<List<CookieData>>(serializedCookies);
|
||||||
|
if (cookies == null) return;
|
||||||
foreach (var c in cookies)
|
foreach (var c in cookies)
|
||||||
container.Add(new Cookie(c.Name, c.Value, c.Path, c.Domain));
|
container.Add(new Cookie(c.Name, c.Value, c.Path, c.Domain));
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CookieData
|
private class CookieData
|
||||||
{
|
{
|
||||||
public string Name { get; set; }
|
public string Name { get; set; } = string.Empty;
|
||||||
public string Value { get; set; }
|
public string Value { get; set; } = string.Empty;
|
||||||
public string Domain { get; set; }
|
public string Domain { get; set; } = string.Empty;
|
||||||
public string Path { get; set; }
|
public string Path { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
PlaylistShared.Api/ruvector.db
Normal file
BIN
PlaylistShared.Api/ruvector.db
Normal file
Binary file not shown.
@@ -14,7 +14,7 @@
|
|||||||
Paper="true">
|
Paper="true">
|
||||||
<MudPaper Class="pa-4">
|
<MudPaper Class="pa-4">
|
||||||
<MudText Typo="Typo.body2" Class="mb-2">Ссылка для приглашения:</MudText>
|
<MudText Typo="Typo.body2" Class="mb-2">Ссылка для приглашения:</MudText>
|
||||||
<MudStack Row Gap="2" AlignItems="AlignItems.Center">
|
<MudStack Row Spacing="2" AlignItems="AlignItems.Center">
|
||||||
<MudTextField @bind-Value="_shareUrl"
|
<MudTextField @bind-Value="_shareUrl"
|
||||||
ReadOnly="true"
|
ReadOnly="true"
|
||||||
Variant="Variant.Outlined"
|
Variant="Variant.Outlined"
|
||||||
|
|||||||
@@ -6,18 +6,17 @@
|
|||||||
<MudItem @onmouseenter="HandleMouseEnter"
|
<MudItem @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;">
|
||||||
|
|
||||||
<MudImage Src="@Track?.CoverUri.FormatCoverUrl(Width, Height)" Height="@Height" Width="@Width" Class="rounded" Style="display: block;" />
|
<MudImage Src="@Track?.CoverUri.FormatCoverUrl(Width, Height)" Height="@Height" Width="@Width" Class="rounded" Style="display: block;" />
|
||||||
|
|
||||||
@if (CanPlay && (_isHovered || IsCurrentTrackPlaying))
|
@if (CanPlay)
|
||||||
{
|
{
|
||||||
<MudItem class="play-overlay"
|
<div class="play-overlay @(_isHovered || IsCurrentTrackPlaying ? "play-overlay--visible" : "")">
|
||||||
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; transition: opacity 0.2s ease; cursor: pointer;">
|
<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" />
|
||||||
</MudItem>
|
</div>
|
||||||
}
|
}
|
||||||
</MudItem>
|
</MudItem>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
@using PlaylistShared.Pwa.Extensions
|
@using PlaylistShared.Pwa.Extensions
|
||||||
@using PlaylistShared.Shared.Yandex
|
@using PlaylistShared.Shared.Yandex
|
||||||
|
|
||||||
<MudStack Row AlignItems="AlignItems.Center">
|
<MudStack Row AlignItems="AlignItems.Center" Class="@(IsCurrentTrack ? "current-track" : "")">
|
||||||
<!-- Обложка с фиксированной шириной -->
|
<!-- Обложка с фиксированной шириной -->
|
||||||
<MudItem>
|
<MudItem>
|
||||||
<TrackCoverWithPlay TrackId="@Track.TrackId"
|
<TrackCoverWithPlay TrackId="@Track.TrackId"
|
||||||
@@ -31,13 +31,26 @@
|
|||||||
|
|
||||||
<!-- Длительность (фиксированная ширина по содержимому) -->
|
<!-- Длительность (фиксированная ширина по содержимому) -->
|
||||||
<MudItem>
|
<MudItem>
|
||||||
<MudText Typo="Typo.body2">@Track.DurationMs.FormatDuration()</MudText>
|
@if (IsCurrentTrack)
|
||||||
|
{
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.GraphicEq" Color="Color.Primary" Size="Size.Small" Class="mr-1 eq-animate" Style="vertical-align: middle;" />
|
||||||
|
}
|
||||||
|
<MudText Typo="Typo.body2" Inline Color="@(IsCurrentTrack ? Color.Primary : Color.Default)">@Track.DurationMs.FormatDuration()</MudText>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
</MudStack>
|
</MudStack>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.current-track {
|
||||||
|
background: color-mix(in srgb, var(--mud-palette-primary) 10%, transparent);
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: inset 2px 0 0 var(--mud-palette-primary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter] public YandexTrack Track { get; set; } = null!;
|
[Parameter] public YandexTrack Track { get; set; } = null!;
|
||||||
[Parameter] public string PlaylistShareToken { get; set; } = string.Empty;
|
[Parameter] public string PlaylistShareToken { get; set; } = string.Empty;
|
||||||
[Parameter] public bool CanPlay { get; set; } = true;
|
[Parameter] public bool CanPlay { get; set; } = true;
|
||||||
[Parameter] public string? AddedByName { get; set; }
|
[Parameter] public string? AddedByName { get; set; }
|
||||||
|
[Parameter] public bool IsCurrentTrack { get; set; }
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
@implements IAsyncDisposable
|
@implements IAsyncDisposable
|
||||||
|
|
||||||
<MudStack Spacing="1" Row AlignItems="AlignItems.Center" Wrap="Wrap.NoWrap">
|
<MudStack Spacing="1" Row AlignItems="AlignItems.Center" Wrap="Wrap.NoWrap" Class="player-enter">
|
||||||
<!-- Кнопки управления -->
|
<!-- Кнопки управления -->
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.SkipPrevious"
|
<MudIconButton Icon="@Icons.Material.Filled.SkipPrevious"
|
||||||
Size="Size.Small"
|
Size="Size.Small"
|
||||||
@@ -17,9 +17,7 @@
|
|||||||
Disabled="@(!AudioPlayerService.HasPrevious)"
|
Disabled="@(!AudioPlayerService.HasPrevious)"
|
||||||
OnClick="() => AudioPlayerService.PlayPreviousAsync()" />
|
OnClick="() => AudioPlayerService.PlayPreviousAsync()" />
|
||||||
|
|
||||||
<MudItem @onmouseenter="() => { _isPlayHovered = true; }"
|
<MudItem Class="relative d-inline-block rounded overflow-hidden cursor-pointer"
|
||||||
@onmouseleave="() => { _isPlayHovered = false; }"
|
|
||||||
Class="relative d-inline-block rounded overflow-hidden cursor-pointer"
|
|
||||||
Style="width: 50px; height: 50px;">
|
Style="width: 50px; height: 50px;">
|
||||||
|
|
||||||
@if (!string.IsNullOrEmpty(AudioPlayerService.CurrentTrack?.CoverUri))
|
@if (!string.IsNullOrEmpty(AudioPlayerService.CurrentTrack?.CoverUri))
|
||||||
@@ -125,7 +123,6 @@
|
|||||||
private double _volumeBeforeMute;
|
private double _volumeBeforeMute;
|
||||||
private double _bufferSecond;
|
private double _bufferSecond;
|
||||||
|
|
||||||
private bool _isPlayHovered;
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
@@ -179,6 +176,29 @@
|
|||||||
{
|
{
|
||||||
_bufferSecond = second;
|
_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
|
#endregion
|
||||||
|
|
||||||
#region Обработка сервиса
|
#region Обработка сервиса
|
||||||
@@ -307,7 +327,10 @@
|
|||||||
AudioPlayerService.OnStateChanged -= OnServiceStateChanged;
|
AudioPlayerService.OnStateChanged -= OnServiceStateChanged;
|
||||||
|
|
||||||
if (_audioElement != null)
|
if (_audioElement != null)
|
||||||
|
{
|
||||||
|
try { await _audioElement.InvokeVoidAsync("destroy"); } catch { }
|
||||||
await _audioElement.DisposeAsync();
|
await _audioElement.DisposeAsync();
|
||||||
|
}
|
||||||
if (_audioModule != null)
|
if (_audioModule != null)
|
||||||
await _audioModule.DisposeAsync();
|
await _audioModule.DisposeAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
0
PlaylistShared.Pwa/DarkModeToggle())`
Normal file
0
PlaylistShared.Pwa/DarkModeToggle())`
Normal file
@@ -3,9 +3,11 @@
|
|||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
@inject PwaUpdateService PwaUpdateService
|
@inject PwaUpdateService PwaUpdateService
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
|
@inject NavigationManager NavigationManager
|
||||||
@inject ContextualActionBarService ContextualActionBarService
|
@inject ContextualActionBarService ContextualActionBarService
|
||||||
@inject IBrowserViewportService BrowserViewportService
|
@inject IBrowserViewportService BrowserViewportService
|
||||||
@implements IBrowserViewportObserver
|
@implements IBrowserViewportObserver
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
<MudThemeProvider Theme="@_theme" IsDarkMode="_isDarkMode" />
|
<MudThemeProvider Theme="@_theme" IsDarkMode="_isDarkMode" />
|
||||||
<MudPopoverProvider />
|
<MudPopoverProvider />
|
||||||
@@ -23,7 +25,7 @@
|
|||||||
{
|
{
|
||||||
<MudSpacer />
|
<MudSpacer />
|
||||||
<LoginDisplay />
|
<LoginDisplay />
|
||||||
<MudIconButton Icon="@(DarkLightModeButtonIcon)" Color="Color.Inherit" OnClick="@DarkModeToggle" Class="ml-2" />
|
<MudIconButton Icon="@(DarkLightModeButtonIcon)" Color="Color.Inherit" OnClick="@(() => DarkModeToggle())" Class="ml-2" />
|
||||||
<MudLink Href="https://git.frigat.duckdns.org/FrigaT/PlaylistShared" Target="_blank" Color="Color.Inherit" Underline="Underline.None" Class="ml-4">
|
<MudLink Href="https://git.frigat.duckdns.org/FrigaT/PlaylistShared" Target="_blank" Color="Color.Inherit" Underline="Underline.None" Class="ml-4">
|
||||||
<MudIcon Icon="@Icons.Custom.Brands.GitHub" Size="Size.Small" Class="mr-1" /> Git
|
<MudIcon Icon="@Icons.Custom.Brands.GitHub" Size="Size.Small" Class="mr-1" /> Git
|
||||||
</MudLink>
|
</MudLink>
|
||||||
@@ -68,6 +70,22 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
ContextualActionBarService.OnChanged += OnContextualChangedHandler;
|
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)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
@@ -77,6 +95,13 @@
|
|||||||
_dotNetRef = DotNetObjectReference.Create(PwaUpdateService);
|
_dotNetRef = DotNetObjectReference.Create(PwaUpdateService);
|
||||||
await JSRuntime.InvokeVoidAsync("registerSWMessageHandler", _dotNetRef);
|
await JSRuntime.InvokeVoidAsync("registerSWMessageHandler", _dotNetRef);
|
||||||
await BrowserViewportService.SubscribeAsync(this, fireImmediately: true);
|
await BrowserViewportService.SubscribeAsync(this, fireImmediately: true);
|
||||||
|
|
||||||
|
var savedTheme = await JSRuntime.InvokeAsync<string?>("localStorage.getItem", "theme");
|
||||||
|
if (savedTheme != null)
|
||||||
|
{
|
||||||
|
_isDarkMode = savedTheme != "light";
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,9 +117,10 @@
|
|||||||
_drawerOpen = !_drawerOpen;
|
_drawerOpen = !_drawerOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DarkModeToggle()
|
private async Task DarkModeToggle()
|
||||||
{
|
{
|
||||||
_isDarkMode = !_isDarkMode;
|
_isDarkMode = !_isDarkMode;
|
||||||
|
await JSRuntime.InvokeVoidAsync("localStorage.setItem", "theme", _isDarkMode ? "dark" : "light");
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly PaletteLight _lightPalette = new()
|
private readonly PaletteLight _lightPalette = new()
|
||||||
@@ -154,8 +180,14 @@
|
|||||||
|
|
||||||
Task IBrowserViewportObserver.NotifyBrowserViewportChangeAsync(BrowserViewportEventArgs browserViewportEventArgs)
|
Task IBrowserViewportObserver.NotifyBrowserViewportChangeAsync(BrowserViewportEventArgs browserViewportEventArgs)
|
||||||
{
|
{
|
||||||
|
var wasMobile = _isMobile;
|
||||||
_isMobile = browserViewportEventArgs.Breakpoint <= Breakpoint.Sm;
|
_isMobile = browserViewportEventArgs.Breakpoint <= Breakpoint.Sm;
|
||||||
|
|
||||||
|
if (!wasMobile && _isMobile)
|
||||||
|
_drawerOpen = false;
|
||||||
|
else if (wasMobile && !_isMobile)
|
||||||
|
_drawerOpen = true;
|
||||||
|
|
||||||
return InvokeAsync(StateHasChanged);
|
return InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,7 @@
|
|||||||
<MudTd DataLabel="">
|
<MudTd DataLabel="">
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||||
Color="Color.Error"
|
Color="Color.Error"
|
||||||
OnClick="() => RemoveFromFavorites(context)"
|
OnClick="() => RemoveFromFavorites(context)" />
|
||||||
Title="Удалить из избранного" />
|
|
||||||
</MudTd>
|
</MudTd>
|
||||||
</RowTemplate>
|
</RowTemplate>
|
||||||
</MudTable>
|
</MudTable>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@inject AuthenticationStateProvider AuthProvider
|
@inject AuthenticationStateProvider AuthProvider
|
||||||
|
|
||||||
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-8">
|
<MudContainer MaxWidth="MaxWidth.Large" Class="mt-4 mt-sm-8">
|
||||||
<MudCard>
|
<MudCard>
|
||||||
<MudCardHeader>
|
<MudCardHeader>
|
||||||
<CardHeaderContent>
|
<CardHeaderContent>
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
<div style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap;">
|
<div style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap;">
|
||||||
<MudIcon Icon="@Icons.Material.Filled.Share" Size="Size.Medium" />
|
<MudIcon Icon="@Icons.Material.Filled.Share" Size="Size.Medium" />
|
||||||
<div style="flex: 1;">
|
<div style="flex: 1;">
|
||||||
<MudText Typo="Typo.body1" FontWeight="FontWeight.Bold">
|
<MudText Typo="Typo.body1" Style="font-weight: bold;">
|
||||||
Чтобы расшаривать плейлисты, необходимо зарегистрироваться
|
Чтобы расшаривать плейлисты, необходимо зарегистрироваться
|
||||||
</MudText>
|
</MudText>
|
||||||
<MudText Typo="Typo.body2">
|
<MudText Typo="Typo.body2">
|
||||||
@@ -45,19 +45,19 @@
|
|||||||
<!-- Краткие преимущества -->
|
<!-- Краткие преимущества -->
|
||||||
<MudGrid>
|
<MudGrid>
|
||||||
<MudItem xs="12" sm="4">
|
<MudItem xs="12" sm="4">
|
||||||
<MudPaper Class="pa-3 text-center" Elevation="0" Style="background-color: rgba(0,0,0,0.04); border-radius: 8px;">
|
<MudPaper Class="pa-3 text-center feature-card" Elevation="0" Style="background-color: rgba(0,0,0,0.04); border-radius: 8px;">
|
||||||
<MudIcon Icon="@Icons.Material.Filled.Link" Size="Size.Medium" Color="Color.Primary" />
|
<MudIcon Icon="@Icons.Material.Filled.Link" Size="Size.Medium" Color="Color.Primary" />
|
||||||
<MudText Typo="Typo.body2" Class="mt-2">Создавайте ссылки-приглашения</MudText>
|
<MudText Typo="Typo.body2" Class="mt-2">Создавайте ссылки-приглашения</MudText>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="12" sm="4">
|
<MudItem xs="12" sm="4">
|
||||||
<MudPaper Class="pa-3 text-center" Elevation="0" Style="background-color: rgba(0,0,0,0.04); border-radius: 8px;">
|
<MudPaper Class="pa-3 text-center feature-card" Elevation="0" Style="background-color: rgba(0,0,0,0.04); border-radius: 8px;">
|
||||||
<MudIcon Icon="@Icons.Material.Filled.People" Size="Size.Medium" Color="Color.Primary" />
|
<MudIcon Icon="@Icons.Material.Filled.People" Size="Size.Medium" Color="Color.Primary" />
|
||||||
<MudText Typo="Typo.body2" Class="mt-2">Совместное управление треками</MudText>
|
<MudText Typo="Typo.body2" Class="mt-2">Совместное управление треками</MudText>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="12" sm="4">
|
<MudItem xs="12" sm="4">
|
||||||
<MudPaper Class="pa-3 text-center" Elevation="0" Style="background-color: rgba(0,0,0,0.04); border-radius: 8px;">
|
<MudPaper Class="pa-3 text-center feature-card" Elevation="0" Style="background-color: rgba(0,0,0,0.04); border-radius: 8px;">
|
||||||
<MudIcon Icon="@Icons.Material.Filled.Security" Size="Size.Medium" Color="Color.Primary" />
|
<MudIcon Icon="@Icons.Material.Filled.Security" Size="Size.Medium" Color="Color.Primary" />
|
||||||
<MudText Typo="Typo.body2" Class="mt-2">Гибкие настройки доступа</MudText>
|
<MudText Typo="Typo.body2" Class="mt-2">Гибкие настройки доступа</MudText>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
</CardHeaderContent>
|
</CardHeaderContent>
|
||||||
<CardHeaderActions>
|
<CardHeaderActions>
|
||||||
<!-- Явно указываем T="bool" для MudSwitch -->
|
<!-- Явно указываем T="bool" для MudSwitch -->
|
||||||
<MudSwitch T="bool" @bind-Checked="_showOnlyShared" Color="Color.Primary" Label="Только расшаренные" />
|
<MudSwitch T="bool" @bind-Value="_showOnlyShared" Color="Color.Primary" Label="Только расшаренные" />
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="LoadPlaylists" />
|
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="LoadPlaylists" />
|
||||||
</CardHeaderActions>
|
</CardHeaderActions>
|
||||||
</MudCardHeader>
|
</MudCardHeader>
|
||||||
@@ -71,11 +71,11 @@
|
|||||||
</MudContainer>
|
</MudContainer>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private List<YandexPlaylistShare> _playlists;
|
private List<YandexPlaylistShare> _playlists = new();
|
||||||
private bool _loading = true;
|
private bool _loading = true;
|
||||||
private bool _showOnlyShared = false;
|
private bool _showOnlyShared = false;
|
||||||
|
|
||||||
private List<YandexPlaylistShare> FilteredPlaylists => _showOnlyShared ? _playlists?.Where(p => p.IsShared).ToList() : _playlists;
|
private List<YandexPlaylistShare> FilteredPlaylists => _showOnlyShared ? _playlists.Where(p => p.IsShared).ToList() : _playlists;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
{
|
{
|
||||||
var response = await Http.GetFromJsonAsync<ApiResponse<List<YandexPlaylistShare>>>("/api/playlists");
|
var response = await Http.GetFromJsonAsync<ApiResponse<List<YandexPlaylistShare>>>("/api/playlists");
|
||||||
if (response?.Success == true)
|
if (response?.Success == true)
|
||||||
_playlists = response.Data;
|
_playlists = response.Data ?? new();
|
||||||
else
|
else
|
||||||
Snackbar.Add("Ошибка загрузки плейлистов", Severity.Error);
|
Snackbar.Add("Ошибка загрузки плейлистов", Severity.Error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,6 @@
|
|||||||
</MudContainer>
|
</MudContainer>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private string _email = "user@example.com";
|
|
||||||
private string _statusText = "Загрузка...";
|
private string _statusText = "Загрузка...";
|
||||||
private bool _hasToken;
|
private bool _hasToken;
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
var result = await response.Content.ReadFromJsonAsync<ApiResponse<LoginResponse>>();
|
var result = await response.Content.ReadFromJsonAsync<ApiResponse<LoginResponse>>();
|
||||||
if (result?.Success == true)
|
if (result?.Success == true)
|
||||||
{
|
{
|
||||||
await AuthProvider.MarkUserAsAuthenticated(result.Data.Token, result.Data.RefreshToken);
|
await AuthProvider.MarkUserAsAuthenticated(result.Data!.Token, result.Data.RefreshToken);
|
||||||
Navigation.NavigateTo("/");
|
Navigation.NavigateTo("/");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -60,9 +60,9 @@
|
|||||||
|
|
||||||
public class RegisterModel
|
public class RegisterModel
|
||||||
{
|
{
|
||||||
public string Username { get; set; }
|
public string Username { get; set; } = string.Empty;
|
||||||
public string Email { get; set; }
|
public string Email { get; set; } = string.Empty;
|
||||||
public string Password { get; set; }
|
public string Password { get; set; } = string.Empty;
|
||||||
public string ConfirmPassword { get; set; }
|
public string ConfirmPassword { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,14 +249,16 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<MudTable Items="@FilteredPlaylistTracks"
|
<MudTable Items="@FilteredPlaylistTracks"
|
||||||
Hover
|
Hover
|
||||||
Elevation="0"
|
Elevation="0"
|
||||||
Breakpoint="Breakpoint.None"
|
Breakpoint="Breakpoint.None"
|
||||||
|
Class="content-fade-in"
|
||||||
Style="min-height: 0;">
|
Style="min-height: 0;">
|
||||||
<RowTemplate>
|
<RowTemplate>
|
||||||
<MudTd Class="py-1 px-0" Style="width: 100%;">
|
<MudTd Class="py-1 px-0" Style="width: 100%;">
|
||||||
<TrackItem Track="@context" PlaylistShareToken="@Token" CanPlay="@_canPlay"
|
<TrackItem Track="@context" PlaylistShareToken="@Token" CanPlay="@_canPlay"
|
||||||
|
IsCurrentTrack="@(AudioPlayerService.CurrentTrackId == context.TrackId)"
|
||||||
AddedByName="@(_trackAdditions.TryGetValue(context.TrackId, out var n) ? n : null)" />
|
AddedByName="@(_trackAdditions.TryGetValue(context.TrackId, out var n) ? n : null)" />
|
||||||
</MudTd>
|
</MudTd>
|
||||||
@if (_canRemove)
|
@if (_canRemove)
|
||||||
@@ -508,7 +510,7 @@
|
|||||||
/// <summary>Состояние: Происходит поиск.</summary>
|
/// <summary>Состояние: Происходит поиск.</summary>
|
||||||
private bool _isSearching = false;
|
private bool _isSearching = false;
|
||||||
/// <summary>Ссылка на поле ввода</summary>
|
/// <summary>Ссылка на поле ввода</summary>
|
||||||
private MudTextField<string> _searchField;
|
private MudTextField<string> _searchField = null!;
|
||||||
/// <summary>Результат поиска.</summary>
|
/// <summary>Результат поиска.</summary>
|
||||||
private YandexSearchResult? _searchResult = null;
|
private YandexSearchResult? _searchResult = null;
|
||||||
/// <summary>Текст фильтра для результатов поиска</summary>
|
/// <summary>Текст фильтра для результатов поиска</summary>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ events {
|
|||||||
http {
|
http {
|
||||||
include /etc/nginx/mime.types;
|
include /etc/nginx/mime.types;
|
||||||
default_type application/octet-stream;
|
default_type application/octet-stream;
|
||||||
|
|
||||||
# Не раскрывайть версию Nginx в ответах.
|
# Не раскрывайть версию Nginx в ответах.
|
||||||
server_tokens off;
|
server_tokens off;
|
||||||
|
|
||||||
@@ -18,6 +18,29 @@ http {
|
|||||||
gzip_min_length 1024;
|
gzip_min_length 1024;
|
||||||
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/wasm application/json;
|
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 {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name localhost;
|
server_name localhost;
|
||||||
@@ -54,6 +77,18 @@ http {
|
|||||||
try_files $uri =404;
|
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
|
# Основной SPA fallback: все неизвестные пути отдаём через index.html
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ /index.html?$args;
|
try_files $uri $uri/ /index.html?$args;
|
||||||
|
|||||||
@@ -113,15 +113,108 @@ code {
|
|||||||
.horizontal-scroll {
|
.horizontal-scroll {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
scroll-snap-type: x mandatory;
|
scroll-snap-type: x mandatory;
|
||||||
overflow-y: hidden; /* отключаем вертикальный скролл */
|
overflow-y: hidden;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
}
|
}
|
||||||
|
|
||||||
.horizontal-scroll:active {
|
.horizontal-scroll:active {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
/* Для WebKit (Chrome, Edge, Safari) можно включить горизонтальный скролл мышью */
|
|
||||||
.horizontal-scroll {
|
.horizontal-scroll {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
-webkit-overflow-scrolling: touch;
|
-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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -58,6 +58,36 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Возвращаем все методы, которые будут вызываться из C#
|
const handleKeyDown = (e) => {
|
||||||
return { loadAndPlay, play, pause, stop, setVolume, setCurrentTime };
|
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 };
|
||||||
}
|
}
|
||||||
@@ -2,5 +2,5 @@
|
|||||||
|
|
||||||
public class SetYandexTokenRequest
|
public class SetYandexTokenRequest
|
||||||
{
|
{
|
||||||
public string Token { get; set; }
|
public string Token { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
|
|
||||||
public class AddTrackByLinkRequest
|
public class AddTrackByLinkRequest
|
||||||
{
|
{
|
||||||
public string Link { get; set; }
|
public string Link { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ public class SharePlaylistDto
|
|||||||
|
|
||||||
/// <summary>Токен для расшаривания плейлиста.</summary>
|
/// <summary>Токен для расшаривания плейлиста.</summary>
|
||||||
[JsonPropertyName("shareToken")]
|
[JsonPropertyName("shareToken")]
|
||||||
public string ShareToken { get; set; }
|
public string ShareToken { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>Права на просмотр.</summary>
|
/// <summary>Права на просмотр.</summary>
|
||||||
[JsonPropertyName("viewPermission")]
|
[JsonPropertyName("viewPermission")]
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
|
|
||||||
public class SharePlaylistRequest
|
public class SharePlaylistRequest
|
||||||
{
|
{
|
||||||
public string Kind { get; set; }
|
public string Kind { get; set; } = string.Empty;
|
||||||
public string OwnerUid { get; set; }
|
public string OwnerUid { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user