2026-04-24 02:05:56 -05:00
|
|
|
|
@using Microsoft.Extensions.Options
|
|
|
|
|
|
@using LANCommander.Server.Settings
|
|
|
|
|
|
@using LANCommander.Server.Services.Abstractions
|
2026-04-27 17:58:48 -05:00
|
|
|
|
@using LANCommander.Server.Services.Models
|
2026-04-24 02:05:56 -05:00
|
|
|
|
@using Microsoft.EntityFrameworkCore
|
|
|
|
|
|
@using System.Net.Mime
|
2026-04-27 17:58:48 -05:00
|
|
|
|
@using LANCommander.UI.Providers
|
2026-04-24 02:05:56 -05:00
|
|
|
|
@inject MediaService MediaService
|
|
|
|
|
|
@inject GameService GameService
|
|
|
|
|
|
@inject IMediaGrabberService MediaGrabberService
|
|
|
|
|
|
@inject ModalService ModalService
|
|
|
|
|
|
@inject IMessageService MessageService
|
|
|
|
|
|
@inject IOptions<Settings> Settings
|
|
|
|
|
|
@inject ILogger<GameDetailPreview> Logger
|
2026-04-27 17:58:48 -05:00
|
|
|
|
@inject ScriptProvider ScriptProvider
|
2026-04-24 02:05:56 -05:00
|
|
|
|
|
|
|
|
|
|
<div class="game-detail-preview">
|
|
|
|
|
|
<!-- ══ Hero Section ═══════════════════════════════════════════ -->
|
|
|
|
|
|
<div class="game-detail-preview__hero">
|
|
|
|
|
|
@{
|
|
|
|
|
|
var bgMedia = GetMedia(MediaType.Background);
|
|
|
|
|
|
var bgUrl = GetMediaUrl(bgMedia);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@if (bgUrl != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
<img class="game-detail-preview__hero-bg" src="@bgUrl" />
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
<div class="game-detail-preview__hero-gradient-top"></div>
|
|
|
|
|
|
<div class="game-detail-preview__hero-gradient-bottom"></div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Background upload overlay -->
|
|
|
|
|
|
<div class="game-detail-preview__media-overlay game-detail-preview__media-overlay--hero">
|
|
|
|
|
|
<div class="game-detail-preview__media-overlay-actions">
|
|
|
|
|
|
@{ var bgInputId = "bg-upload-" + Guid.NewGuid(); }
|
2026-06-22 17:05:34 -05:00
|
|
|
|
<InputFile id="@bgInputId" OnChange="(e) => UploadMediaByType(e, MediaType.Background)" hidden accept=".jpg,.jpeg,.png,.gif,.webp" />
|
2026-04-24 02:05:56 -05:00
|
|
|
|
<label class="game-detail-preview__overlay-btn" for="@bgInputId" title="Upload Background">
|
|
|
|
|
|
<Icon Type="@IconType.Outline.Upload" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<button class="game-detail-preview__overlay-btn" @onclick="() => SearchMediaByType(MediaType.Background)" title="Search Background">
|
|
|
|
|
|
<Icon Type="@IconType.Outline.Search" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
@if (bgMedia != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
<button class="game-detail-preview__overlay-btn game-detail-preview__overlay-btn--danger" @onclick="() => RemoveMediaByType(MediaType.Background)" title="Remove Background">
|
|
|
|
|
|
<Icon Type="@IconType.Outline.Delete" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="game-detail-preview__media-overlay-label">Background</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Logo (bottom-left) -->
|
|
|
|
|
|
<div class="game-detail-preview__logo-area">
|
|
|
|
|
|
@{
|
|
|
|
|
|
var logoMedia = GetMedia(MediaType.Logo);
|
|
|
|
|
|
var logoUrl = GetMediaUrl(logoMedia);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@if (logoUrl != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
<img class="game-detail-preview__logo-img" src="@logoUrl" />
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
<span class="game-detail-preview__title-fallback">@_game?.Title</span>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Logo upload overlay -->
|
|
|
|
|
|
<div class="game-detail-preview__media-overlay game-detail-preview__media-overlay--logo">
|
|
|
|
|
|
<div class="game-detail-preview__media-overlay-actions">
|
|
|
|
|
|
@{ var logoInputId = "logo-upload-" + Guid.NewGuid(); }
|
2026-06-22 17:05:34 -05:00
|
|
|
|
<InputFile id="@logoInputId" OnChange="(e) => UploadMediaByType(e, MediaType.Logo)" hidden accept=".jpg,.jpeg,.png,.gif,.webp" />
|
2026-04-24 02:05:56 -05:00
|
|
|
|
<label class="game-detail-preview__overlay-btn" for="@logoInputId" title="Upload Logo">
|
|
|
|
|
|
<Icon Type="@IconType.Outline.Upload" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<button class="game-detail-preview__overlay-btn" @onclick="() => SearchMediaByType(MediaType.Logo)" title="Search Logo">
|
|
|
|
|
|
<Icon Type="@IconType.Outline.Search" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
@if (logoMedia != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
<button class="game-detail-preview__overlay-btn game-detail-preview__overlay-btn--danger" @onclick="() => RemoveMediaByType(MediaType.Logo)" title="Remove Logo">
|
|
|
|
|
|
<Icon Type="@IconType.Outline.Delete" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="game-detail-preview__media-overlay-label">Logo</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ══ Content Grid ═══════════════════════════════════════════ -->
|
|
|
|
|
|
<div class="game-detail-preview__content">
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Left Column -->
|
|
|
|
|
|
<div class="game-detail-preview__left">
|
|
|
|
|
|
<!-- Description -->
|
|
|
|
|
|
@if (!string.IsNullOrWhiteSpace(_game?.Description))
|
|
|
|
|
|
{
|
|
|
|
|
|
<p class="game-detail-preview__description">@_game.Description</p>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Screenshots & Videos Carousel -->
|
|
|
|
|
|
<div class="game-detail-preview__media-section">
|
|
|
|
|
|
<div class="game-detail-preview__media-section-header">
|
|
|
|
|
|
<span class="game-detail-preview__media-section-title">Media</span>
|
|
|
|
|
|
<div class="game-detail-preview__media-section-actions">
|
2026-06-22 17:05:34 -05:00
|
|
|
|
<InputFile id="@_ssInputId" OnChange="(e) => UploadMediaByType(e, MediaType.Screenshot)" hidden accept=".jpg,.jpeg,.png,.gif,.webp" multiple />
|
2026-04-30 23:47:15 -05:00
|
|
|
|
<InputFile id="@_vidInputId" OnChange="(e) => UploadMediaByType(e, MediaType.Video)" hidden accept=".mp4,.webm,.mkv" multiple />
|
2026-04-24 02:05:56 -05:00
|
|
|
|
|
|
|
|
|
|
<Dropdown>
|
|
|
|
|
|
<Overlay>
|
|
|
|
|
|
<Menu Mode="MenuMode.Vertical">
|
|
|
|
|
|
<SubMenu Key="screenshot" Title="Screenshot">
|
|
|
|
|
|
<MenuItem OnClick="() => ClickFileInput(_ssInputId)">
|
|
|
|
|
|
<Icon Type="@IconType.Outline.Upload" /> Upload
|
|
|
|
|
|
</MenuItem>
|
|
|
|
|
|
<MenuItem OnClick="() => SearchMediaByType(MediaType.Screenshot)">
|
|
|
|
|
|
<Icon Type="@IconType.Outline.Search" /> Search
|
|
|
|
|
|
</MenuItem>
|
|
|
|
|
|
</SubMenu>
|
|
|
|
|
|
<SubMenu Key="video" Title="Video">
|
|
|
|
|
|
<MenuItem OnClick="() => ClickFileInput(_vidInputId)">
|
|
|
|
|
|
<Icon Type="@IconType.Outline.Upload" /> Upload
|
|
|
|
|
|
</MenuItem>
|
|
|
|
|
|
<MenuItem OnClick="() => SearchMediaByType(MediaType.Video)">
|
|
|
|
|
|
<Icon Type="@IconType.Outline.Search" /> Search
|
|
|
|
|
|
</MenuItem>
|
|
|
|
|
|
</SubMenu>
|
|
|
|
|
|
</Menu>
|
|
|
|
|
|
</Overlay>
|
|
|
|
|
|
<ChildContent>
|
|
|
|
|
|
<Button Size="@ButtonSize.Small" Type="ButtonType.Text">
|
|
|
|
|
|
<Icon Type="@IconType.Outline.Plus" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</ChildContent>
|
|
|
|
|
|
</Dropdown>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="game-detail-preview__carousel">
|
|
|
|
|
|
@foreach (var media in GetMediaList(MediaType.Screenshot, MediaType.Video))
|
|
|
|
|
|
{
|
|
|
|
|
|
<div class="game-detail-preview__carousel-item">
|
|
|
|
|
|
@if (media.Type == MediaType.Video)
|
|
|
|
|
|
{
|
|
|
|
|
|
<video class="game-detail-preview__carousel-video" src="@GetMediaDownloadUrl(media)" muted loop />
|
|
|
|
|
|
<div class="game-detail-preview__carousel-video-badge">
|
|
|
|
|
|
<Icon Type="@IconType.Outline.PlayCircle" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
<img class="game-detail-preview__carousel-img" src="@GetMediaDownloadUrl(media)" />
|
|
|
|
|
|
}
|
|
|
|
|
|
<div class="game-detail-preview__carousel-item-overlay">
|
|
|
|
|
|
<button class="game-detail-preview__overlay-btn" @onclick="() => MoveMediaLeft(media)" title="Move Left">
|
|
|
|
|
|
<Icon Type="@IconType.Outline.Left" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button class="game-detail-preview__overlay-btn" @onclick="() => MoveMediaRight(media)" title="Move Right">
|
|
|
|
|
|
<Icon Type="@IconType.Outline.Right" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<Popconfirm Title="Remove this media?" OnConfirm="() => RemoveMedia(media)">
|
|
|
|
|
|
<button class="game-detail-preview__overlay-btn game-detail-preview__overlay-btn--danger" title="Remove">
|
|
|
|
|
|
<Icon Type="@IconType.Outline.Delete" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</Popconfirm>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@if (!GetMediaList(MediaType.Screenshot, MediaType.Video).Any())
|
|
|
|
|
|
{
|
|
|
|
|
|
<div class="game-detail-preview__carousel-empty">
|
|
|
|
|
|
<Empty Description="@("No screenshots or videos")" Simple />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Right Column -->
|
|
|
|
|
|
<div class="game-detail-preview__right">
|
|
|
|
|
|
<!-- Cover Image -->
|
|
|
|
|
|
<div class="game-detail-preview__cover-wrapper">
|
|
|
|
|
|
@{
|
|
|
|
|
|
var coverMedia = GetMedia(MediaType.Cover);
|
|
|
|
|
|
var coverUrl = GetMediaUrl(coverMedia);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 23:47:15 -05:00
|
|
|
|
@if (coverUrl != null && coverMedia?.MimeType?.StartsWith("video/") == true)
|
|
|
|
|
|
{
|
|
|
|
|
|
<video class="game-detail-preview__cover-img" src="@coverUrl" autoplay muted loop />
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (coverUrl != null)
|
2026-04-24 02:05:56 -05:00
|
|
|
|
{
|
|
|
|
|
|
<img class="game-detail-preview__cover-img" src="@coverUrl" />
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
<div class="game-detail-preview__cover-placeholder">
|
|
|
|
|
|
<span>@_game?.Title</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Cover upload overlay -->
|
|
|
|
|
|
<div class="game-detail-preview__media-overlay game-detail-preview__media-overlay--cover">
|
|
|
|
|
|
<div class="game-detail-preview__media-overlay-actions">
|
|
|
|
|
|
@{ var coverInputId = "cover-upload-" + Guid.NewGuid(); }
|
2026-06-22 17:05:34 -05:00
|
|
|
|
<InputFile id="@coverInputId" OnChange="(e) => UploadMediaByType(e, MediaType.Cover)" hidden accept=".jpg,.jpeg,.png,.gif,.webp,.apng,.mp4,.webm,.mkv" />
|
2026-04-24 02:05:56 -05:00
|
|
|
|
<label class="game-detail-preview__overlay-btn" for="@coverInputId" title="Upload Cover">
|
|
|
|
|
|
<Icon Type="@IconType.Outline.Upload" />
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<button class="game-detail-preview__overlay-btn" @onclick="() => SearchMediaByType(MediaType.Cover)" title="Search Cover">
|
|
|
|
|
|
<Icon Type="@IconType.Outline.Search" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
@if (coverMedia != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
<button class="game-detail-preview__overlay-btn game-detail-preview__overlay-btn--danger" @onclick="() => RemoveMediaByType(MediaType.Cover)" title="Remove Cover">
|
|
|
|
|
|
<Icon Type="@IconType.Outline.Delete" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="game-detail-preview__media-overlay-label">Cover</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Metadata Badges -->
|
|
|
|
|
|
@if (_game?.Singleplayer == true || (_game?.MultiplayerModes?.Any() ?? false))
|
|
|
|
|
|
{
|
|
|
|
|
|
<div class="game-detail-preview__badge-section">
|
|
|
|
|
|
<span class="game-detail-preview__badge-header">Players</span>
|
|
|
|
|
|
<div class="game-detail-preview__badge-wrap">
|
|
|
|
|
|
@if (_game.Singleplayer)
|
|
|
|
|
|
{
|
|
|
|
|
|
<span class="game-detail-preview__badge">Singleplayer</span>
|
|
|
|
|
|
}
|
|
|
|
|
|
@if (_game.MultiplayerModes != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
@foreach (var mode in _game.MultiplayerModes)
|
|
|
|
|
|
{
|
|
|
|
|
|
<span class="game-detail-preview__badge">
|
|
|
|
|
|
@mode.Type.GetDisplayName()
|
|
|
|
|
|
@if (mode.MinPlayers > 0 && mode.MaxPlayers > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
<text> · @mode.MinPlayers–@mode.MaxPlayers players</text>
|
|
|
|
|
|
}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@if (_game?.Developers?.Any() == true)
|
|
|
|
|
|
{
|
|
|
|
|
|
<div class="game-detail-preview__badge-section">
|
|
|
|
|
|
<span class="game-detail-preview__badge-header">Developers</span>
|
|
|
|
|
|
<div class="game-detail-preview__badge-wrap">
|
|
|
|
|
|
@foreach (var dev in _game.Developers)
|
|
|
|
|
|
{
|
|
|
|
|
|
<span class="game-detail-preview__badge">@dev.Name</span>
|
|
|
|
|
|
}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@if (_game?.Publishers?.Any() == true)
|
|
|
|
|
|
{
|
|
|
|
|
|
<div class="game-detail-preview__badge-section">
|
|
|
|
|
|
<span class="game-detail-preview__badge-header">Publishers</span>
|
|
|
|
|
|
<div class="game-detail-preview__badge-wrap">
|
|
|
|
|
|
@foreach (var pub in _game.Publishers)
|
|
|
|
|
|
{
|
|
|
|
|
|
<span class="game-detail-preview__badge">@pub.Name</span>
|
|
|
|
|
|
}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@if (_game?.Genres?.Any() == true)
|
|
|
|
|
|
{
|
|
|
|
|
|
<div class="game-detail-preview__badge-section">
|
|
|
|
|
|
<span class="game-detail-preview__badge-header">Genres</span>
|
|
|
|
|
|
<div class="game-detail-preview__badge-wrap">
|
|
|
|
|
|
@foreach (var genre in _game.Genres)
|
|
|
|
|
|
{
|
|
|
|
|
|
<span class="game-detail-preview__badge">@genre.Name</span>
|
|
|
|
|
|
}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@if (_game?.Tags?.Any() == true)
|
|
|
|
|
|
{
|
|
|
|
|
|
<div class="game-detail-preview__badge-section">
|
|
|
|
|
|
<span class="game-detail-preview__badge-header">Tags</span>
|
|
|
|
|
|
<div class="game-detail-preview__badge-wrap">
|
|
|
|
|
|
@foreach (var tag in _game.Tags)
|
|
|
|
|
|
{
|
|
|
|
|
|
<span class="game-detail-preview__badge">@tag.Name</span>
|
|
|
|
|
|
}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
@code {
|
|
|
|
|
|
[CascadingParameter(Name = "GameId")] public Guid GameId { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
string _ssInputId = "ss-upload-" + Guid.NewGuid();
|
|
|
|
|
|
string _vidInputId = "vid-upload-" + Guid.NewGuid();
|
|
|
|
|
|
|
|
|
|
|
|
string[] ValidImageMimeTypes = [
|
|
|
|
|
|
MediaTypeNames.Image.Png,
|
|
|
|
|
|
MediaTypeNames.Image.Jpeg,
|
|
|
|
|
|
MediaTypeNames.Image.Gif,
|
2026-06-22 17:05:34 -05:00
|
|
|
|
MediaTypeNames.Image.Webp,
|
2026-04-30 23:47:15 -05:00
|
|
|
|
"image/apng",
|
2026-04-24 02:05:56 -05:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
string[] ValidVideoMimeTypes = [
|
|
|
|
|
|
"video/mp4",
|
|
|
|
|
|
"video/webm",
|
|
|
|
|
|
"video/x-matroska"
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-04-30 23:47:15 -05:00
|
|
|
|
string[] ValidCoverMimeTypes = [
|
|
|
|
|
|
MediaTypeNames.Image.Png,
|
|
|
|
|
|
MediaTypeNames.Image.Jpeg,
|
|
|
|
|
|
MediaTypeNames.Image.Gif,
|
2026-06-22 17:05:34 -05:00
|
|
|
|
MediaTypeNames.Image.Webp,
|
2026-04-30 23:47:15 -05:00
|
|
|
|
"image/apng",
|
|
|
|
|
|
"video/mp4",
|
|
|
|
|
|
"video/webm",
|
|
|
|
|
|
"video/x-matroska"
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-04-24 02:05:56 -05:00
|
|
|
|
List<Media> _media = new();
|
|
|
|
|
|
Game _game;
|
2026-04-27 17:58:48 -05:00
|
|
|
|
IJSObjectReference _domHelper;
|
2026-04-24 02:05:56 -05:00
|
|
|
|
|
|
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
_game = await GameService.Query(q =>
|
|
|
|
|
|
{
|
|
|
|
|
|
return q
|
|
|
|
|
|
.Include(g => g.Media)
|
|
|
|
|
|
.ThenInclude(m => m.StorageLocation)
|
|
|
|
|
|
.Include(g => g.Developers)
|
|
|
|
|
|
.Include(g => g.Publishers)
|
|
|
|
|
|
.Include(g => g.Genres)
|
|
|
|
|
|
.Include(g => g.Tags)
|
|
|
|
|
|
.Include(g => g.MultiplayerModes);
|
|
|
|
|
|
})
|
|
|
|
|
|
.GetAsync(GameId);
|
|
|
|
|
|
|
|
|
|
|
|
_media = _game.Media?.ToList() ?? new();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-27 17:58:48 -05:00
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (firstRender)
|
|
|
|
|
|
_domHelper = await ScriptProvider.ImportModuleAsync("DomHelper");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-24 02:05:56 -05:00
|
|
|
|
async Task ClickFileInput(string inputId) =>
|
2026-04-27 17:58:48 -05:00
|
|
|
|
await _domHelper.InvokeVoidAsync("ClickElement", inputId);
|
2026-04-24 02:05:56 -05:00
|
|
|
|
|
|
|
|
|
|
Media? GetMedia(MediaType type) =>
|
|
|
|
|
|
_media.FirstOrDefault(m => m.Type == type);
|
|
|
|
|
|
|
|
|
|
|
|
string? GetMediaUrl(Media? media) =>
|
|
|
|
|
|
media != null && MediaService.FileExists(media)
|
|
|
|
|
|
? $"/api/Media/{media.Id}/Download?fileId={media.FileId}&v={media.UpdatedOn.Ticks}"
|
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
|
|
string GetMediaDownloadUrl(Media media) =>
|
|
|
|
|
|
$"/api/Media/{media.Id}/Download?fileId={media.FileId}&v={media.UpdatedOn.Ticks}";
|
|
|
|
|
|
|
|
|
|
|
|
List<Media> GetMediaList(params MediaType[] types) =>
|
|
|
|
|
|
_media.Where(m => types.Contains(m.Type) && MediaService.FileExists(m))
|
|
|
|
|
|
.OrderBy(m => m.SortOrder)
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
|
|
async Task UploadMediaByType(InputFileChangeEventArgs e, MediaType type)
|
|
|
|
|
|
{
|
2026-04-30 23:47:15 -05:00
|
|
|
|
var validTypes = type switch
|
2026-04-24 02:05:56 -05:00
|
|
|
|
{
|
2026-04-30 23:47:15 -05:00
|
|
|
|
MediaType.Video => ValidVideoMimeTypes,
|
|
|
|
|
|
MediaType.Cover => ValidCoverMimeTypes,
|
|
|
|
|
|
_ => ValidImageMimeTypes
|
|
|
|
|
|
};
|
2026-04-24 02:05:56 -05:00
|
|
|
|
var config = Settings.Value.Server.Media.GetMediaTypeConfig(type);
|
2026-04-30 23:47:15 -05:00
|
|
|
|
var isSingular = type != MediaType.Screenshot && type != MediaType.Video;
|
2026-04-24 02:05:56 -05:00
|
|
|
|
|
2026-04-30 23:47:15 -05:00
|
|
|
|
foreach (var file in e.GetMultipleFiles())
|
2026-04-24 02:05:56 -05:00
|
|
|
|
{
|
2026-04-30 23:47:15 -05:00
|
|
|
|
if (!validTypes.Contains(file.ContentType))
|
|
|
|
|
|
{
|
|
|
|
|
|
MessageService.Error($"Unsupported file type: {file.Name}");
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-04-24 02:05:56 -05:00
|
|
|
|
|
2026-04-30 23:47:15 -05:00
|
|
|
|
if (file.Size > config.MaxFileSize)
|
|
|
|
|
|
{
|
|
|
|
|
|
MessageService.Error($"File size must be smaller than {ByteSizeLib.ByteSize.FromBytes(config.MaxFileSize)}: {file.Name}");
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-04-24 02:05:56 -05:00
|
|
|
|
|
2026-04-30 23:47:15 -05:00
|
|
|
|
// For singular types (Background, Logo, Cover), replace existing
|
|
|
|
|
|
var existing = isSingular ? GetMedia(type) : null;
|
2026-04-24 02:05:56 -05:00
|
|
|
|
|
2026-04-30 23:47:15 -05:00
|
|
|
|
Media media;
|
2026-04-24 02:05:56 -05:00
|
|
|
|
|
2026-04-30 23:47:15 -05:00
|
|
|
|
if (existing != null)
|
2026-04-24 02:05:56 -05:00
|
|
|
|
{
|
2026-04-30 23:47:15 -05:00
|
|
|
|
media = existing;
|
|
|
|
|
|
media.MimeType = file.ContentType;
|
|
|
|
|
|
media.SourceUrl = "";
|
|
|
|
|
|
|
|
|
|
|
|
MediaService.DeleteLocalMediaFile(media);
|
|
|
|
|
|
media = await MediaService.WriteToFileAsync(media, file.OpenReadStream(maxAllowedSize: config.MaxFileSize));
|
|
|
|
|
|
await MediaService.UpdateAsync(media);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
media = new Media
|
|
|
|
|
|
{
|
|
|
|
|
|
GameId = GameId,
|
|
|
|
|
|
Type = type,
|
|
|
|
|
|
MimeType = file.ContentType,
|
|
|
|
|
|
SourceUrl = "",
|
|
|
|
|
|
StorageLocation = await MediaService.GetDefaultStorageLocationAsync(),
|
|
|
|
|
|
Crc32 = string.Empty,
|
|
|
|
|
|
SortOrder = GetNextSortOrder(type)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
media = await MediaService.WriteToFileAsync(media, file.OpenReadStream(maxAllowedSize: config.MaxFileSize));
|
|
|
|
|
|
}
|
2026-04-24 02:05:56 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await RefreshMedia();
|
|
|
|
|
|
MessageService.Success($"{type} uploaded!");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async Task SearchMediaByType(MediaType type)
|
|
|
|
|
|
{
|
2026-04-30 23:47:15 -05:00
|
|
|
|
var multiSelect = type == MediaType.Screenshot || type == MediaType.Video;
|
2026-04-30 02:16:17 -05:00
|
|
|
|
|
2026-04-24 02:05:56 -05:00
|
|
|
|
var modalOptions = new ModalOptions
|
|
|
|
|
|
{
|
|
|
|
|
|
Title = $"Download {type}",
|
|
|
|
|
|
Maximizable = false,
|
|
|
|
|
|
DefaultMaximized = true,
|
|
|
|
|
|
Closable = true,
|
|
|
|
|
|
OkText = "Select",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
var grabberOptions = new MediaGrabberOptions
|
|
|
|
|
|
{
|
|
|
|
|
|
Type = type,
|
2026-04-30 02:16:17 -05:00
|
|
|
|
Search = _game.Title,
|
|
|
|
|
|
MultiSelect = multiSelect
|
2026-04-24 02:05:56 -05:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-27 17:58:48 -05:00
|
|
|
|
var modalRef = await ModalService.CreateModalAsync<MediaGrabberDialog, MediaGrabberOptions, MediaGrabberDownloadResult>(modalOptions, grabberOptions);
|
2026-04-24 02:05:56 -05:00
|
|
|
|
|
2026-04-27 17:58:48 -05:00
|
|
|
|
modalRef.OnOk = async (downloadResult) =>
|
2026-04-24 02:05:56 -05:00
|
|
|
|
{
|
2026-04-30 02:16:17 -05:00
|
|
|
|
if (multiSelect && downloadResult.Results.Any())
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var entry in downloadResult.Results)
|
|
|
|
|
|
{
|
|
|
|
|
|
using var download = entry.Download;
|
|
|
|
|
|
|
|
|
|
|
|
var media = new Media
|
|
|
|
|
|
{
|
|
|
|
|
|
GameId = GameId,
|
|
|
|
|
|
Type = type,
|
|
|
|
|
|
SourceUrl = entry.Result.SourceUrl,
|
|
|
|
|
|
MimeType = download.MimeType,
|
|
|
|
|
|
StorageLocation = await MediaService.GetDefaultStorageLocationAsync(),
|
|
|
|
|
|
Crc32 = string.Empty,
|
|
|
|
|
|
SortOrder = GetNextSortOrder(type)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
await MediaService.WriteToFileAsync(media, download.Stream);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await RefreshMedia();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
using var singleDownload = downloadResult.Download;
|
2026-04-24 02:05:56 -05:00
|
|
|
|
|
|
|
|
|
|
var isSingular = type != MediaType.Screenshot && type != MediaType.Video;
|
|
|
|
|
|
var existing = isSingular ? GetMedia(type) : null;
|
|
|
|
|
|
|
2026-04-30 02:16:17 -05:00
|
|
|
|
Media singleMedia;
|
2026-04-24 02:05:56 -05:00
|
|
|
|
|
|
|
|
|
|
if (existing != null)
|
|
|
|
|
|
{
|
2026-04-30 02:16:17 -05:00
|
|
|
|
singleMedia = existing;
|
|
|
|
|
|
singleMedia.SourceUrl = downloadResult.Result.SourceUrl;
|
|
|
|
|
|
singleMedia.MimeType = singleDownload.MimeType;
|
2026-04-24 02:05:56 -05:00
|
|
|
|
|
2026-04-30 02:16:17 -05:00
|
|
|
|
MediaService.DeleteLocalMediaFile(singleMedia);
|
|
|
|
|
|
singleMedia = await MediaService.WriteToFileAsync(singleMedia, singleDownload.Stream);
|
2026-04-24 02:05:56 -05:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2026-04-30 02:16:17 -05:00
|
|
|
|
singleMedia = new Media
|
2026-04-24 02:05:56 -05:00
|
|
|
|
{
|
|
|
|
|
|
GameId = GameId,
|
|
|
|
|
|
Type = type,
|
2026-04-27 17:58:48 -05:00
|
|
|
|
SourceUrl = downloadResult.Result.SourceUrl,
|
2026-04-30 02:16:17 -05:00
|
|
|
|
MimeType = singleDownload.MimeType,
|
2026-04-24 02:05:56 -05:00
|
|
|
|
StorageLocation = await MediaService.GetDefaultStorageLocationAsync(),
|
|
|
|
|
|
Crc32 = string.Empty,
|
|
|
|
|
|
SortOrder = GetNextSortOrder(type)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-04-30 02:16:17 -05:00
|
|
|
|
singleMedia = await MediaService.WriteToFileAsync(singleMedia, singleDownload.Stream);
|
2026-04-24 02:05:56 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await RefreshMedia();
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async Task RemoveMediaByType(MediaType type)
|
|
|
|
|
|
{
|
|
|
|
|
|
var media = GetMedia(type);
|
|
|
|
|
|
|
|
|
|
|
|
if (media != null)
|
|
|
|
|
|
await RemoveMedia(media);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async Task RemoveMedia(Media media)
|
|
|
|
|
|
{
|
|
|
|
|
|
_media.Remove(media);
|
|
|
|
|
|
|
|
|
|
|
|
if (media.Id != Guid.Empty)
|
|
|
|
|
|
await MediaService.DeleteAsync(media);
|
|
|
|
|
|
|
|
|
|
|
|
await InvokeAsync(StateHasChanged);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int GetNextSortOrder(MediaType type)
|
|
|
|
|
|
{
|
|
|
|
|
|
var existing = _media.Where(m => m.Type == type).ToList();
|
|
|
|
|
|
return existing.Any() ? existing.Max(m => m.SortOrder) + 1 : 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async Task NormalizeSortOrders()
|
|
|
|
|
|
{
|
|
|
|
|
|
var list = _media
|
|
|
|
|
|
.Where(m => m.Type == MediaType.Screenshot || m.Type == MediaType.Video)
|
|
|
|
|
|
.OrderBy(m => m.SortOrder)
|
|
|
|
|
|
.ThenBy(m => m.CreatedOn)
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < list.Count; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (list[i].SortOrder != i)
|
|
|
|
|
|
{
|
|
|
|
|
|
list[i].SortOrder = i;
|
|
|
|
|
|
await MediaService.UpdateAsync(list[i]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async Task MoveMediaLeft(Media media)
|
|
|
|
|
|
{
|
|
|
|
|
|
await NormalizeSortOrders();
|
|
|
|
|
|
|
|
|
|
|
|
var list = GetMediaList(MediaType.Screenshot, MediaType.Video);
|
|
|
|
|
|
var index = list.IndexOf(media);
|
|
|
|
|
|
|
|
|
|
|
|
if (index <= 0)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
// Swap sort orders
|
|
|
|
|
|
(list[index].SortOrder, list[index - 1].SortOrder) = (list[index - 1].SortOrder, list[index].SortOrder);
|
|
|
|
|
|
|
|
|
|
|
|
await MediaService.UpdateAsync(list[index]);
|
|
|
|
|
|
await MediaService.UpdateAsync(list[index - 1]);
|
|
|
|
|
|
await RefreshMedia();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async Task MoveMediaRight(Media media)
|
|
|
|
|
|
{
|
|
|
|
|
|
await NormalizeSortOrders();
|
|
|
|
|
|
|
|
|
|
|
|
var list = GetMediaList(MediaType.Screenshot, MediaType.Video);
|
|
|
|
|
|
var index = list.IndexOf(media);
|
|
|
|
|
|
|
|
|
|
|
|
if (index < 0 || index >= list.Count - 1)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
// Swap sort orders
|
|
|
|
|
|
(list[index].SortOrder, list[index + 1].SortOrder) = (list[index + 1].SortOrder, list[index].SortOrder);
|
|
|
|
|
|
|
|
|
|
|
|
await MediaService.UpdateAsync(list[index]);
|
|
|
|
|
|
await MediaService.UpdateAsync(list[index + 1]);
|
|
|
|
|
|
await RefreshMedia();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async Task RefreshMedia()
|
|
|
|
|
|
{
|
|
|
|
|
|
_media = (await MediaService.Include(m => m.StorageLocation).GetAsync(m => m.GameId == GameId)).ToList();
|
|
|
|
|
|
await InvokeAsync(StateHasChanged);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|