617 lines
25 KiB
Text
617 lines
25 KiB
Text
@using Microsoft.Extensions.Options
|
||
@using LANCommander.Server.Settings
|
||
@using LANCommander.Server.Services.Abstractions
|
||
@using LANCommander.Server.Services.Models
|
||
@using Microsoft.EntityFrameworkCore
|
||
@using System.Net.Mime
|
||
@using LANCommander.UI.Providers
|
||
@inject MediaService MediaService
|
||
@inject GameService GameService
|
||
@inject IMediaGrabberService MediaGrabberService
|
||
@inject ModalService ModalService
|
||
@inject IMessageService MessageService
|
||
@inject IOptions<Settings> Settings
|
||
@inject ILogger<GameDetailPreview> Logger
|
||
@inject ScriptProvider ScriptProvider
|
||
|
||
<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(); }
|
||
<InputFile id="@bgInputId" OnChange="(e) => UploadMediaByType(e, MediaType.Background)" hidden accept=".jpg,.jpeg,.png,.gif,.webp" />
|
||
<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(); }
|
||
<InputFile id="@logoInputId" OnChange="(e) => UploadMediaByType(e, MediaType.Logo)" hidden accept=".jpg,.jpeg,.png,.gif,.webp" />
|
||
<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">
|
||
<InputFile id="@_ssInputId" OnChange="(e) => UploadMediaByType(e, MediaType.Screenshot)" hidden accept=".jpg,.jpeg,.png,.gif,.webp" multiple />
|
||
<InputFile id="@_vidInputId" OnChange="(e) => UploadMediaByType(e, MediaType.Video)" hidden accept=".mp4,.webm,.mkv" multiple />
|
||
|
||
<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);
|
||
}
|
||
|
||
@if (coverUrl != null && coverMedia?.MimeType?.StartsWith("video/") == true)
|
||
{
|
||
<video class="game-detail-preview__cover-img" src="@coverUrl" autoplay muted loop />
|
||
}
|
||
else if (coverUrl != null)
|
||
{
|
||
<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(); }
|
||
<InputFile id="@coverInputId" OnChange="(e) => UploadMediaByType(e, MediaType.Cover)" hidden accept=".jpg,.jpeg,.png,.gif,.webp,.apng,.mp4,.webm,.mkv" />
|
||
<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,
|
||
MediaTypeNames.Image.Webp,
|
||
"image/apng",
|
||
];
|
||
|
||
string[] ValidVideoMimeTypes = [
|
||
"video/mp4",
|
||
"video/webm",
|
||
"video/x-matroska"
|
||
];
|
||
|
||
string[] ValidCoverMimeTypes = [
|
||
MediaTypeNames.Image.Png,
|
||
MediaTypeNames.Image.Jpeg,
|
||
MediaTypeNames.Image.Gif,
|
||
MediaTypeNames.Image.Webp,
|
||
"image/apng",
|
||
"video/mp4",
|
||
"video/webm",
|
||
"video/x-matroska"
|
||
];
|
||
|
||
List<Media> _media = new();
|
||
Game _game;
|
||
IJSObjectReference _domHelper;
|
||
|
||
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();
|
||
}
|
||
|
||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||
{
|
||
if (firstRender)
|
||
_domHelper = await ScriptProvider.ImportModuleAsync("DomHelper");
|
||
}
|
||
|
||
async Task ClickFileInput(string inputId) =>
|
||
await _domHelper.InvokeVoidAsync("ClickElement", inputId);
|
||
|
||
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)
|
||
{
|
||
var validTypes = type switch
|
||
{
|
||
MediaType.Video => ValidVideoMimeTypes,
|
||
MediaType.Cover => ValidCoverMimeTypes,
|
||
_ => ValidImageMimeTypes
|
||
};
|
||
var config = Settings.Value.Server.Media.GetMediaTypeConfig(type);
|
||
var isSingular = type != MediaType.Screenshot && type != MediaType.Video;
|
||
|
||
foreach (var file in e.GetMultipleFiles())
|
||
{
|
||
if (!validTypes.Contains(file.ContentType))
|
||
{
|
||
MessageService.Error($"Unsupported file type: {file.Name}");
|
||
continue;
|
||
}
|
||
|
||
if (file.Size > config.MaxFileSize)
|
||
{
|
||
MessageService.Error($"File size must be smaller than {ByteSizeLib.ByteSize.FromBytes(config.MaxFileSize)}: {file.Name}");
|
||
continue;
|
||
}
|
||
|
||
// For singular types (Background, Logo, Cover), replace existing
|
||
var existing = isSingular ? GetMedia(type) : null;
|
||
|
||
Media media;
|
||
|
||
if (existing != null)
|
||
{
|
||
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));
|
||
}
|
||
}
|
||
|
||
await RefreshMedia();
|
||
MessageService.Success($"{type} uploaded!");
|
||
}
|
||
|
||
async Task SearchMediaByType(MediaType type)
|
||
{
|
||
var multiSelect = type == MediaType.Screenshot || type == MediaType.Video;
|
||
|
||
var modalOptions = new ModalOptions
|
||
{
|
||
Title = $"Download {type}",
|
||
Maximizable = false,
|
||
DefaultMaximized = true,
|
||
Closable = true,
|
||
OkText = "Select",
|
||
};
|
||
|
||
var grabberOptions = new MediaGrabberOptions
|
||
{
|
||
Type = type,
|
||
Search = _game.Title,
|
||
MultiSelect = multiSelect
|
||
};
|
||
|
||
var modalRef = await ModalService.CreateModalAsync<MediaGrabberDialog, MediaGrabberOptions, MediaGrabberDownloadResult>(modalOptions, grabberOptions);
|
||
|
||
modalRef.OnOk = async (downloadResult) =>
|
||
{
|
||
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;
|
||
|
||
var isSingular = type != MediaType.Screenshot && type != MediaType.Video;
|
||
var existing = isSingular ? GetMedia(type) : null;
|
||
|
||
Media singleMedia;
|
||
|
||
if (existing != null)
|
||
{
|
||
singleMedia = existing;
|
||
singleMedia.SourceUrl = downloadResult.Result.SourceUrl;
|
||
singleMedia.MimeType = singleDownload.MimeType;
|
||
|
||
MediaService.DeleteLocalMediaFile(singleMedia);
|
||
singleMedia = await MediaService.WriteToFileAsync(singleMedia, singleDownload.Stream);
|
||
}
|
||
else
|
||
{
|
||
singleMedia = new Media
|
||
{
|
||
GameId = GameId,
|
||
Type = type,
|
||
SourceUrl = downloadResult.Result.SourceUrl,
|
||
MimeType = singleDownload.MimeType,
|
||
StorageLocation = await MediaService.GetDefaultStorageLocationAsync(),
|
||
Crc32 = string.Empty,
|
||
SortOrder = GetNextSortOrder(type)
|
||
};
|
||
|
||
singleMedia = await MediaService.WriteToFileAsync(singleMedia, singleDownload.Stream);
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|