299 lines
12 KiB
Plaintext
299 lines
12 KiB
Plaintext
@using Microsoft.AspNetCore.Components.Web
|
|
@using Microsoft.JSInterop
|
|
@inject IJSRuntime JSRuntime
|
|
|
|
<div class="flex flex-col gap-4 @Class">
|
|
<!-- Main Viewer -->
|
|
<div class="relative w-full rounded-2xl overflow-hidden bg-black/5 shadow-xl border border-white/10 group"
|
|
style="aspect-ratio: @Width / @Height;">
|
|
|
|
<!-- Zooming Image Wrapper -->
|
|
@if (!string.IsNullOrEmpty(_selectedUrl))
|
|
{
|
|
var type = GetMediaType(_selectedUrl);
|
|
|
|
@if (type == MediaType.Image)
|
|
{
|
|
<div class="w-full h-full">
|
|
<img src="@_selectedUrl" class="w-full h-full object-cover" alt="Main View" />
|
|
</div>
|
|
|
|
<!-- The Spotlight Lens -->
|
|
<div class="absolute w-[180px] h-[180px] rounded-2xl border-2 border-white/50 shadow-2xl pointer-events-none z-30 overflow-hidden bg-no-repeat bg-white/10 backdrop-blur-sm"
|
|
style="@_zoomStyle">
|
|
</div>
|
|
}
|
|
else if (type == MediaType.Video)
|
|
{
|
|
<video src="@_selectedUrl" controls class="w-full h-full object-cover" />
|
|
}
|
|
else if (type == MediaType.YouTube)
|
|
{
|
|
var id = GetYouTubeId(_selectedUrl);
|
|
<iframe class="w-full h-full" src="https://www.youtube.com/embed/@id" frameborder="0"
|
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
allowfullscreen></iframe>
|
|
}
|
|
else if (type == MediaType.Vimeo)
|
|
{
|
|
<iframe src="@_selectedUrl" class="w-full h-full" frameborder="0"
|
|
allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe>
|
|
}
|
|
else
|
|
{
|
|
<div class="flex items-center justify-center h-full text-gray-400">
|
|
<span>Unsupported Media</span>
|
|
</div>
|
|
}
|
|
|
|
<!-- Event Overlay (Stable Mouse Targets) -->
|
|
@if (type == MediaType.Image)
|
|
{
|
|
<div class="absolute inset-0 z-10 cursor-zoom-in" @onmousemove="HandleMouseMove"
|
|
@onmouseleave="HandleMouseLeave">
|
|
</div>
|
|
}
|
|
|
|
<!-- Navigation Arrows (Main) -->
|
|
@if (MediaUrls.Count() > 1)
|
|
{
|
|
<button @onclick="PreviousMedia" @onclick:stopPropagation="true"
|
|
class="absolute left-4 top-1/2 -translate-y-1/2 w-12 h-12 rounded-full bg-white/40 backdrop-blur-md text-gray-800 border border-white/50 flex items-center justify-center transition-all duration-300 hover:bg-white/60 hover:scale-110 z-20 shadow-xl">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
|
|
stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
|
</svg>
|
|
</button>
|
|
<button @onclick="NextMedia" @onclick:stopPropagation="true"
|
|
class="absolute right-4 top-1/2 -translate-y-1/2 w-12 h-12 rounded-full bg-white/40 backdrop-blur-md text-gray-800 border border-white/50 flex items-center justify-center transition-all duration-300 hover:bg-white/60 hover:scale-110 z-20 shadow-xl">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
|
|
stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</button>
|
|
}
|
|
}
|
|
else
|
|
{
|
|
<div class="flex items-center justify-center h-full text-gray-400 bg-gray-50 dark:bg-gray-800">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 opacity-50" fill="none" viewBox="0 0 24 24"
|
|
stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
</svg>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<!-- Thumbnails List -->
|
|
@if (MediaUrls != null && MediaUrls.Count() > 1)
|
|
{
|
|
<div class="relative group/thumbs">
|
|
<!-- Scroll Buttons -->
|
|
<button @onclick="() => Scroll(-200)"
|
|
class="absolute left-0 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-white/80 dark:bg-gray-800/80 shadow-md z-20 transition-all flex items-center justify-center text-gray-800 dark:text-white hover:scale-110 -ml-5">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
|
|
stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
|
</svg>
|
|
</button>
|
|
<button @onclick="() => Scroll(200)"
|
|
class="absolute right-0 top-1/2 -translate-y-1/2 w-10 h-10 rounded-full bg-white/80 dark:bg-gray-800/80 shadow-md z-20 transition-all flex items-center justify-center text-gray-800 dark:text-white hover:scale-110 -mr-5">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24"
|
|
stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
</button>
|
|
|
|
<div @ref="_thumbContainer" class="flex gap-3 overflow-x-auto pb-2 scrollbar-hide px-1 scroll-smooth">
|
|
@foreach (var url in MediaUrls)
|
|
{
|
|
var isSelected = url == _selectedUrl;
|
|
var type = GetMediaType(url);
|
|
|
|
<button @onclick="() => SelectMedia(url)" style="width: @(ThumbSize)px; height: @(ThumbSize)px;"
|
|
class="relative flex-none aspect-square rounded-xl overflow-hidden border-2 transition-all duration-500 @(isSelected ? "border-sky-500 shadow-xl shadow-sky-400/30 scale-105 z-10 ring-4 ring-sky-500/10" : "border-gray-100 opacity-60 hover:opacity-100 hover:scale-105")">
|
|
|
|
@if (type == MediaType.Image)
|
|
{
|
|
<img src="@url" class="w-full h-full object-cover" />
|
|
}
|
|
else if (type == MediaType.Video || type == MediaType.YouTube || type == MediaType.Vimeo)
|
|
{
|
|
<div class="w-full h-full bg-gray-100 dark:bg-gray-700 flex items-center justify-center">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-sky-500" fill="none" viewBox="0 0 24 24"
|
|
stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
</div>
|
|
}
|
|
|
|
@if (isSelected)
|
|
{
|
|
<div class="absolute inset-0 bg-sky-500/10 animate-pulse"></div>
|
|
}
|
|
</button>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
@code {
|
|
[Parameter] public IEnumerable<string> MediaUrls { get; set; } = Enumerable.Empty<string>();
|
|
[Parameter] public string? Class { get; set; }
|
|
|
|
[Parameter] public int Width { get; set; } = 1;
|
|
[Parameter] public int Height { get; set; } = 1;
|
|
[Parameter] public int ThumbSize { get; set; } = 64;
|
|
|
|
private ElementReference _thumbContainer;
|
|
private string? _selectedUrl;
|
|
private string _zoomStyle = "";
|
|
|
|
protected override void OnInitialized()
|
|
{
|
|
if (MediaUrls.Any()) _selectedUrl = MediaUrls.First();
|
|
}
|
|
|
|
protected override void OnParametersSet()
|
|
{
|
|
if (!string.IsNullOrEmpty(_selectedUrl) && !MediaUrls.Contains(_selectedUrl))
|
|
{
|
|
_selectedUrl = MediaUrls.FirstOrDefault();
|
|
}
|
|
else if (string.IsNullOrEmpty(_selectedUrl) && MediaUrls.Any())
|
|
{
|
|
_selectedUrl = MediaUrls.First();
|
|
}
|
|
}
|
|
|
|
private void HandleMouseMove(MouseEventArgs e)
|
|
{
|
|
// For a spotlight lens, we need to calculate the percentage of mouse position
|
|
// We assume a base size or just use a placeholder for now.
|
|
// Since we can't get the width/height easily, we'll use a standard percentage approach
|
|
// if we assume the container is roughly the size of viewport or fixed.
|
|
// Actually, without JS, OffsetX/Y is the only way.
|
|
// We'll use a rough 600px base for calculation or just standard 0-1 range if we knew bounds.
|
|
|
|
// Let's use a "Magic Lens" approach:
|
|
double zoomLevel = 2.5;
|
|
double lensSize = 180;
|
|
|
|
// We'll estimate the percentage by assuming the mouse is within the container bounds.
|
|
// A better way is to pass the container size via a simplistic JS on resize.
|
|
// But for now, let's just use the current offset and hope it's reasonably mapped.
|
|
|
|
double posX = e.OffsetX;
|
|
double posY = e.OffsetY;
|
|
|
|
// We'll just use the raw offsets to position the lens
|
|
_zoomStyle = $"display: block; left: {posX - (lensSize / 2)}px; top: {posY - (lensSize / 2)}px; " +
|
|
$"background-image: url('{_selectedUrl}'); " +
|
|
$"background-size: {zoomLevel * 100}%; " +
|
|
$"background-position: {(posX / 500.0) * 100}% {(posY / 500.0) * 100}%;";
|
|
}
|
|
|
|
private void HandleMouseLeave()
|
|
{
|
|
_zoomStyle = "display: none;";
|
|
}
|
|
|
|
private async Task SelectMedia(string url)
|
|
{
|
|
_selectedUrl = url;
|
|
HandleMouseLeave();
|
|
StateHasChanged();
|
|
await ScrollToActiveThumbnail();
|
|
}
|
|
|
|
private async Task NextMedia()
|
|
{
|
|
var list = MediaUrls.ToList();
|
|
var index = list.IndexOf(_selectedUrl ?? "");
|
|
if (index != -1)
|
|
{
|
|
var nextIndex = (index + 1) % list.Count;
|
|
await SelectMedia(list[nextIndex]);
|
|
}
|
|
}
|
|
|
|
private async Task PreviousMedia()
|
|
{
|
|
var list = MediaUrls.ToList();
|
|
var index = list.IndexOf(_selectedUrl ?? "");
|
|
if (index != -1)
|
|
{
|
|
var prevIndex = (index - 1 + list.Count) % list.Count;
|
|
await SelectMedia(list[prevIndex]);
|
|
}
|
|
}
|
|
|
|
private async Task ScrollToActiveThumbnail()
|
|
{
|
|
try
|
|
{
|
|
await JSRuntime.InvokeVoidAsync("eval", @"
|
|
var container = document.querySelector('.scrollbar-hide');
|
|
if (container) {
|
|
var activeThumb = container.querySelector('.border-sky-500');
|
|
if (activeThumb) {
|
|
activeThumb.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
|
|
}
|
|
}
|
|
");
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
private async Task Scroll(int amount)
|
|
{
|
|
try
|
|
{
|
|
await JSRuntime.InvokeVoidAsync("eval", $"document.querySelector('.scrollbar-hide').scrollBy({{ left: {amount}, behavior: 'smooth' }})");
|
|
}
|
|
catch { } // Fail gracefully if JS is busy
|
|
}
|
|
|
|
private enum MediaType { Image, Video, YouTube, Vimeo, Unknown }
|
|
|
|
private MediaType GetMediaType(string url)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(url)) return MediaType.Image;
|
|
url = url.ToLower();
|
|
if (url.Contains("youtube.com") || url.Contains("youtu.be")) return MediaType.YouTube;
|
|
if (url.Contains("vimeo.com")) return MediaType.Vimeo;
|
|
if (url.EndsWith(".mp4") || url.EndsWith(".webm") || url.EndsWith(".ogg")) return MediaType.Video;
|
|
return MediaType.Image;
|
|
}
|
|
|
|
private string GetYouTubeId(string url)
|
|
{
|
|
if (url.Contains("youtu.be/")) return url.Split("youtu.be/")[1].Split("?")[0];
|
|
if (url.Contains("v=")) return url.Split("v=")[1].Split("&")[0];
|
|
return "";
|
|
}
|
|
}
|
|
|
|
<style>
|
|
.scrollbar-hide::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
.scrollbar-hide {
|
|
-ms-overflow-style: none;
|
|
scrollbar-width: none;
|
|
}
|
|
|
|
.aspect-square {
|
|
aspect-ratio: 1 / 1;
|
|
}
|
|
|
|
.scroll-smooth {
|
|
scroll-behavior: smooth;
|
|
}
|
|
</style> |