Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9d7663c
Add synced highlights flow
AhmedNMahran May 8, 2026
fb9ddb6
Align highlight localizations with saved branch state
AhmedNMahran May 8, 2026
b805ee3
Localize bookmark collection names via HighlightColor
AhmedNMahran May 8, 2026
1093257
Consume collection updates as async sequence for highlights
AhmedNMahran May 9, 2026
884f806
Fix synced highlight collection entry points
AhmedNMahran May 9, 2026
dc404a2
Ensure filtered highlight collections
AhmedNMahran May 9, 2026
1aabac4
Restore bookmark model imports
AhmedNMahran May 14, 2026
4973e9e
Import QuranKit for bookmark navigation
AhmedNMahran May 14, 2026
53c1e3e
Decouple highlight collection preparation
AhmedNMahran May 10, 2026
1f4db83
Hide standalone highlights bookmark row
AhmedNMahran May 10, 2026
f61d96e
Rename highlight color theme extension
AhmedNMahran May 16, 2026
28f3f2d
Add sync reading bookmark flow
AhmedNMahran May 9, 2026
6e821d8
Polish sync reading bookmark flow
AhmedNMahran May 9, 2026
faeb6e4
Polish reading bookmark presentation
AhmedNMahran May 9, 2026
d9bfc16
Keep reading bookmark nudge title on one line
AhmedNMahran May 9, 2026
9cc3a7d
Cover shared reading bookmark state
AhmedNMahran May 9, 2026
3740321
Move reading bookmark service to annotations domain
AhmedNMahran May 10, 2026
c256d2b
Clean up reading bookmark dependencies
AhmedNMahran May 10, 2026
71b2bd6
Fix reading bookmark presentation state
AhmedNMahran May 10, 2026
83df64e
Use AsyncButton in reading bookmark nudge
AhmedNMahran May 10, 2026
cca2d75
add missing import
AhmedNMahran May 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion Core/Localization/Resources/ar.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,17 @@
"bookmarks.sync.body" = "سجّل الدخول إلى Quran.com للاحتفاظ بعلاماتك المرجعية متاحة عبر أجهزتك.";
"bookmarks.sync.action" = "تسجيل الدخول";

"bookmarks.collections" = "المجموعات";
"bookmarks.collections.add" = "إضافة مجموعة";
"bookmarks.collections.edit" = "تحرير المجموعات";
"bookmarks.collections.new.placeholder" = "اسم المجموعة";
"bookmarks.collections.no-data.title" = "لا توجد مجموعات حتى الآن";
"bookmarks.collections.no-data.text" = "أنشئ مجموعة لتنظيم إشاراتك المرجعية للآيات.";
"bookmarks.collections.ayah-count" = "%d إشارات مرجعية للآيات";
"bookmarks.collections.ayahs.no-data.title" = "لا توجد إشارات مرجعية في هذه المجموعة";
"bookmarks.collections.ayahs.no-data.text" = "أضف إشارات مرجعية للآيات لرؤيتها هنا.";
"bookmarks.highlights" = "التمييزات";

// MARK: - Notes

"notes.no-data.title" = "إضافة آيات مرجعية...";
Expand Down Expand Up @@ -156,6 +167,14 @@
"ayah.menu.delete-note" = "حذف الملاحظة";
"ayah.menu.delete-highlight" = "حذف التحديد";

// MARK: - Highlights

"highlight.color.red" = "أحمر";
"highlight.color.green" = "أخضر";
"highlight.color.blue" = "أزرق";
"highlight.color.yellow" = "أصفر";
"highlight.color.purple" = "بنفسجي";

// MARK: - Reading Selector

// Selector UI
Expand Down Expand Up @@ -246,6 +265,19 @@
"bookmarks.collections.new.placeholder" = "اسم المجموعة";
"bookmarks.collections.no-data.title" = "لا توجد مجموعات حتى الآن";
"bookmarks.collections.no-data.text" = "أنشئ مجموعة لتنظيم إشاراتك المرجعية للآيات.";

"notes.minimum-length.alert.title" = "الملاحظة قصيرة جدًا";
"notes.minimum-length.alert.body" = "يجب أن تكون الملاحظة %d أحرف على الأقل.";

"highlight.color.red" = "أحمر";
"highlight.color.green" = "أخضر";
"highlight.color.blue" = "أزرق";
"highlight.color.yellow" = "أصفر";
"highlight.color.purple" = "بنفسجي";

