@@ -53,23 +53,31 @@
}
-@if(_documents?.Count > 0)
+@if(_total > 0)
{
-
@string.Format(L["{0} documents found in the database."], _documents.Count)
-
- @foreach(DocumentDto document in _documents)
+ @string.Format(L["{0} documents found in the database."], _total)
+
}
else
{
diff --git a/Marechai/Pages/Documents/Search.razor.cs b/Marechai/Pages/Documents/Search.razor.cs
index 15ca071e..fcb67868 100644
--- a/Marechai/Pages/Documents/Search.razor.cs
+++ b/Marechai/Pages/Documents/Search.razor.cs
@@ -23,20 +23,57 @@
// Copyright © 2003-2026 Natalia Portillo
*******************************************************************************/
+/******************************************************************************
+// MARECHAI: Master repository of computing history artifacts information
+// ----------------------------------------------------------------------------
+//
+// Author(s) : Natalia Portillo
+//
+// --[ License ] --------------------------------------------------------------
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+//
+// ----------------------------------------------------------------------------
+// Copyright © 2003-2026 Natalia Portillo
+*******************************************************************************/
+
+using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Marechai.ApiClient.Models;
using Microsoft.AspNetCore.Components;
+using Microsoft.JSInterop;
namespace Marechai.Pages.Documents;
-public partial class Search
+public partial class Search : IAsyncDisposable
{
- char? _character;
- List _documents;
- bool _loaded;
- string _lastStartingCharacter;
- int? _lastYear;
+ const int _pageSize = 100;
+ readonly string _sentinelId = $"documents-search-sentinel-{Guid.NewGuid():N}";
+ readonly string _scrollerId = $"documents-search-scroll-{Guid.NewGuid():N}";
+ readonly List _documents = [];
+ char? _character;
+ bool _initialized;
+ bool _loadingMore;
+ bool _observerRegistered;
+ string _lastStartingCharacter;
+ int? _lastYear;
+ int _total;
+ DotNetObjectReference _selfRef;
+
+ [Inject]
+ IJSRuntime JS { get; set; }
[Parameter]
public int? Year { get; set; }
@@ -44,38 +81,127 @@ public partial class Search
[Parameter]
public string StartingCharacter { get; set; }
+ bool HasMore => _documents.Count < _total;
+
protected override void OnParametersSet()
{
if(Year == _lastYear && StartingCharacter == _lastStartingCharacter) return;
_lastYear = Year;
_lastStartingCharacter = StartingCharacter;
- _loaded = false;
+ _initialized = false;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
- if(_loaded) return;
-
- _character = null;
-
- if(!string.IsNullOrWhiteSpace(StartingCharacter) && StartingCharacter.Length == 1)
+ if(!_initialized)
{
- _character = StartingCharacter[0];
+ await UnobserveAsync();
- // ToUpper()
- if(_character >= 'a' && _character <= 'z') _character -= (char)32;
+ _character = null;
+ _total = 0;
+ _documents.Clear();
- // Check if not letter or number
- if(_character < '0' || _character > '9' && _character < 'A' || _character > 'Z') _character = null;
+ if(!string.IsNullOrWhiteSpace(StartingCharacter) && StartingCharacter.Length == 1)
+ {
+ _character = StartingCharacter[0];
+
+ // ToUpper()
+ if(_character >= 'a' && _character <= 'z') _character -= (char)32;
+
+ // Check if not letter or number
+ if(_character < '0' || _character > '9' && _character < 'A' || _character > 'Z') _character = null;
+ }
+
+ Task countTask = GetCurrentCountAsync();
+ Task> firstPageTask = FetchBatchAsync(0, _pageSize);
+
+ await Task.WhenAll(countTask, firstPageTask);
+
+ _total = countTask.Result;
+ _documents.AddRange(firstPageTask.Result);
+ _initialized = true;
+
+ StateHasChanged();
+ return;
}
- if(_character.HasValue) _documents = await Service.GetDocumentsByLetterAsync(_character.Value);
+ if(!_observerRegistered && HasMore)
+ {
+ _selfRef ??= DotNetObjectReference.Create(this);
- if(Year.HasValue && _documents is null) _documents = await Service.GetDocumentsByYearAsync(Year.Value);
+ try
+ {
+ await JS.InvokeVoidAsync("marechaiInfiniteScroll.observe", _sentinelId, _selfRef, $"#{_scrollerId}",
+ "200px");
+ _observerRegistered = true;
+ }
+ catch(JSDisconnectedException)
+ {
+ // Circuit gone — nothing to do.
+ }
+ }
+ }
- _documents ??= await Service.GetDocumentsAsync();
- _loaded = true;
+ [JSInvokable]
+ public async Task OnSentinelVisibleAsync()
+ {
+ if(_loadingMore || !HasMore) return;
+
+ _loadingMore = true;
StateHasChanged();
+
+ try
+ {
+ List next = await FetchBatchAsync(_documents.Count, _pageSize);
+ _documents.AddRange(next);
+
+ if(!HasMore) await UnobserveAsync();
+ }
+ finally
+ {
+ _loadingMore = false;
+ StateHasChanged();
+ }
+ }
+
+ Task GetCurrentCountAsync()
+ {
+ if(_character.HasValue) return Service.GetDocumentsByLetterCountAsync(_character.Value);
+
+ if(Year.HasValue) return Service.GetDocumentsByYearCountAsync(Year.Value);
+
+ return Service.GetDocumentsCountAsync();
+ }
+
+ Task> FetchBatchAsync(int skip, int take)
+ {
+ if(_character.HasValue) return Service.GetDocumentsByLetterAsync(_character.Value, skip, take);
+
+ if(Year.HasValue) return Service.GetDocumentsByYearAsync(Year.Value, skip, take);
+
+ return Service.GetDocumentsAsync(skip, take);
+ }
+
+ async Task UnobserveAsync()
+ {
+ if(!_observerRegistered) return;
+
+ try
+ {
+ await JS.InvokeVoidAsync("marechaiInfiniteScroll.unobserve", _sentinelId);
+ }
+ catch(JSDisconnectedException)
+ {
+ // Circuit gone; nothing to do.
+ }
+
+ _observerRegistered = false;
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ await UnobserveAsync();
+ _selfRef?.Dispose();
}
}
diff --git a/Marechai/Services/DocumentsService.cs b/Marechai/Services/DocumentsService.cs
index 84fe5530..41bd27aa 100644
--- a/Marechai/Services/DocumentsService.cs
+++ b/Marechai/Services/DocumentsService.cs
@@ -25,6 +25,7 @@
using System;
using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
using Marechai.ApiClient.Models;
using Microsoft.Kiota.Abstractions;
@@ -103,11 +104,16 @@ public class DocumentsService(Marechai.ApiClient.Client client)
}
}
- public async Task> GetDocumentsByLetterAsync(char c)
+ public async Task> GetDocumentsByLetterAsync(char c, int? skip = null, int? take = null,
+ CancellationToken cancellationToken = default)
{
try
{
- List documents = await client.Documents.ByLetter[c.ToString()].GetAsync();
+ List documents = await client.Documents.ByLetter[c.ToString()].GetAsync(rc =>
+ {
+ if(skip.HasValue) rc.QueryParameters.Skip = skip.Value;
+ if(take.HasValue) rc.QueryParameters.Take = take.Value;
+ }, cancellationToken);
return documents ?? [];
}
@@ -117,11 +123,30 @@ public class DocumentsService(Marechai.ApiClient.Client client)
}
}
- public async Task> GetDocumentsByYearAsync(int year)
+ public async Task GetDocumentsByLetterCountAsync(char c, CancellationToken cancellationToken = default)
{
try
{
- List documents = await client.Documents.ByYear[year].GetAsync();
+ int? count = await client.Documents.ByLetter[c.ToString()].Count.GetAsync(cancellationToken: cancellationToken);
+
+ return count ?? 0;
+ }
+ catch
+ {
+ return 0;
+ }
+ }
+
+ public async Task> GetDocumentsByYearAsync(int year, int? skip = null, int? take = null,
+ CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ List documents = await client.Documents.ByYear[year].GetAsync(rc =>
+ {
+ if(skip.HasValue) rc.QueryParameters.Skip = skip.Value;
+ if(take.HasValue) rc.QueryParameters.Take = take.Value;
+ }, cancellationToken);
return documents ?? [];
}
@@ -131,11 +156,30 @@ public class DocumentsService(Marechai.ApiClient.Client client)
}
}
- public async Task> GetDocumentsAsync()
+ public async Task GetDocumentsByYearCountAsync(int year, CancellationToken cancellationToken = default)
{
try
{
- List documents = await client.Documents.GetAsync();
+ int? count = await client.Documents.ByYear[year].Count.GetAsync(cancellationToken: cancellationToken);
+
+ return count ?? 0;
+ }
+ catch
+ {
+ return 0;
+ }
+ }
+
+ public async Task> GetDocumentsAsync(int? skip = null, int? take = null,
+ CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ List documents = await client.Documents.GetAsync(rc =>
+ {
+ if(skip.HasValue) rc.QueryParameters.Skip = skip.Value;
+ if(take.HasValue) rc.QueryParameters.Take = take.Value;
+ }, cancellationToken);
return documents ?? [];
}