@using Microsoft.AspNetCore.Components.Forms @using NexusReader.Application.Abstractions.Services @using NexusReader.Application.Queries.Reader @using NexusReader.Application.Commands.Library @using NexusReader.UI.Shared.Services @using System.Net.Http.Json @inject IEpubMetadataExtractor MetadataExtractor @inject ILogger Logger @inject HttpClient Http @inject IReaderNavigationService ReaderNavigation @inject IJSRuntime JSRuntime @inject ISyncService SyncService @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 bool IsIndexing { get; set; } private string IngestionStatusMessage { get; set; } = "Initializing..."; private double IngestionProgressPercent { get; set; } private Guid IngestedBookId { get; set; } = Guid.Empty; 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; protected override async Task OnInitializedAsync() { await SyncService.InitializeAsync(); SyncService.OnIngestionProgressReceived += HandleIngestionProgress; } private async Task HandleIngestionProgress(string message, double progress) { if (!IsIndexing) return; IngestionStatusMessage = message; IngestionProgressPercent = progress; await InvokeAsync(StateHasChanged); if (progress >= 1.0) { // Give the user a moment to see the completion message await Task.Delay(2500); // Now close the modal and navigate to the book if (IngestedBookId != Guid.Empty) { var bookId = IngestedBookId; await InvokeAsync(async () => { await CloseModal(); ReaderNavigation.NavigateToBook(bookId); }); } } } private async Task CloseModal() { if (IsIngesting || IsIndexing) return; IsOpen = false; Reset(); await IsOpenChanged.InvokeAsync(false); } private void Reset() { IsParsing = false; IsVerifying = false; IsIngesting = false; IsIndexing = false; IngestionStatusMessage = "Initializing..."; IngestionProgressPercent = 0.0; IngestedBookId = Guid.Empty; 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), Metadata.Description ); var response = await Http.PostAsJsonAsync("api/library/ingest", request); if (response.IsSuccessStatusCode) { var result = await response.Content.ReadFromJsonAsync(); if (result != null) { IngestedBookId = result.Id; IsVerifying = false; IsIngesting = false; IsIndexing = true; IngestionStatusMessage = "Book saved! Starting background indexing..."; IngestionProgressPercent = 0.0; StateHasChanged(); } } else { ErrorMessage = await response.Content.ReadAsStringAsync(); IsIngesting = false; } } catch (Exception ex) { Logger.LogError(ex, "Error during ingestion"); ErrorMessage = "Failed to save book to library. Please try again."; IsIngesting = false; } finally { StateHasChanged(); } } private record IngestResult(Guid Id); public async ValueTask DisposeAsync() { SyncService.OnIngestionProgressReceived -= HandleIngestionProgress; // Clear the large byte array so it is eligible for GC even if the component is cached. _epubBytes = null; await ValueTask.CompletedTask; } }