// MARK: - Reading Bookmark
"reading-bookmark.education.title.prefix" = "هذه ";
"reading-bookmark.education.title.link" = "علامة قراءة متنقلة";
"reading-bookmark.education.body" = "حفظ علامة قراءة سيستبدل علامة القراءة القديمة. لحفظ عدة آيات، استخدم ميزة التمييز.";
"reading-bookmark.education.undo" = "تراجع";
"reading-bookmark.title" = "علامة القراءة";
"reading-bookmark.my-title" = "علامة القراءة الخاصة بي";
34 changes: 33 additions & 1 deletion Core/Localization/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,17 @@
"bookmarks.sync.body" = "Sign in to Quran.com to keep your bookmarks available across devices.";
"bookmarks.sync.action" = "Sign In";

"bookmarks.collections" = "Collections";
"bookmarks.collections.add" = "Add Collection";
"bookmarks.collections.edit" = "Edit Collections";
"bookmarks.collections.new.placeholder" = "Collection name";
"bookmarks.collections.no-data.title" = "No collections yet";
"bookmarks.collections.no-data.text" = "Create a collection to organize ayah bookmarks.";
"bookmarks.collections.ayah-count" = "%d ayah bookmarks";
"bookmarks.collections.ayahs.no-data.title" = "No bookmarks in this collection";
"bookmarks.collections.ayahs.no-data.text" = "Add ayah bookmarks to see them here.";
"bookmarks.highlights" = "Highlights";

// MARK: - Notes

"notes.no-data.title" = "Adding Ayah Highlights...";
Expand Down Expand Up @@ -160,6 +171,14 @@
"ayah.menu.delete-note" = "Delete Note";
"ayah.menu.delete-highlight" = "Delete Highlight";

// MARK: - Highlights

"highlight.color.red" = "Red";
"highlight.color.green" = "Green";
"highlight.color.blue" = "Blue";
"highlight.color.yellow" = "Yellow";
"highlight.color.purple" = "Purple";

// MARK: - Reading Selector

// Selector UI
Expand Down Expand Up @@ -252,8 +271,21 @@
"bookmarks.collections.new.placeholder" = "Collection name";
"bookmarks.collections.no-data.title" = "No collections yet";
"bookmarks.collections.no-data.text" = "Create a collection to organize your ayah bookmarks.";

"bookmarks.old-page-bookmarks" = "Old Page Bookmarks";

"notes.minimum-length.alert.title" = "Note is too short";
"notes.minimum-length.alert.body" = "Notes must be at least %d characters.";

"highlight.color.red" = "Red";
"highlight.color.green" = "Green";
"highlight.color.blue" = "Blue";
"highlight.color.yellow" = "Yellow";
"highlight.color.purple" = "Purple";

// MARK: - Reading Bookmark
"reading-bookmark.education.title.prefix" = "This is a movable ";
"reading-bookmark.education.title.link" = "reading bookmark";
"reading-bookmark.education.body" = "Saving a reading bookmark will now replace your old reading bookmark. To save multiple verses, please use the highlight feature.";
"reading-bookmark.education.undo" = "Undo";
"reading-bookmark.title" = "Reading Bookmark";
"reading-bookmark.my-title" = "My Reading Bookmark";
153 changes: 153 additions & 0 deletions Domain/AnnotationsService/Sources/ReadingBookmarkService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//
// ReadingBookmarkService.swift
//
// Created by Ahmed Nabil on 2026-05-09.
//

import Foundation
#if QURAN_SYNC
import MobileSync
import ReadingService
#endif
import QuranKit

public enum QuranReadingBookmark: Equatable {
case ayah(AyahNumber, Date)
case page(Page, Date)

public var ayah: AyahNumber {
switch self {
case .ayah(let ayah, _): ayah
case .page(let page, _): page.firstVerse
}
}

public var page: Page {
switch self {
case .ayah(let ayah, _): ayah.page
case .page(let page, _): page
}
}

public var lastUpdated: Date {
switch self {
case .ayah(_, let lastUpdated), .page(_, let lastUpdated): lastUpdated
}
}

public func isAyahBookmark(for ayah: AyahNumber) -> Bool {
guard case .ayah(let bookmarkedAyah, _) = self else {
return false
}
return bookmarkedAyah == ayah
}

public func isReadingBookmark(for ayah: AyahNumber) -> Bool {
switch self {
case .ayah(let bookmarkedAyah, _):
return bookmarkedAyah == ayah
case .page(let page, _):
return page == ayah.page
}
}

public func isPageBookmark(for pages: [Page]) -> Bool {
guard case .page(let page, _) = self else {
return false
}
return pages.contains(page)
}
}

