Add Surah/Ayah browse with Gharaati Tafsir Noor crawl.
build-and-deploy / deploy (push) Failing after 11m0s
build-and-deploy / deploy (push) Failing after 11m0s
Replace episode bookmarks with surah/ayah bookmarks and ayah favorites; import ayah text, notes, messages, and official per-ayah audio from gharaati.ir.
This commit is contained in:
@@ -3,29 +3,46 @@
|
||||
@implements IAsyncDisposable
|
||||
|
||||
<div class="audio-player" @ref="root">
|
||||
<div class="player-title">@Episode.Title</div>
|
||||
<div class="player-title">@Title</div>
|
||||
<div class="player-actions">
|
||||
<button type="button" class="chip @(IsFavorite ? "on" : null)" @onclick="OnFavoriteToggle">علاقه</button>
|
||||
<button type="button" class="chip @(IsBookmarked ? "on" : null)" @onclick="OnBookmarkToggle">نشان</button>
|
||||
@if (ShowFavorite)
|
||||
{
|
||||
<button type="button" class="chip @(IsFavorite ? "on" : null)" @onclick="OnFavoriteToggle">علاقه</button>
|
||||
}
|
||||
@if (ShowBookmark)
|
||||
{
|
||||
<button type="button" class="chip @(IsBookmarked ? "on" : null)" @onclick="OnBookmarkToggle">نشان</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<audio @ref="audioEl" preload="metadata" src="@AudioSrc"></audio>
|
||||
@if (!string.IsNullOrWhiteSpace(AudioUrl))
|
||||
{
|
||||
<audio @ref="audioEl" preload="metadata" src="@AudioSrc"></audio>
|
||||
|
||||
<div class="player-controls">
|
||||
<button type="button" class="play-btn" @onclick="TogglePlay">
|
||||
@(playing ? "❚❚" : "▶")
|
||||
</button>
|
||||
<input class="seek" type="range" min="0" max="@duration.ToString("0.###")" step="0.1"
|
||||
value="@current.ToString("0.###")" @oninput="OnSeek" />
|
||||
<div class="time">@Format(current) / @Format(duration)</div>
|
||||
</div>
|
||||
<div class="player-controls">
|
||||
<button type="button" class="play-btn" @onclick="TogglePlay">
|
||||
@(playing ? "❚❚" : "▶")
|
||||
</button>
|
||||
<input class="seek" type="range" min="0" max="@duration.ToString("0.###")" step="0.1"
|
||||
value="@current.ToString("0.###")" @oninput="OnSeek" />
|
||||
<div class="time">@Format(current) / @Format(duration)</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="muted">صوت این آیه در دسترس نیست</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public EpisodeDetailDto Episode { get; set; } = default!;
|
||||
[Parameter, EditorRequired] public string Title { get; set; } = "";
|
||||
[Parameter] public string? AudioUrl { get; set; }
|
||||
[Parameter] public string AudioKey { get; set; } = "";
|
||||
[Parameter] public double InitialPosition { get; set; }
|
||||
[Parameter] public bool IsFavorite { get; set; }
|
||||
[Parameter] public bool IsBookmarked { get; set; }
|
||||
[Parameter] public bool ShowFavorite { get; set; } = true;
|
||||
[Parameter] public bool ShowBookmark { get; set; } = true;
|
||||
[Parameter] public EventCallback OnFavoriteToggle { get; set; }
|
||||
[Parameter] public EventCallback OnBookmarkToggle { get; set; }
|
||||
[Parameter] public EventCallback<double> OnProgress { get; set; }
|
||||
@@ -36,21 +53,24 @@
|
||||
private bool playing;
|
||||
private double current;
|
||||
private double duration;
|
||||
private Guid boundEpisodeId;
|
||||
private string boundKey = "";
|
||||
|
||||
private string AudioSrc => ResolveAudioUrl(Episode.AudioUrl);
|
||||
private string AudioSrc => ResolveAudioUrl(AudioUrl ?? "");
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(AudioUrl)) return;
|
||||
|
||||
if (firstRender)
|
||||
{
|
||||
self = DotNetObjectReference.Create(this);
|
||||
await Js.InvokeVoidAsync("tqAudio.bind", audioEl, self);
|
||||
}
|
||||
|
||||
if (boundEpisodeId != Episode.Id)
|
||||
var key = string.IsNullOrWhiteSpace(AudioKey) ? AudioSrc : AudioKey;
|
||||
if (boundKey != key)
|
||||
{
|
||||
boundEpisodeId = Episode.Id;
|
||||
boundKey = key;
|
||||
playing = false;
|
||||
current = InitialPosition;
|
||||
await Js.InvokeVoidAsync("tqAudio.load", audioEl, AudioSrc, InitialPosition);
|
||||
@@ -59,6 +79,7 @@
|
||||
|
||||
private string ResolveAudioUrl(string url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url)) return "";
|
||||
if (url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
return url;
|
||||
return new Uri(new Uri(ApiOptions.BaseAddress), url.TrimStart('/')).ToString();
|
||||
|
||||
@@ -3,39 +3,68 @@
|
||||
@inject IDeviceIdStore DeviceStore
|
||||
|
||||
<section class="list-page">
|
||||
<h1>نشانشدهها</h1>
|
||||
<header class="page-header">
|
||||
<h1>نشانها</h1>
|
||||
</header>
|
||||
|
||||
@if (loading)
|
||||
{
|
||||
<p class="muted">در حال بارگذاری...</p>
|
||||
}
|
||||
else if (items.Count == 0)
|
||||
{
|
||||
<p class="muted">هنوز نشانی ذخیره نشده است.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ul class="simple-list">
|
||||
@foreach (var item in items)
|
||||
{
|
||||
<li>
|
||||
<a href="/tafsir/ghatreh-chekani">@item.Episode.Title</a>
|
||||
<span class="ep-meta">@item.Episode.SurahNameFa — آیه @item.Episode.AyahNumber</span>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<h2 class="section-title">سورهها</h2>
|
||||
@if (surahs.Count == 0)
|
||||
{
|
||||
<p class="muted">سوره نشانشدهای نیست</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="simple-list">
|
||||
@foreach (var b in surahs)
|
||||
{
|
||||
<a class="list-row" href="@($"/tafsir/ghatreh-chekani/surah/{b.SurahNumber}")">
|
||||
<strong>@b.SurahNameFa</strong>
|
||||
<span class="muted">سوره @b.SurahNumber</span>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<h2 class="section-title">آیهها</h2>
|
||||
@if (ayahs.Count == 0)
|
||||
{
|
||||
<p class="muted">آیه نشانشدهای نیست</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="simple-list">
|
||||
@foreach (var b in ayahs)
|
||||
{
|
||||
<a class="list-row" href="@($"/tafsir/{b.TafsirSlug}/surah/{b.SurahNumber}/ayah/{b.AyahNumber}")">
|
||||
<strong>@b.SurahNameFa — آیه @b.AyahNumber</strong>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</section>
|
||||
|
||||
@code {
|
||||
private List<BookmarkDto> items = [];
|
||||
private bool loading = true;
|
||||
private List<SurahBookmarkDto> surahs = [];
|
||||
private List<AyahBookmarkDto> ayahs = [];
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!firstRender) return;
|
||||
var deviceId = await DeviceStore.GetOrCreateAsync();
|
||||
try { items = (await Api.GetBookmarksAsync(deviceId)).ToList(); }
|
||||
catch { items = []; }
|
||||
try
|
||||
{
|
||||
surahs = (await Api.GetSurahBookmarksAsync(deviceId)).ToList();
|
||||
ayahs = (await Api.GetAyahBookmarksAsync(deviceId)).ToList();
|
||||
}
|
||||
catch { /* ignore */ }
|
||||
finally
|
||||
{
|
||||
loading = false;
|
||||
|
||||
@@ -3,39 +3,41 @@
|
||||
@inject IDeviceIdStore DeviceStore
|
||||
|
||||
<section class="list-page">
|
||||
<h1>علاقهمندیها</h1>
|
||||
<header class="page-header">
|
||||
<h1>علاقهمندیها</h1>
|
||||
</header>
|
||||
|
||||
@if (loading)
|
||||
{
|
||||
<p class="muted">در حال بارگذاری...</p>
|
||||
}
|
||||
else if (items.Count == 0)
|
||||
{
|
||||
<p class="muted">هنوز علاقهمندیای ندارید.</p>
|
||||
<p class="muted">آیه علاقهمندی ثبت نشده</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ul class="simple-list">
|
||||
@foreach (var item in items)
|
||||
<div class="simple-list">
|
||||
@foreach (var f in items)
|
||||
{
|
||||
<li>
|
||||
<a href="/tafsir/ghatreh-chekani">@item.Episode.Title</a>
|
||||
<span class="ep-meta">@item.Episode.SurahNameFa — آیه @item.Episode.AyahNumber</span>
|
||||
</li>
|
||||
<a class="list-row" href="@($"/tafsir/{f.TafsirSlug}/surah/{f.SurahNumber}/ayah/{f.AyahNumber}")">
|
||||
<strong>@f.SurahNameFa — آیه @f.AyahNumber</strong>
|
||||
</a>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
@code {
|
||||
private List<FavoriteDto> items = [];
|
||||
private bool loading = true;
|
||||
private List<AyahFavoriteDto> items = [];
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!firstRender) return;
|
||||
var deviceId = await DeviceStore.GetOrCreateAsync();
|
||||
try { items = (await Api.GetFavoritesAsync(deviceId)).ToList(); }
|
||||
catch { items = []; }
|
||||
catch { /* ignore */ }
|
||||
finally
|
||||
{
|
||||
loading = false;
|
||||
|
||||
@@ -1,71 +1,136 @@
|
||||
@page "/tafsir/{Slug}"
|
||||
@page "/tafsir/{Slug}/surah/{SurahNumber:int}"
|
||||
@page "/tafsir/{Slug}/surah/{SurahNumber:int}/ayah/{AyahNumber:int}"
|
||||
@inject TafsirApiClient Api
|
||||
@inject IDeviceIdStore DeviceStore
|
||||
@inject NavigationManager Nav
|
||||
|
||||
<section class="tafsir-page">
|
||||
<header class="page-header">
|
||||
<h1>@title</h1>
|
||||
<p class="muted">@author</p>
|
||||
@if (view == View.Ayahs || view == View.Detail)
|
||||
{
|
||||
<button type="button" class="linkish" @onclick="BackToSurahs">← فهرست سورهها</button>
|
||||
}
|
||||
</header>
|
||||
|
||||
@if (loading)
|
||||
{
|
||||
<p class="muted">در حال بارگذاری...</p>
|
||||
}
|
||||
else
|
||||
else if (view == View.Surahs)
|
||||
{
|
||||
<div class="episode-layout">
|
||||
<div class="episode-list">
|
||||
@foreach (var ep in episodes)
|
||||
{
|
||||
<button type="button"
|
||||
class="episode-row @(selected?.Id == ep.Id ? "active" : null)"
|
||||
@onclick="() => SelectAsync(ep.Id)">
|
||||
<span class="ep-title">@ep.Title</span>
|
||||
<span class="ep-meta">@ep.SurahNameFa — آیه @ep.AyahNumber</span>
|
||||
<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>
|
||||
}
|
||||
else if (detail is not null)
|
||||
{
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="ayah-block">
|
||||
<p class="ayah-ar">@detail.TextAr</p>
|
||||
@if (!string.IsNullOrWhiteSpace(detail.TextFa))
|
||||
{
|
||||
<p class="ayah-fa">@detail.TextFa</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="player-panel">
|
||||
@if (detail is null)
|
||||
{
|
||||
<p class="muted">یک آیه را انتخاب کنید</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="ayah-block">
|
||||
<p class="ayah-ar">@detail.AyahTextAr</p>
|
||||
@if (!string.IsNullOrWhiteSpace(detail.AyahTextFa))
|
||||
{
|
||||
<p class="ayah-fa">@detail.AyahTextFa</p>
|
||||
}
|
||||
<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>
|
||||
|
||||
<AudioPlayer @key="detail.Id"
|
||||
Episode="detail"
|
||||
InitialPosition="initialPosition"
|
||||
IsFavorite="isFavorite"
|
||||
IsBookmarked="isBookmarked"
|
||||
OnFavoriteToggle="ToggleFavorite"
|
||||
OnBookmarkToggle="ToggleBookmark"
|
||||
OnProgress="SaveProgress" />
|
||||
}
|
||||
</div>
|
||||
</details>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
@code {
|
||||
private enum View { Surahs, Ayahs, Detail }
|
||||
|
||||
[Parameter] public string Slug { get; set; } = "";
|
||||
[Parameter] public int? SurahNumber { get; set; }
|
||||
[Parameter] public int? AyahNumber { get; set; }
|
||||
|
||||
private string title = "تفسیر";
|
||||
private string author = "";
|
||||
private Guid tafsirId;
|
||||
private bool loading = true;
|
||||
private string? loadedForSlug;
|
||||
private List<EpisodeListItemDto> episodes = [];
|
||||
private EpisodeListItemDto? selected;
|
||||
private EpisodeDetailDto? detail;
|
||||
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;
|
||||
@@ -74,31 +139,52 @@
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender) ready = true;
|
||||
if (!ready || string.IsNullOrWhiteSpace(Slug) || loadedForSlug == Slug) return;
|
||||
if (!firstRender) return;
|
||||
ready = true;
|
||||
deviceId = await DeviceStore.GetOrCreateAsync();
|
||||
await LoadRouteAsync();
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (!ready) return;
|
||||
await LoadRouteAsync();
|
||||
}
|
||||
|
||||
private async Task LoadRouteAsync()
|
||||
{
|
||||
var key = $"{Slug}|{SurahNumber}|{AyahNumber}";
|
||||
if (routeKey == key) return;
|
||||
routeKey = key;
|
||||
|
||||
loading = true;
|
||||
loadedForSlug = Slug;
|
||||
deviceId = await DeviceStore.GetOrCreateAsync();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(deviceId))
|
||||
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;
|
||||
|
||||
episodes = (await Api.GetEpisodesAsync(Slug)).ToList();
|
||||
|
||||
var progress = await Api.GetProgressAsync(deviceId);
|
||||
var startId = progress?.Episode?.Id;
|
||||
if (startId is null || episodes.All(e => e.Id != startId))
|
||||
startId = episodes.FirstOrDefault()?.Id;
|
||||
|
||||
if (startId is Guid id)
|
||||
if (AyahNumber is int a && SurahNumber is int s)
|
||||
{
|
||||
initialPosition = progress?.EpisodeId == id ? progress.PositionSeconds : 0;
|
||||
await SelectAsync(id, keepPosition: true);
|
||||
view = View.Detail;
|
||||
await LoadDetail(s, a);
|
||||
}
|
||||
else if (SurahNumber is int sn)
|
||||
{
|
||||
view = View.Ayahs;
|
||||
await LoadAyahs(sn);
|
||||
}
|
||||
else
|
||||
{
|
||||
view = View.Surahs;
|
||||
surahs = (await Api.GetSurahsAsync(deviceId, tafsirId == Guid.Empty ? null : tafsirId)).ToList();
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -108,25 +194,61 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SelectAsync(Guid id, bool keepPosition = false)
|
||||
private string slug => Slug;
|
||||
|
||||
private void BackToSurahs() => Nav.NavigateTo($"/tafsir/{Slug}");
|
||||
|
||||
private void OpenSurah(int n) => Nav.NavigateTo($"/tafsir/{Slug}/surah/{n}");
|
||||
|
||||
private void OpenAyah(int n)
|
||||
{
|
||||
selected = episodes.FirstOrDefault(e => e.Id == id);
|
||||
detail = await Api.GetEpisodeAsync(id);
|
||||
if (!keepPosition) initialPosition = 0;
|
||||
var s = SurahNumber ?? detail?.SurahNumber ?? selectedSurah?.Number ?? 1;
|
||||
Nav.NavigateTo($"/tafsir/{Slug}/surah/{s}/ayah/{n}");
|
||||
}
|
||||
|
||||
private async Task LoadAyahs(int surahNumber)
|
||||
{
|
||||
surahs = 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();
|
||||
}
|
||||
|
||||
private async Task LoadDetail(int surahNumber, int ayahNumber)
|
||||
{
|
||||
detail = await Api.GetAyahAsync(Slug, surahNumber, ayahNumber);
|
||||
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.GetBookmarksAsync(deviceId);
|
||||
isFavorite = favorites.Any(f => f.EpisodeId == id);
|
||||
isBookmarked = bookmarks.Any(b => b.EpisodeId == id);
|
||||
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;
|
||||
}
|
||||
|
||||
private async Task ToggleSurahBookmark(SurahListItemDto s)
|
||||
{
|
||||
if (tafsirId == Guid.Empty) return;
|
||||
if (s.IsBookmarked)
|
||||
await Api.RemoveSurahBookmarkAsync(tafsirId, s.Number, deviceId);
|
||||
else
|
||||
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.Id, deviceId);
|
||||
await Api.RemoveFavoriteAsync(detail.TafsirId, detail.SurahNumber, detail.AyahNumber, deviceId);
|
||||
else
|
||||
await Api.AddFavoriteAsync(new ToggleEpisodeRequest(detail.Id, deviceId));
|
||||
await Api.AddFavoriteAsync(new ToggleAyahRequest(detail.TafsirId, detail.SurahNumber, detail.AyahNumber, deviceId));
|
||||
isFavorite = !isFavorite;
|
||||
}
|
||||
|
||||
@@ -134,15 +256,16 @@
|
||||
{
|
||||
if (detail is null) return;
|
||||
if (isBookmarked)
|
||||
await Api.RemoveBookmarkAsync(detail.Id, deviceId);
|
||||
await Api.RemoveAyahBookmarkAsync(detail.TafsirId, detail.SurahNumber, detail.AyahNumber, deviceId);
|
||||
else
|
||||
await Api.AddBookmarkAsync(new ToggleEpisodeRequest(detail.Id, deviceId));
|
||||
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.Id, position, deviceId));
|
||||
await Api.SaveProgressAsync(new UpsertProgressRequest(
|
||||
detail.TafsirId, detail.SurahNumber, detail.AyahNumber, position, deviceId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,3 +226,49 @@ button, input { font: inherit; }
|
||||
.link-btn {
|
||||
margin-top: 0.85rem; border: 0; background: transparent; color: var(--brand-deep); cursor: pointer;
|
||||
}
|
||||
|
||||
.surah-grid { display: grid; gap: 0.55rem; }
|
||||
.surah-card {
|
||||
display: grid; grid-template-columns: 1fr auto; gap: 0.5rem; align-items: center;
|
||||
border: 1px solid var(--line); background: var(--surface); border-radius: 16px; padding: 0.35rem;
|
||||
}
|
||||
.surah-main {
|
||||
border: 0; background: transparent; cursor: pointer; text-align: right;
|
||||
display: grid; grid-template-columns: auto 1fr auto; gap: 0.75rem; align-items: center;
|
||||
padding: 0.65rem 0.75rem; width: 100%;
|
||||
}
|
||||
.surah-num {
|
||||
width: 2.2rem; height: 2.2rem; border-radius: 10px; display: grid; place-items: center;
|
||||
background: rgba(31,107,74,0.12); color: var(--brand-deep); font-weight: 700; font-size: 0.85rem;
|
||||
}
|
||||
.surah-names { display: flex; flex-direction: column; gap: 0.1rem; }
|
||||
.ayah-list { display: flex; flex-direction: column; gap: 0.4rem; max-height: 70vh; overflow: auto; }
|
||||
.ayah-row {
|
||||
border: 1px solid var(--line); background: var(--surface); border-radius: 14px;
|
||||
padding: 0.75rem 0.9rem; text-align: right; cursor: pointer;
|
||||
display: grid; grid-template-columns: auto 1fr auto; gap: 0.7rem; align-items: center;
|
||||
}
|
||||
.ayah-row:hover { border-color: rgba(31,107,74,0.45); background: #fff; }
|
||||
.ayah-num { font-weight: 700; color: var(--brand); }
|
||||
.ayah-preview { color: var(--muted); font-size: 0.92rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.ayah-flags { display: flex; gap: 0.25rem; }
|
||||
.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
||||
.dot.fav { background: #c45c26; }
|
||||
.dot.mark { background: var(--brand); }
|
||||
.dot.audio { background: #3b6ea5; }
|
||||
.section-title { margin: 1.25rem 0 0.6rem; font-size: 1.05rem; }
|
||||
.linkish { border: 0; background: transparent; color: var(--brand-deep); cursor: pointer; padding: 0; }
|
||||
.ayah-nav { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1rem; }
|
||||
.content-block {
|
||||
margin-top: 1rem; border: 1px solid var(--line); border-radius: 16px; background: var(--surface); padding: 0.85rem 1rem;
|
||||
}
|
||||
.content-block summary { cursor: pointer; font-weight: 600; margin-bottom: 0.5rem; }
|
||||
.html-body { line-height: 1.95; color: var(--ink); }
|
||||
.html-body p { margin: 0 0 0.65rem; }
|
||||
.video-wrap { position: relative; aspect-ratio: 16 / 9; max-width: 520px; }
|
||||
.video-wrap iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; border-radius: 12px; }
|
||||
.list-row {
|
||||
display: flex; flex-direction: column; gap: 0.2rem;
|
||||
padding: 0.9rem 1rem; border-radius: 14px; background: var(--surface); border: 1px solid var(--line);
|
||||
}
|
||||
.chip:disabled { opacity: 0.45; cursor: default; }
|
||||
|
||||
Reference in New Issue
Block a user