mirror of https://github.com/ErsatzTV/ErsatzTV.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
260 lines
10 KiB
260 lines
10 KiB
@page "/media/sources/local/{Id:int}/edit" |
|
@page "/media/sources/local/add" |
|
@using ErsatzTV.Application.Libraries |
|
@implements IDisposable |
|
@inject IDialogService Dialog |
|
@inject IEntityLocker Locker |
|
@inject IMediator Mediator |
|
@inject ILogger<LocalLibraryEditor> Logger |
|
@inject ISnackbar Snackbar |
|
@inject NavigationManager NavigationManager |
|
|
|
<MudForm Model="@_model" @bind-IsValid="@_success" Style="max-height: 100%"> |
|
<MudPaper Square="true" Style="display: flex; height: 64px; min-height: 64px; width: 100%; z-index: 100; align-items: center"> |
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="ml-6" OnClick="SaveChangesAsync" StartIcon="@(IsEdit ? Icons.Material.Filled.Save : Icons.Material.Filled.Add)">@(IsEdit ? "Save Local Library" : "Add Local Library")</MudButton> |
|
</MudPaper> |
|
<div class="d-flex flex-column" style="height: 100vh; overflow-x: auto"> |
|
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8"> |
|
<MudText Typo="Typo.h5" Class="mb-2">Local Library</MudText> |
|
<MudDivider Class="mb-6"/> |
|
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> |
|
<div class="d-flex"> |
|
<MudText>Name</MudText> |
|
</div> |
|
<MudTextField @bind-Value="_model.Name" For="@(() => _model.Name)" Required="true" RequiredError="Local library name is required!"/> |
|
</MudStack> |
|
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> |
|
<div class="d-flex"> |
|
<MudText>Media Kind</MudText> |
|
</div> |
|
<MudSelect Disabled="IsEdit" @bind-Value="_model.MediaKind" For="@(() => _model.MediaKind)"> |
|
@foreach (LibraryMediaKind mediaKind in Enum.GetValues<LibraryMediaKind>()) |
|
{ |
|
<MudSelectItem Value="@mediaKind">@mediaKind</MudSelectItem> |
|
} |
|
</MudSelect> |
|
</MudStack> |
|
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> |
|
<div class="d-flex"> |
|
<MudText>Path</MudText> |
|
</div> |
|
<MudTextField @bind-Value="_newPath.Path" For="@(() => _newPath.Path)"/> |
|
</MudStack> |
|
<MudStack Row="true" Breakpoint="Breakpoint.SmAndDown" Class="form-field-stack gap-md-8 mb-5"> |
|
<div class="d-flex"></div> |
|
<MudButton Variant="Variant.Filled" Color="Color.Secondary" OnClick="@(_ => AddLibraryPath())" StartIcon="@Icons.Material.Filled.Add"> |
|
Add Path |
|
</MudButton> |
|
</MudStack> |
|
<MudTable Hover="true" Items="_model.Paths" Class="mt-6"> |
|
<ToolBarContent> |
|
<MudText Typo="Typo.h6">Library Paths</MudText> |
|
</ToolBarContent> |
|
<ColGroup> |
|
<MudHidden Breakpoint="Breakpoint.Xs"> |
|
<col/> |
|
<col style="width: 120px;"/> |
|
</MudHidden> |
|
</ColGroup> |
|
<HeaderContent> |
|
<MudTh>Path</MudTh> |
|
<MudTh/> |
|
</HeaderContent> |
|
<RowTemplate> |
|
<MudTd>@context.Path</MudTd> |
|
<MudTd> |
|
<div style="align-items: center; display: flex;"> |
|
<MudTooltip Text="Move Library Path"> |
|
<MudIconButton Icon="@Icons.Material.Filled.DriveFileMove" |
|
Disabled="@(_model.Id == 0 || context.Id == 0 || Locker.IsLibraryLocked(_model.Id))" |
|
OnClick="@(() => MoveLibraryPath(context))"> |
|
</MudIconButton> |
|
</MudTooltip> |
|
<MudTooltip Text="Delete Library Path"> |
|
<MudIconButton Icon="@Icons.Material.Filled.Delete" |
|
OnClick="@(() => DeleteLibraryPath(context))"> |
|
</MudIconButton> |
|
</MudTooltip> |
|
</div> |
|
</MudTd> |
|
</RowTemplate> |
|
</MudTable> |
|
</MudContainer> |
|
</div> |
|
</MudForm> |
|
|
|
@code { |
|
private readonly CancellationTokenSource _cts = new(); |
|
|
|
[Parameter] |
|
public int Id { get; set; } |
|
|
|
private readonly LocalLibraryEditViewModel _model = new(); |
|
private readonly LocalLibraryPathEditViewModel _newPath = new(); |
|
private bool _success; |
|
|
|
private bool IsEdit => Id != 0; |
|
|
|
protected override async Task OnParametersSetAsync() |
|
{ |
|
if (IsEdit) |
|
{ |
|
Option<LocalLibraryViewModel> maybeLibrary = await Mediator.Send(new GetLocalLibraryById(Id), _cts.Token); |
|
await maybeLibrary.Match( |
|
async library => |
|
{ |
|
_model.Id = library.Id; |
|
_model.Name = library.Name; |
|
_model.MediaKind = library.MediaKind; |
|
|
|
await LoadLibraryPaths(); |
|
}, |
|
() => |
|
{ |
|
NavigationManager.NavigateTo("404"); |
|
return Task.CompletedTask; |
|
}); |
|
} |
|
else |
|
{ |
|
_model.HasChanges = true; |
|
_model.Name = "New Local Library"; |
|
_model.MediaKind = LibraryMediaKind.Movies; |
|
_model.Paths = new List<LocalLibraryPathEditViewModel>(); |
|
} |
|
} |
|
|
|
protected override void OnInitialized() |
|
{ |
|
Locker.OnLibraryChanged += LockChanged; |
|
} |
|
|
|
private void LockChanged(object sender, EventArgs e) => |
|
InvokeAsync(StateHasChanged); |
|
|
|
private async Task LoadLibraryPaths() |
|
{ |
|
_model.HasChanges = false; |
|
_model.Paths = await Mediator.Send(new GetLocalLibraryPaths(Id), _cts.Token) |
|
.Map(list => list.Map(vm => new LocalLibraryPathEditViewModel |
|
{ |
|
Id = vm.Id, |
|
Path = vm.Path |
|
}).ToList()); |
|
} |
|
|
|
private async Task MoveLibraryPath(LocalLibraryPathEditViewModel libraryPath) |
|
{ |
|
var parameters = new DialogParameters { { "MediaKind", _model.MediaKind }, { "SourceLibraryId", Id } }; |
|
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; |
|
|
|
IDialogReference dialog = await Dialog.ShowAsync<MoveLocalLibraryPathDialog>("Move Local Library Path", parameters, options); |
|
DialogResult result = await dialog.Result; |
|
if (result is { Canceled: false, Data: LocalLibraryViewModel library }) |
|
{ |
|
var request = new MoveLocalLibraryPath(libraryPath.Id, library.Id); |
|
Either<BaseError, Unit> moveResult = await Mediator.Send(request, _cts.Token); |
|
moveResult.Match( |
|
_ => NavigationManager.NavigateTo($"media/sources/local/{library.Id}/edit"), |
|
error => |
|
{ |
|
Snackbar.Add(error.Value, Severity.Error); |
|
Logger.LogError("Unexpected error moving local library path: {Error}", error.Value); |
|
}); |
|
} |
|
} |
|
|
|
private async Task DeleteLibraryPath(LocalLibraryPathEditViewModel libraryPath) |
|
{ |
|
if (libraryPath.Id == 0) |
|
{ |
|
_model.HasChanges = true; |
|
_model.Paths.Remove(libraryPath); |
|
} |
|
|
|
int count = await Mediator.Send(new CountMediaItemsByLibraryPath(libraryPath.Id), _cts.Token); |
|
var parameters = new DialogParameters |
|
{ |
|
{ "EntityType", "library path" }, |
|
{ "EntityName", libraryPath.Path }, |
|
{ "DetailText", $"This library path contains {count} media items." }, |
|
{ "DetailHighlight", count.ToString() } |
|
}; |
|
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall }; |
|
|
|
IDialogReference dialog = await Dialog.ShowAsync<DeleteDialog>("Delete Library Path", parameters, options); |
|
DialogResult result = await dialog.Result; |
|
if (result is { Canceled: false }) |
|
{ |
|
_model.HasChanges = true; |
|
_model.Paths.Remove(libraryPath); |
|
} |
|
} |
|
|
|
private void AddLibraryPath() |
|
{ |
|
if (string.IsNullOrWhiteSpace(_newPath.Path)) |
|
{ |
|
return; |
|
} |
|
|
|
if (!Directory.Exists(_newPath.Path)) |
|
{ |
|
Snackbar.Add("Path must exist on filesystem", Severity.Error); |
|
return; |
|
} |
|
|
|
if (!string.IsNullOrWhiteSpace(_newPath.Path) && _model.Paths.All(p => NormalizePath(p.Path) != NormalizePath(_newPath.Path))) |
|
{ |
|
_model.HasChanges = true; |
|
_model.Paths.Add( |
|
new LocalLibraryPathEditViewModel |
|
{ |
|
Path = _newPath.Path |
|
}); |
|
} |
|
|
|
_newPath.Path = null; |
|
} |
|
|
|
private async Task SaveChangesAsync() |
|
{ |
|
if (_success) |
|
{ |
|
Either<BaseError, LocalLibraryViewModel> result = IsEdit |
|
? await Mediator.Send( |
|
new UpdateLocalLibrary( |
|
_model.Id, |
|
_model.Name, |
|
_model.Paths.Map(p => new UpdateLocalLibraryPath(p.Id, p.Path)).ToList()), |
|
_cts.Token) |
|
: await Mediator.Send( |
|
new CreateLocalLibrary( |
|
_model.Name, |
|
_model.MediaKind, |
|
_model.Paths.Map(p => p.Path).ToList()), |
|
_cts.Token); |
|
|
|
result.Match( |
|
_ => NavigationManager.NavigateTo("media/sources/local"), |
|
error => |
|
{ |
|
Snackbar.Add(error.Value, Severity.Error); |
|
Logger.LogError("Unexpected error saving local library: {Error}", error.Value); |
|
}); |
|
} |
|
} |
|
|
|
private string NormalizePath(string path) => Path.GetFullPath(new Uri(path).LocalPath) |
|
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) |
|
.ToUpperInvariant(); |
|
|
|
void IDisposable.Dispose() |
|
{ |
|
Locker.OnLibraryChanged -= LockChanged; |
|
|
|
_cts.Cancel(); |
|
_cts.Dispose(); |
|
} |
|
|
|
} |