#if QURAN_SYNC
public struct ReadingBookmarkSequence: AsyncSequence {
public typealias Element = QuranReadingBookmark?

public struct AsyncIterator: AsyncIteratorProtocol {
init<S: AsyncSequence>(_ sequence: S) where S.Element == Element {
var iterator = sequence.makeAsyncIterator()
nextValue = {
try await iterator.next()
}
}

public mutating func next() async throws -> Element? {
try await nextValue()
}

private let nextValue: () async throws -> Element?
}

init<S: AsyncSequence>(_ sequence: S) where S.Element == Element {
makeIterator = {
AsyncIterator(sequence)
}
}

public func makeAsyncIterator() -> AsyncIterator {
makeIterator()
}

private let makeIterator: () -> AsyncIterator
}

public struct ReadingBookmarkService {
// MARK: Lifecycle

public init(syncService: SyncService, readingPreferences: ReadingPreferences = .shared) {
self.syncService = syncService
self.readingPreferences = readingPreferences
}

// MARK: Public

public func readingBookmarkSequence() -> ReadingBookmarkSequence {
let sequence = syncService.readingBookmarkSequence()
.map { bookmark in
Self.bookmark(from: bookmark, quran: readingPreferences.reading.quran)
}
return ReadingBookmarkSequence(sequence)
}

@discardableResult
public func addReadingBookmark(page: Page) async throws -> QuranReadingBookmark {
let bookmark = try await syncService.addPageReadingBookmark(page: Int32(page.pageNumber))
return .page(page, bookmark.lastUpdated)
}

@discardableResult
public func addReadingBookmark(ayah: AyahNumber) async throws -> QuranReadingBookmark {
let bookmark = try await syncService.addAyahReadingBookmark(
sura: Int32(ayah.sura.suraNumber),
ayah: Int32(ayah.ayah)
)
return .ayah(ayah, bookmark.lastUpdated)
}

public func removeReadingBookmark() async throws {
_ = try await syncService.removeReadingBookmark()
}

public static func bookmark(from bookmark: (any ReadingBookmark)?, quran: Quran) -> QuranReadingBookmark? {
switch bookmark {
case let bookmark as AyahReadingBookmark:
return AyahNumber(quran: quran, sura: Int(bookmark.sura), ayah: Int(bookmark.ayah)).map {
QuranReadingBookmark.ayah($0, bookmark.lastUpdated)
}
case let bookmark as PageReadingBookmark:
return Page(quran: quran, pageNumber: Int(bookmark.page)).map {
QuranReadingBookmark.page($0, bookmark.lastUpdated)
}
default:
return nil
}
}

// MARK: Private

private let syncService: SyncService
private let readingPreferences: ReadingPreferences
}
#else
public struct ReadingBookmarkService {}
#endif
60 changes: 60 additions & 0 deletions Domain/AnnotationsService/Tests/ReadingBookmarkServiceTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#if QURAN_SYNC
import MobileSync
import QuranKit
import XCTest
@testable import AnnotationsService

final class ReadingBookmarkServiceTests: XCTestCase {
func test_bookmark_mapsAyahReadingBookmark() {
let bookmark = AyahReadingBookmark(
sura: 1,
ayah: 1,
lastUpdated: .distantPast,
localId: "ayah-bookmark"
)

XCTAssertEqual(
ReadingBookmarkService.bookmark(from: bookmark, quran: .hafsMadani1405),
.ayah(AyahNumber(quran: .hafsMadani1405, sura: 1, ayah: 1)!, .distantPast)
)
}

func test_bookmark_mapsPageReadingBookmark() {
let bookmark = PageReadingBookmark(
page: 1,
lastUpdated: .distantPast,
localId: "page-bookmark"
)

XCTAssertEqual(
ReadingBookmarkService.bookmark(from: bookmark, quran: .hafsMadani1405),
.page(Page(quran: .hafsMadani1405, pageNumber: 1)!, .distantPast)
)
}

func test_isReadingBookmark_matchesPageBookmarkAyah() {
let ayah = AyahNumber(quran: .hafsMadani1405, sura: 1, ayah: 1)!
let bookmark = QuranReadingBookmark.page(ayah.page, .distantPast)

XCTAssertTrue(bookmark.isReadingBookmark(for: ayah))
}

func test_isPageBookmark_doesNotMatchAyahBookmarkPage() {
let ayah = AyahNumber(quran: .hafsMadani1405, sura: 1, ayah: 1)!
let bookmark = QuranReadingBookmark.ayah(ayah, .distantPast)

XCTAssertFalse(bookmark.isPageBookmark(for: [ayah.page]))
}

func test_bookmark_skipsInvalidAyah() {
let bookmark = AyahReadingBookmark(
sura: 999,
ayah: 1,
lastUpdated: .distantPast,
localId: "invalid-ayah-bookmark"
)

XCTAssertNil(ReadingBookmarkService.bookmark(from: bookmark, quran: .hafsMadani1405))
}
}
#endif
Loading
Loading