diff --git a/src/TafsirQuran.UI/Components/AudioPlayer.razor b/src/TafsirQuran.UI/Components/AudioPlayer.razor index 84fb15c..bf1b7da 100644 --- a/src/TafsirQuran.UI/Components/AudioPlayer.razor +++ b/src/TafsirQuran.UI/Components/AudioPlayer.razor @@ -2,8 +2,8 @@ @inject ApiOptions ApiOptions @implements IAsyncDisposable -
صوت این آیه در دسترس نیست
+صوت این آیه در دسترس نیست
}@detail.TextAr
@if (!string.IsNullOrWhiteSpace(detail.TextFa)) diff --git a/src/TafsirQuran.UI/Services/PlayerSessionService.cs b/src/TafsirQuran.UI/Services/PlayerSessionService.cs index a0190b7..b68aae5 100644 --- a/src/TafsirQuran.UI/Services/PlayerSessionService.cs +++ b/src/TafsirQuran.UI/Services/PlayerSessionService.cs @@ -5,10 +5,16 @@ namespace TafsirQuran.UI.Services; public sealed class PlayerSessionService { + private const double PersistAfterPlayedSeconds = 2.5; + private readonly TafsirApiClient _api; private readonly IDeviceIdStore _devices; private bool _loaded; private string _deviceId = ""; + private DateTimeOffset? _playSegmentStarted; + private double _playedAccumulated; + private bool _persistedForTrack; + private DateTimeOffset _lastPersistAt = DateTimeOffset.MinValue; public PlayerSessionService(TafsirApiClient api, IDeviceIdStore devices) { @@ -52,6 +58,7 @@ public sealed class PlayerSessionService AyahNumber = progress.AyahNumber; SurahNameFa = progress.SurahNameFa ?? ""; PositionSeconds = progress.PositionSeconds; + _persistedForTrack = true; var tafsirs = await _api.GetTafsirsAsync(); var tafsir = tafsirs.FirstOrDefault(t => t.Id == progress.TafsirId); @@ -78,6 +85,10 @@ public sealed class PlayerSessionService public async Task SetTrackAsync(AyahDetailDto detail, double position = 0) { await EnsureDeviceAsync(); + var same = TafsirId == detail.TafsirId + && SurahNumber == detail.SurahNumber + && AyahNumber == detail.AyahNumber; + TafsirId = detail.TafsirId; TafsirSlug = detail.TafsirSlug; SurahNumber = detail.SurahNumber; @@ -85,15 +96,57 @@ public sealed class PlayerSessionService SurahNameFa = detail.SurahNameFa; AudioUrl = detail.AudioUrl; PositionSeconds = position; + + if (!same) + { + _playSegmentStarted = null; + _playedAccumulated = 0; + _persistedForTrack = false; + } + await SyncFlagsAsync(); Notify(); - await PersistAsync(position); + // Do not persist yet — wait until user actually plays ~2.5s + } + + public void MarkPlaying(bool isPlaying) + { + if (isPlaying) + { + _playSegmentStarted ??= DateTimeOffset.UtcNow; + } + else if (_playSegmentStarted is DateTimeOffset start) + { + _playedAccumulated += (DateTimeOffset.UtcNow - start).TotalSeconds; + _playSegmentStarted = null; + } } public async Task ReportPositionAsync(double position) { if (!HasTrack) return; PositionSeconds = position; + + var played = _playedAccumulated; + if (_playSegmentStarted is DateTimeOffset start) + played += (DateTimeOffset.UtcNow - start).TotalSeconds; + + // Only save resume after ~2.5s of real playback on this track + 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(); } diff --git a/src/TafsirQuran.UI/wwwroot/css/app.css b/src/TafsirQuran.UI/wwwroot/css/app.css index 60dd22f..87d928a 100644 --- a/src/TafsirQuran.UI/wwwroot/css/app.css +++ b/src/TafsirQuran.UI/wwwroot/css/app.css @@ -291,9 +291,13 @@ button, input { font: inherit; } } .title-author { font-weight: 500; color: var(--muted); font-size: 0.92em; } .back-btn { - flex-shrink: 0; width: 42px; height: 42px; border-radius: 12px; + flex-shrink: 0; width: 52px; height: 52px; border-radius: 14px; border: 1px solid var(--line); background: #fff; cursor: pointer; - font-size: 1.25rem; line-height: 1; color: var(--brand-deep); + font-size: 1.6rem; line-height: 1; color: var(--brand-deep); + display: grid; place-items: center; +} +.ayah-page-title { + margin: 0 0 0.85rem; font-size: 1.1rem; font-weight: 700; color: var(--brand-deep); } .skeleton-stack { display: grid; gap: 0.65rem; margin: 0.5rem 0 1rem; } @@ -313,39 +317,45 @@ button, input { font: inherit; } .player-dock { position: fixed; left: 0; right: 0; bottom: 0; z-index: 30; - padding: 0.65rem 0.85rem calc(0.65rem + env(safe-area-inset-bottom)); - background: rgba(243, 239, 230, 0.94); - backdrop-filter: blur(14px); + padding: 0; + padding-bottom: env(safe-area-inset-bottom); + background: #fff; border-top: 1px solid var(--line); box-shadow: 0 -10px 28px rgba(28, 43, 36, 0.08); } .dock-player { - max-width: 980px; margin: 0 auto; - border: 1px solid var(--line); + max-width: none; margin: 0; width: 100%; + border: 0; border-radius: 0; background: #fff; - border-radius: 18px; - padding: 0.75rem 0.9rem 0.85rem; + padding: 0.85rem 1rem 0.95rem; direction: rtl; text-align: right; + box-sizing: border-box; } .dock-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 0.75rem; - margin-bottom: 0.55rem; + margin-bottom: 0.65rem; } .dock-title { border: 0; background: transparent; padding: 0; cursor: pointer; - font-weight: 700; font-size: 0.98rem; color: var(--ink); text-align: right; + font-weight: 700; font-size: 1.05rem; color: var(--ink); text-align: right; } -.dock-actions { display: flex; gap: 0.35rem; flex-shrink: 0; } +.dock-actions { display: flex; gap: 0.45rem; flex-shrink: 0; } .icon-chip { - width: 36px; height: 36px; border-radius: 10px; - border: 1px solid var(--line); background: #fff; cursor: pointer; - color: var(--muted); font-size: 1rem; + width: 44px; height: 44px; border-radius: 12px; + border: 1px solid rgba(28, 43, 36, 0.22); background: #f3f6f4; cursor: pointer; + color: #1c2b24; font-size: 1.35rem; + display: grid; place-items: center; } .icon-chip.on { background: var(--brand); color: #fff; border-color: var(--brand); } -.rtl-controls { - display: grid; grid-template-columns: auto 1fr auto; gap: 0.65rem; align-items: center; - direction: rtl; +.ltr-controls { + display: grid; grid-template-columns: auto 1fr auto; gap: 0.7rem; align-items: center; + direction: ltr; +} +.ltr-controls .seek { direction: ltr; } +.ltr-controls .time { + font-variant-numeric: tabular-nums; font-size: 0.85rem; color: var(--muted); white-space: nowrap; + min-width: 5.5rem; text-align: left; } .dock-empty { margin: 0; font-size: 0.9rem; }