Polish ayah UX with fixed player dock and progressive skeletons.
build-and-deploy / deploy (push) Successful in 4m45s
build-and-deploy / deploy (push) Successful in 4m45s
Compact header, sidebar surah list, closed notes, resume CTA, and larger ayah typography.
This commit is contained in:
@@ -4,111 +4,128 @@
|
||||
@inject TafsirApiClient Api
|
||||
@inject IDeviceIdStore DeviceStore
|
||||
@inject NavigationManager Nav
|
||||
@inject PlayerSessionService Player
|
||||
|
||||
<section class="tafsir-page">
|
||||
<header class="page-header">
|
||||
<h1>@title</h1>
|
||||
<p class="muted">@author</p>
|
||||
@if (view == View.Ayahs || view == View.Detail)
|
||||
<header class="page-header page-header-row">
|
||||
@if (loadingMeta)
|
||||
{
|
||||
<button type="button" class="linkish" @onclick="BackToSurahs">← فهرست سورهها</button>
|
||||
<SkeletonBlock Variant="lines" Lines="1" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<h1 class="page-title-inline">
|
||||
<span>@title</span>
|
||||
@if (!string.IsNullOrWhiteSpace(author))
|
||||
{
|
||||
<span class="title-author">(@author)</span>
|
||||
}
|
||||
</h1>
|
||||
<button type="button" class="back-btn" @onclick="GoBack" aria-label="بازگشت">←</button>
|
||||
}
|
||||
</header>
|
||||
|
||||
@if (loading)
|
||||
@if (view == View.Surahs)
|
||||
{
|
||||
<p class="muted">در حال بارگذاری...</p>
|
||||
}
|
||||
else if (view == View.Surahs)
|
||||
{
|
||||
<div class="surah-grid">
|
||||
@foreach (var s in surahs)
|
||||
{
|
||||
<div class="surah-card">
|
||||
<button type="button" class="surah-main" @onclick="() => OpenSurah(s.Number)">
|
||||
<span class="surah-num">@s.Number</span>
|
||||
<span class="surah-names">
|
||||
<strong>@s.NameFa</strong>
|
||||
<span class="muted">@s.NameAr</span>
|
||||
</span>
|
||||
<span class="muted">@s.AyahCount آیه</span>
|
||||
</button>
|
||||
<button type="button" class="chip @(s.IsBookmarked ? "on" : null)"
|
||||
@onclick="() => ToggleSurahBookmark(s)">نشان</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (loadingSurahs)
|
||||
{
|
||||
<SkeletonBlock Variant="cards" Lines="8" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="surah-grid">
|
||||
@foreach (var s in surahs)
|
||||
{
|
||||
<div class="surah-card">
|
||||
<button type="button" class="surah-main" @onclick="() => OpenSurah(s.Number)">
|
||||
<span class="surah-num">@s.Number</span>
|
||||
<span class="surah-names">
|
||||
<strong>@s.NameFa</strong>
|
||||
<span class="muted">@s.NameAr</span>
|
||||
</span>
|
||||
<span class="muted">@s.AyahCount آیه</span>
|
||||
</button>
|
||||
<button type="button" class="chip @(s.IsBookmarked ? "on" : null)"
|
||||
@onclick="() => ToggleSurahBookmark(s)">نشان</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else if (view == View.Ayahs)
|
||||
{
|
||||
<h2 class="section-title">سوره @selectedSurah?.NameFa</h2>
|
||||
<div class="ayah-list">
|
||||
@foreach (var a in ayahs)
|
||||
{
|
||||
<button type="button" class="ayah-row" @onclick="() => OpenAyah(a.AyahNumber)">
|
||||
<span class="ayah-num">@a.AyahNumber</span>
|
||||
<span class="ayah-preview">@(a.TextFaPreview ?? "آیه " + a.AyahNumber)</span>
|
||||
<span class="ayah-flags">
|
||||
@if (a.IsFavorite) { <span class="dot fav" title="علاقه"></span> }
|
||||
@if (a.IsBookmarked) { <span class="dot mark" title="نشان"></span> }
|
||||
@if (a.HasAudio) { <span class="dot audio" title="صوت"></span> }
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<h2 class="section-title">سوره @(selectedSurah?.NameFa ?? "…")</h2>
|
||||
@if (loadingAyahs)
|
||||
{
|
||||
<SkeletonBlock Variant="cards" Lines="10" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="ayah-list">
|
||||
@foreach (var a in ayahs)
|
||||
{
|
||||
<button type="button" class="ayah-row" @onclick="() => OpenAyah(a.AyahNumber)">
|
||||
<span class="ayah-num">@a.AyahNumber</span>
|
||||
<span class="ayah-preview">@(a.TextFaPreview ?? "آیه " + a.AyahNumber)</span>
|
||||
<span class="ayah-flags">
|
||||
@if (a.IsFavorite) { <span class="dot fav" title="علاقه"></span> }
|
||||
@if (a.IsBookmarked) { <span class="dot mark" title="نشان"></span> }
|
||||
@if (a.HasAudio) { <span class="dot audio" title="صوت"></span> }
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else if (detail is not null)
|
||||
else if (view == View.Detail)
|
||||
{
|
||||
<div class="ayah-detail">
|
||||
<div class="ayah-nav">
|
||||
<button type="button" class="chip" disabled="@(detail.PrevAyah is null)"
|
||||
@onclick="() => OpenAyah(detail.PrevAyah!.Value)">آیه قبل</button>
|
||||
<button type="button" class="chip" @onclick="() => OpenSurah(detail.SurahNumber)">فهرست آیات</button>
|
||||
<button type="button" class="chip" disabled="@(detail.NextAyah is null)"
|
||||
@onclick="() => OpenAyah(detail.NextAyah!.Value)">آیه بعد</button>
|
||||
<button type="button" class="chip" disabled="@(detail?.PrevAyah is null)"
|
||||
@onclick="() => GoPrevAyah()">آیه قبل</button>
|
||||
<button type="button" class="chip" @onclick="() => OpenSurah(SurahNumber ?? 1)">فهرست آیات</button>
|
||||
<button type="button" class="chip" disabled="@(detail?.NextAyah is null)"
|
||||
@onclick="() => GoNextAyah()">آیه بعد</button>
|
||||
</div>
|
||||
|
||||
<div class="ayah-block">
|
||||
<p class="ayah-ar">@detail.TextAr</p>
|
||||
@if (!string.IsNullOrWhiteSpace(detail.TextFa))
|
||||
@if (loadingDetail || detail is null)
|
||||
{
|
||||
<SkeletonBlock Variant="lines" Lines="5" />
|
||||
<SkeletonBlock Variant="cards" Lines="2" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="ayah-block">
|
||||
<p class="ayah-ar">@detail.TextAr</p>
|
||||
@if (!string.IsNullOrWhiteSpace(detail.TextFa))
|
||||
{
|
||||
<p class="ayah-fa">@detail.TextFa</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(detail.NotesHtml))
|
||||
{
|
||||
<p class="ayah-fa">@detail.TextFa</p>
|
||||
<details class="content-block">
|
||||
<summary>نکتهها</summary>
|
||||
<div class="html-body">@((MarkupString)detail.NotesHtml)</div>
|
||||
</details>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(detail.MessagesHtml))
|
||||
{
|
||||
<details class="content-block">
|
||||
<summary>پیامها</summary>
|
||||
<div class="html-body">@((MarkupString)detail.MessagesHtml)</div>
|
||||
</details>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(detail.VideoEmbedUrl))
|
||||
{
|
||||
<details class="content-block">
|
||||
<summary>فیلم</summary>
|
||||
<div class="video-wrap">
|
||||
<iframe src="@detail.VideoEmbedUrl" allowfullscreen loading="lazy"></iframe>
|
||||
</div>
|
||||
</details>
|
||||
}
|
||||
</div>
|
||||
|
||||
<AudioPlayer @key="@($"{detail.SurahNumber}:{detail.AyahNumber}")"
|
||||
Title="@($"{detail.SurahNameFa} — آیه {detail.AyahNumber}")"
|
||||
AudioUrl="@detail.AudioUrl"
|
||||
AudioKey="@($"{detail.SurahNumber}:{detail.AyahNumber}")"
|
||||
InitialPosition="initialPosition"
|
||||
IsFavorite="isFavorite"
|
||||
IsBookmarked="isBookmarked"
|
||||
OnFavoriteToggle="ToggleFavorite"
|
||||
OnBookmarkToggle="ToggleBookmark"
|
||||
OnProgress="SaveProgress" />
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(detail.NotesHtml))
|
||||
{
|
||||
<details class="content-block" open>
|
||||
<summary>نکتهها</summary>
|
||||
<div class="html-body">@((MarkupString)detail.NotesHtml)</div>
|
||||
</details>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(detail.MessagesHtml))
|
||||
{
|
||||
<details class="content-block" open>
|
||||
<summary>پیامها</summary>
|
||||
<div class="html-body">@((MarkupString)detail.MessagesHtml)</div>
|
||||
</details>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(detail.VideoEmbedUrl))
|
||||
{
|
||||
<details class="content-block">
|
||||
<summary>فیلم</summary>
|
||||
<div class="video-wrap">
|
||||
<iframe src="@detail.VideoEmbedUrl" allowfullscreen loading="lazy"></iframe>
|
||||
</div>
|
||||
</details>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -124,16 +141,16 @@
|
||||
private string title = "تفسیر";
|
||||
private string author = "";
|
||||
private Guid tafsirId;
|
||||
private bool loading = true;
|
||||
private bool loadingMeta = true;
|
||||
private bool loadingSurahs;
|
||||
private bool loadingAyahs;
|
||||
private bool loadingDetail;
|
||||
private string? routeKey;
|
||||
private View view = View.Surahs;
|
||||
private List<SurahListItemDto> surahs = [];
|
||||
private List<AyahListItemDto> ayahs = [];
|
||||
private SurahListItemDto? selectedSurah;
|
||||
private AyahDetailDto? detail;
|
||||
private double initialPosition;
|
||||
private bool isFavorite;
|
||||
private bool isBookmarked;
|
||||
private string deviceId = "";
|
||||
private bool ready;
|
||||
|
||||
@@ -157,7 +174,15 @@
|
||||
if (routeKey == key) return;
|
||||
routeKey = key;
|
||||
|
||||
loading = true;
|
||||
if (AyahNumber is not null && SurahNumber is not null) view = View.Detail;
|
||||
else if (SurahNumber is not null) view = View.Ayahs;
|
||||
else view = View.Surahs;
|
||||
|
||||
loadingMeta = true;
|
||||
loadingSurahs = view == View.Surahs;
|
||||
loadingAyahs = view == View.Ayahs;
|
||||
loadingDetail = view == View.Detail;
|
||||
detail = null;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
try
|
||||
@@ -166,37 +191,39 @@
|
||||
deviceId = await DeviceStore.GetOrCreateAsync();
|
||||
|
||||
var tafsirs = await Api.GetTafsirsAsync();
|
||||
var tafsir = tafsirs.FirstOrDefault(t => t.Slug == slug);
|
||||
var tafsir = tafsirs.FirstOrDefault(t => t.Slug == Slug);
|
||||
title = tafsir?.Title ?? "تفسیر";
|
||||
author = tafsir?.Author ?? "";
|
||||
tafsirId = tafsir?.Id ?? Guid.Empty;
|
||||
loadingMeta = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
if (AyahNumber is int a && SurahNumber is int s)
|
||||
{
|
||||
view = View.Detail;
|
||||
if (view == View.Detail && SurahNumber is int s && AyahNumber is int a)
|
||||
await LoadDetail(s, a);
|
||||
}
|
||||
else if (SurahNumber is int sn)
|
||||
{
|
||||
view = View.Ayahs;
|
||||
else if (view == View.Ayahs && SurahNumber is int sn)
|
||||
await LoadAyahs(sn);
|
||||
}
|
||||
else
|
||||
{
|
||||
view = View.Surahs;
|
||||
surahs = (await Api.GetSurahsAsync(deviceId, tafsirId == Guid.Empty ? null : tafsirId)).ToList();
|
||||
}
|
||||
await LoadSurahs();
|
||||
}
|
||||
finally
|
||||
{
|
||||
loading = false;
|
||||
loadingMeta = false;
|
||||
loadingSurahs = false;
|
||||
loadingAyahs = false;
|
||||
loadingDetail = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
private string slug => Slug;
|
||||
|
||||
private void BackToSurahs() => Nav.NavigateTo($"/tafsir/{Slug}");
|
||||
private void GoBack()
|
||||
{
|
||||
if (view == View.Detail && SurahNumber is int s)
|
||||
Nav.NavigateTo($"/tafsir/{Slug}/surah/{s}");
|
||||
else if (view == View.Ayahs)
|
||||
Nav.NavigateTo($"/tafsir/{Slug}");
|
||||
else
|
||||
Nav.NavigateTo("/");
|
||||
}
|
||||
|
||||
private void OpenSurah(int n) => Nav.NavigateTo($"/tafsir/{Slug}/surah/{n}");
|
||||
|
||||
@@ -206,30 +233,52 @@
|
||||
Nav.NavigateTo($"/tafsir/{Slug}/surah/{s}/ayah/{n}");
|
||||
}
|
||||
|
||||
private void GoPrevAyah()
|
||||
{
|
||||
if (detail?.PrevAyah is int p) OpenAyah(p);
|
||||
}
|
||||
|
||||
private void GoNextAyah()
|
||||
{
|
||||
if (detail?.NextAyah is int n) OpenAyah(n);
|
||||
}
|
||||
|
||||
private async Task LoadSurahs()
|
||||
{
|
||||
loadingSurahs = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
surahs = (await Api.GetSurahsAsync(deviceId, tafsirId == Guid.Empty ? null : tafsirId)).ToList();
|
||||
loadingSurahs = false;
|
||||
}
|
||||
|
||||
private async Task LoadAyahs(int surahNumber)
|
||||
{
|
||||
surahs = surahs.Count > 0 ? surahs : (await Api.GetSurahsAsync(deviceId, tafsirId == Guid.Empty ? null : tafsirId)).ToList();
|
||||
loadingAyahs = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
if (surahs.Count == 0)
|
||||
surahs = (await Api.GetSurahsAsync(deviceId, tafsirId == Guid.Empty ? null : tafsirId)).ToList();
|
||||
selectedSurah = surahs.FirstOrDefault(x => x.Number == surahNumber);
|
||||
ayahs = (await Api.GetAyahsAsync(Slug, surahNumber, deviceId)).ToList();
|
||||
loadingAyahs = false;
|
||||
}
|
||||
|
||||
private async Task LoadDetail(int surahNumber, int ayahNumber)
|
||||
{
|
||||
loadingDetail = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
detail = await Api.GetAyahAsync(Slug, surahNumber, ayahNumber);
|
||||
loadingDetail = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
if (detail is null) return;
|
||||
|
||||
var progress = await Api.GetProgressAsync(deviceId);
|
||||
initialPosition = progress is not null
|
||||
&& progress.SurahNumber == surahNumber
|
||||
&& progress.AyahNumber == ayahNumber
|
||||
? progress.PositionSeconds
|
||||
: 0;
|
||||
|
||||
var favorites = await Api.GetFavoritesAsync(deviceId);
|
||||
var bookmarks = await Api.GetAyahBookmarksAsync(deviceId);
|
||||
isFavorite = favorites.Any(f => f.TafsirId == detail.TafsirId && f.SurahNumber == surahNumber && f.AyahNumber == ayahNumber);
|
||||
isBookmarked = bookmarks.Any(b => b.TafsirId == detail.TafsirId && b.SurahNumber == surahNumber && b.AyahNumber == ayahNumber);
|
||||
tafsirId = detail.TafsirId;
|
||||
|
||||
var resumePos = Player.HasTrack
|
||||
&& Player.SurahNumber == surahNumber
|
||||
&& Player.AyahNumber == ayahNumber
|
||||
? Player.PositionSeconds
|
||||
: 0;
|
||||
await Player.SetTrackAsync(detail, resumePos);
|
||||
}
|
||||
|
||||
private async Task ToggleSurahBookmark(SurahListItemDto s)
|
||||
@@ -241,31 +290,4 @@
|
||||
await Api.AddSurahBookmarkAsync(new ToggleSurahBookmarkRequest(tafsirId, s.Number, deviceId));
|
||||
surahs = (await Api.GetSurahsAsync(deviceId, tafsirId)).ToList();
|
||||
}
|
||||
|
||||
private async Task ToggleFavorite()
|
||||
{
|
||||
if (detail is null) return;
|
||||
if (isFavorite)
|
||||
await Api.RemoveFavoriteAsync(detail.TafsirId, detail.SurahNumber, detail.AyahNumber, deviceId);
|
||||
else
|
||||
await Api.AddFavoriteAsync(new ToggleAyahRequest(detail.TafsirId, detail.SurahNumber, detail.AyahNumber, deviceId));
|
||||
isFavorite = !isFavorite;
|
||||
}
|
||||
|
||||
private async Task ToggleBookmark()
|
||||
{
|
||||
if (detail is null) return;
|
||||
if (isBookmarked)
|
||||
await Api.RemoveAyahBookmarkAsync(detail.TafsirId, detail.SurahNumber, detail.AyahNumber, deviceId);
|
||||
else
|
||||
await Api.AddAyahBookmarkAsync(new ToggleAyahRequest(detail.TafsirId, detail.SurahNumber, detail.AyahNumber, deviceId));
|
||||
isBookmarked = !isBookmarked;
|
||||
}
|
||||
|
||||
private async Task SaveProgress(double position)
|
||||
{
|
||||
if (detail is null) return;
|
||||
await Api.SaveProgressAsync(new UpsertProgressRequest(
|
||||
detail.TafsirId, detail.SurahNumber, detail.AyahNumber, position, deviceId));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user