Fix TOC open crash from infinite ayah reload loop.
build-and-deploy / deploy (push) Successful in 4m4s

Load the surah list once on open, stop progress ticks from re-rendering the dock, and keep TOC row markup stable for Blazor diffs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
masoodafar-web
2026-10-10 03:17:26 +03:30
parent a63d482e15
commit 0cddd0f59a
4 changed files with 71 additions and 40 deletions
@@ -56,10 +56,9 @@
<div class="sheet-track-meta" dir="rtl">
<div class="sheet-track-name">@SurahLabel — آیه @AyahNumber</div>
@if (SurahAyahCount > 0)
{
<div class="sheet-track-index">@AyahNumber / @SurahAyahCount</div>
}
<div class="sheet-track-index">
@(SurahAyahCount > 0 ? $"{AyahNumber} / {SurahAyahCount}" : "\u00A0")
</div>
</div>
<div class="sheet-actions" dir="rtl" @onclick:stopPropagation="true">
+62 -27
View File
@@ -3,9 +3,9 @@
@if (Session.IsTocOpen && Session.HasTrack)
{
<div class="toc-sheet" dir="rtl">
<div class="toc-sheet" dir="rtl" @key="tocKey">
<div class="toc-header">
<button type="button" class="sheet-icon-btn" @onclick="Session.CloseToc" aria-label="بازگشت">›</button>
<button type="button" class="sheet-icon-btn" @onclick="Close" aria-label="بازگشت">›</button>
<div class="toc-header-text">
<strong>سوره @Session.SurahNameFa</strong>
<span class="muted">@Session.TafsirTitle</span>
@@ -30,12 +30,14 @@
@foreach (var a in ayahs)
{
var active = a.AyahNumber == Session.AyahNumber;
<li class="toc-row @(active ? "active" : null)">
<li @key="a.AyahNumber" class="toc-row @(active ? "active" : null)">
<button type="button" class="toc-row-btn" @onclick="() => Select(a.AyahNumber)">
@if (active)
{
<span class="toc-play-dot" aria-hidden="true">▶</span>
}
<span class="toc-play-slot" aria-hidden="true">
@if (active)
{
<span class="toc-play-dot">▶</span>
}
</span>
<span class="toc-row-main">
<span class="toc-row-title">آیه @a.AyahNumber</span>
@if (!string.IsNullOrWhiteSpace(a.TextFaPreview))
@@ -43,9 +45,18 @@
<span class="toc-row-preview">@a.TextFaPreview</span>
}
<span class="toc-row-flags">
@if (a.IsFavorite) { <span title="علاقه">♥</span> }
@if (a.IsBookmarked) { <span title="نشان">⚑</span> }
@if (!a.HasAudio) { <span class="muted">بدون صوت</span> }
@if (a.IsFavorite)
{
<span title="علاقه">♥</span>
}
@if (a.IsBookmarked)
{
<span title="نشان">⚑</span>
}
@if (!a.HasAudio)
{
<span class="muted">بدون صوت</span>
}
</span>
</span>
<span class="toc-row-num">@a.AyahNumber</span>
@@ -60,30 +71,42 @@
@code {
private List<AyahListItemDto> ayahs = new();
private bool loading;
private bool loadInProgress;
private bool wasOpen;
private int loadedForSurah = -1;
private string loadedSlug = "";
protected override void OnInitialized() => Session.Changed += OnChanged;
private string tocKey => $"{Session.TafsirSlug}:{Session.SurahNumber}";
protected override async Task OnParametersSetAsync()
{
await EnsureLoadedAsync();
}
protected override void OnInitialized() => Session.Changed += OnSessionChanged;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (Session.IsTocOpen)
await EnsureLoadedAsync();
// Load only on open transition — never on every render
if (Session.IsTocOpen && Session.HasTrack && !wasOpen)
{
wasOpen = true;
await LoadAsync(force: true);
}
else if (!Session.IsTocOpen)
{
wasOpen = false;
}
}
private async Task EnsureLoadedAsync()
private async Task LoadAsync(bool force)
{
if (!Session.IsTocOpen || !Session.HasTrack) return;
if (loadedForSurah == Session.SurahNumber && loadedSlug == Session.TafsirSlug && ayahs.Count > 0)
if (loadInProgress) return;
if (!force
&& loadedForSurah == Session.SurahNumber
&& loadedSlug == Session.TafsirSlug)
return;
loadInProgress = true;
loading = true;
await InvokeAsync(StateHasChanged);
try
{
ayahs = (await Session.GetCurrentSurahAyahsAsync()).ToList();
@@ -93,26 +116,38 @@
catch
{
ayahs = new();
loadedForSurah = Session.SurahNumber;
loadedSlug = Session.TafsirSlug;
}
finally
{
loading = false;
loadInProgress = false;
await InvokeAsync(StateHasChanged);
}
}
private async Task Select(int ayahNumber) => await Session.PlayAyahAsync(ayahNumber);
private async void OnChanged()
private void Close() => Session.CloseToc();
private void OnSessionChanged()
{
if (Session.IsTocOpen &&
(loadedForSurah != Session.SurahNumber || loadedSlug != Session.TafsirSlug))
_ = InvokeAsync(async () =>
{
loadedForSurah = -1;
await EnsureLoadedAsync();
}
await InvokeAsync(StateHasChanged);
// Only reload when TOC is open and surah/slug actually changed
if (Session.IsTocOpen && Session.HasTrack
&& (loadedForSurah != Session.SurahNumber || loadedSlug != Session.TafsirSlug)
&& !loadInProgress)
{
await LoadAsync(force: true);
}
else
{
StateHasChanged();
}
});
}
public void Dispose() => Session.Changed -= OnChanged;
public void Dispose() => Session.Changed -= OnSessionChanged;
}
@@ -202,22 +202,16 @@ public sealed class PlayerSessionService
played += (DateTimeOffset.UtcNow - start).TotalSeconds;
if (played < PersistAfterPlayedSeconds)
{
Notify();
return;
}
var now = DateTimeOffset.UtcNow;
if (_persistedForTrack && (now - _lastPersistAt).TotalSeconds < 4)
{
Notify();
return;
}
_persistedForTrack = true;
_lastPersistAt = now;
await PersistAsync(position);
Notify();
// No Notify — position ticks must not re-render dock/TOC
}
public async Task ToggleFavoriteAsync()
+5 -2
View File
@@ -518,19 +518,22 @@ button, input { font: inherit; }
.toc-row-btn {
width: 100%; border: 0; background: transparent; cursor: pointer;
display: grid;
grid-template-columns: auto 1fr auto;
grid-template-columns: 36px 1fr auto;
align-items: center;
gap: 0.75rem;
padding: 0.95rem 1rem;
text-align: right;
color: inherit;
}
.toc-play-slot {
width: 36px; height: 36px;
display: grid; place-items: center;
}
.toc-play-dot {
width: 36px; height: 36px; border-radius: 50%;
background: var(--brand); color: #fff;
display: grid; place-items: center; font-size: 0.85rem;
}
.toc-row:not(.active) .toc-row-btn { grid-template-columns: 1fr auto; }
.toc-row-main { display: grid; gap: 0.2rem; min-width: 0; }
.toc-row-title { font-weight: 700; color: var(--ink); }
.toc-row-preview {