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