@using Microsoft.AspNetCore.Components.Forms @using NexusReader.Application.Abstractions.Services @using NexusReader.Application.Queries.Reader @using NexusReader.Application.Commands.Library @using System.Net.Http.Json @inject IEpubMetadataExtractor MetadataExtractor @inject ILogger Logger @inject HttpClient Http @inject IReaderNavigationService ReaderNavigation @inject IJSRuntime JSRuntime @implements IAsyncDisposable @if (IsOpen) { } @code { /// /// Gets or sets a value indicating whether the modal is open. /// [Parameter] public bool IsOpen { get; set; } /// /// Event triggered when the IsOpen state changes. /// [Parameter] public EventCallback IsOpenChanged { get; set; } private bool _isDragging; private bool IsParsing { get; set; } private bool IsVerifying { get; set; } private bool IsIngesting { get; set; } private LocalEpubMetadata? Metadata { get; set; } private string? ErrorMessage { get; set; } private byte[]? _epubBytes; // Allow up to 50 MB private const long MaxFileSize = 50 * 1024 * 1024; private async Task CloseModal() { IsOpen = false; Reset(); await IsOpenChanged.InvokeAsync(false); } private void Reset() { IsParsing = false; IsVerifying = false; IsIngesting = false; Metadata = null; ErrorMessage = null; _isDragging = false; _epubBytes = null; } private void OnDragEnter() => _isDragging = true; private void OnDragLeave() => _isDragging = false; private async Task HandleFileSelected(InputFileChangeEventArgs e) { _isDragging = false; var file = e.File; if (file == null) return; if (!file.Name.EndsWith(".epub", StringComparison.OrdinalIgnoreCase)) { ErrorMessage = "Only .epub files are supported."; return; } ErrorMessage = null; IsParsing = true; StateHasChanged(); try { using var stream = file.OpenReadStream(MaxFileSize); using var memoryStream = new MemoryStream(); await stream.CopyToAsync(memoryStream); _epubBytes = memoryStream.ToArray(); memoryStream.Position = 0; var result = await MetadataExtractor.ExtractMetadataAsync(memoryStream); if (result.IsSuccess) { Metadata = result.Value; IsVerifying = true; } else { ErrorMessage = result.Errors.FirstOrDefault()?.Message ?? "Failed to parse EPUB."; } } catch (Exception ex) { Logger.LogError(ex, "Error uploading EPUB"); ErrorMessage = $"An unexpected error occurred: {ex.Message}"; } finally { IsParsing = false; StateHasChanged(); } } private async Task SaveToLibrary() { if (Metadata == null || _epubBytes == null) return; IsIngesting = true; ErrorMessage = null; StateHasChanged(); try { var request = new IngestEbookRequest( Metadata.Title, Metadata.Author, Metadata.CoverImage != null ? Convert.ToBase64String(Metadata.CoverImage) : null, Convert.ToBase64String(_epubBytes) ); var response = await Http.PostAsJsonAsync("api/library/ingest", request); if (response.IsSuccessStatusCode) { var result = await response.Content.ReadFromJsonAsync(); if (result != null) { await CloseModal(); ReaderNavigation.NavigateToBook(result.Id); } } else { ErrorMessage = await response.Content.ReadAsStringAsync(); } } catch (Exception ex) { Logger.LogError(ex, "Error during ingestion"); ErrorMessage = "Failed to save book to library. Please try again."; } finally { IsIngesting = false; StateHasChanged(); } } private record IngestResult(Guid Id); public ValueTask DisposeAsync() { // Clear the large byte array so it is eligible for GC even if the component is cached. _epubBytes = null; return ValueTask.CompletedTask; } }