diff --git a/Domain/ReadingService/Sources/ReadingResourcesService.swift b/Domain/ReadingService/Sources/ReadingResourcesService.swift index 950073ad..2ad22e8d 100644 --- a/Domain/ReadingService/Sources/ReadingResourcesService.swift +++ b/Domain/ReadingService/Sources/ReadingResourcesService.swift @@ -52,7 +52,7 @@ public actor ReadingResourcesService { .eraseToAnyPublisher() } - public func startLoadingResources() async { + public func startLoadingResources() { let initialReading = preferences.reading readingsTask = Task { [weak self] in guard let readings = self?.preferences.$reading.prepend(initialReading).values() else { diff --git a/Features/QuranImageFeature/ContentImageBuilder.swift b/Features/QuranImageFeature/ContentImageBuilder.swift index 40a2965d..31ae5772 100644 --- a/Features/QuranImageFeature/ContentImageBuilder.swift +++ b/Features/QuranImageFeature/ContentImageBuilder.swift @@ -29,13 +29,7 @@ public struct ContentImageBuilder: PageViewBuilder { self.highlightsService = highlightsService let reading = ReadingPreferences.shared.reading - let readingDirectory = Self.readingDirectory(reading, container: container) - - let imageService = ImageDataService( - ayahInfoDatabase: reading.ayahInfoDatabase(in: readingDirectory), - imagesURL: reading.images(in: readingDirectory), - cropInsets: reading.cropInsets - ) + let imageService = Self.buildImageDataService(reading: reading, container: container) let pages = reading.quran.pages cacheableImageService = Self.createCahceableImageService(imageService: imageService, pages: pages) @@ -53,6 +47,17 @@ public struct ContentImageBuilder: PageViewBuilder { ) } + // MARK: Internal + + static func buildImageDataService(reading: Reading, container: AppDependencies) -> ImageDataService { + let readingDirectory = Self.readingDirectory(reading, container: container) + return ImageDataService( + ayahInfoDatabase: reading.ayahInfoDatabase(in: readingDirectory), + imagesURL: reading.images(in: readingDirectory), + cropInsets: reading.cropInsets + ) + } + // MARK: Private private let container: AppDependencies diff --git a/Features/QuranTranslationFeature/TranslationItem+View.swift b/Features/QuranTranslationFeature/TranslationItem+View.swift index f2915813..0bf7f147 100644 --- a/Features/QuranTranslationFeature/TranslationItem+View.swift +++ b/Features/QuranTranslationFeature/TranslationItem+View.swift @@ -5,7 +5,6 @@ // Created by Mohamed Afifi on 2023-12-28. // -import Localization import NoorUI import QuranKit import QuranText @@ -13,172 +12,71 @@ import SwiftUI extension TranslationPageHeader: View { var body: some View { - HStack { - Text(page.localizedQuarterName) - Spacer() - page.suraNames() - .view(ofSize: .footnote, alignment: .trailing) - } - .readableInsetsPadding([.top, .horizontal]) - .padding(.bottom, ContentDimension.interSpacing) + QuranPageHeader(quarterName: page.localizedQuarterName, suraNames: page.suraNames()) } } extension TranslationPageFooter: View { var body: some View { - HStack { - Spacer() - Text(page.localizedNumber) - Spacer() - } - .padding(.top, ContentDimension.interSpacing) - .readableInsetsPadding([.bottom, .horizontal]) + QuranPageFooter(page: page.localizedNumber) } } -extension TranslationVerseSeparator: View { +extension TranslationSuraName: View { var body: some View { - Rectangle() - .fill(Color.systemGray4) - .frame(height: 1) - .frame(maxWidth: .infinity, alignment: .trailing) - .padding(.top, ContentDimension.interSpacing) + QuranSuraName(suraName: sura.localizedName(withPrefix: false), besmAllah: sura.quran.arabicBesmAllah, besmAllahFontSize: arabicFontSize) } } -struct TranslationSuraNameView: View { - @ScaledMetric var bottomPadding = 5 - @ScaledMetric var topPadding = 10 - - let suraName: TranslationSuraName - +extension TranslationArabicText: View { var body: some View { - VStack { - NoorImage.suraHeader.image.resizable() - .aspectRatio(contentMode: .fit) - .overlay { - Text(suraName.sura.localizedName(withPrefix: false)) - .font(.title3) - .lineLimit(1) - .minimumScaleFactor(0.3) - } - Text(suraName.sura.quran.arabicBesmAllah) - .font(.quran()) - .dynamicTypeSize(suraName.arabicFontSize.dynamicTypeSize) - } - .padding(.bottom, bottomPadding) - .padding(.top, topPadding) - .readableInsetsPadding(.horizontal) + QuranArabicText(verse: verse, text: text, fontSize: arabicFontSize) } } -struct TranslationArabicTextView: View { - @ScaledMetric var bottomPadding = 5 - @ScaledMetric var topPadding = 10 - - let arabicText: TranslationArabicText - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - Text(lFormat("translation.text.ayah-number", arabicText.verse.sura.suraNumber, arabicText.verse.ayah)) - .padding(8) - .foregroundColor(.secondaryLabel) - .background( - RoundedRectangle(cornerRadius: 6) - .fill(Color.systemGray5.opacity(0.5)) - ) - - Text(arabicText.text) - .font(.quran()) - .dynamicTypeSize(arabicText.arabicFontSize.dynamicTypeSize) - .textAlignment(follows: .rightToLeft) - } - .padding(.bottom, bottomPadding) - .padding(.top, topPadding) - .readableInsetsPadding(.horizontal) +extension TranslationTextChunk { + var readMoreURL: URL { + TranslationURL.readMore( + translationId: translation.id, + sura: verse.sura.suraNumber, + ayah: verse.ayah + ).url } } -struct TranslationTextChunkView: View { - @ScaledMetric var topPadding = 10 - @ScaledMetric var baselineOffset = 5 - - let chunk: TranslationTextChunk - +extension TranslationTextChunk: View { var body: some View { - Text(string) - .font(chunk.translation.textFont) - .dynamicTypeSize(chunk.translationFontSize.dynamicTypeSize) - .textAlignment(follows: chunk.translation.characterDirection) - .padding(.top, chunk.chunkIndex == 0 ? topPadding : 0) - .readableInsetsPadding(.horizontal) - } - - private var string: AttributedString { - let chunkRange = chunk.chunks[chunk.chunkIndex] - let chunkText = chunk.text.text[chunkRange] - - var string = AttributedString(chunkText) - for (index, footnoteRange) in chunk.text.footnoteRanges.enumerated() { - if let range = string.range(from: footnoteRange, overallRange: chunkRange, overallText: chunk.text.text) { - string[range].link = TranslationURL.footnote( - translationId: chunk.translation.id, - sura: chunk.verse.sura.suraNumber, - ayah: chunk.verse.ayah, + QuranTranslationTextChunk( + text: text.text, + chunk: chunks[chunkIndex], + footnoteRanges: text.footnoteRanges, + quranRanges: text.quranRanges, + firstChunk: chunkIndex == 0, + readMoreURL: readMore ? readMoreURL : nil, + footnoteURL: { index in + TranslationURL.footnote( + translationId: translation.id, + sura: verse.sura.suraNumber, + ayah: verse.ayah, footnoteIndex: index ).url - string[range].font = .footnote - string[range].baselineOffset = baselineOffset - } - } - - for quranRange in chunk.text.quranRanges { - if let range = string.range(from: quranRange, overallRange: chunkRange, overallText: chunk.text.text) { - string[range].foregroundColor = .accentColor - } - } - - if chunk.readMore { - var readMore = AttributedString("\n\(l("translation.text.read-more"))") - readMore.foregroundColor = .accentColor - readMore.link = TranslationURL.readMore( - translationId: chunk.translation.id, - sura: chunk.verse.sura.suraNumber, - ayah: chunk.verse.ayah - ).url - readMore.font = .body - string.append(readMore) - } - - return string + }, + font: translation.textFont, + fontSize: translationFontSize, + characterDirection: translation.characterDirection + ) } } -struct TranslationReferenceVerseView: View { - @ScaledMetric var topPadding = 10 - let referenceVerse: TranslationReferenceVerse - +extension TranslationReferenceVerse: View { var body: some View { - Text(lFormat("translation.text.see-referenced-verse", referenceVerse.reference.ayah)) - .font(.body) - .dynamicTypeSize(referenceVerse.translationFontSize.dynamicTypeSize) - .textAlignment(follows: referenceVerse.translation.characterDirection) - .padding(.top, topPadding) - .readableInsetsPadding(.horizontal) + QuranTranslationReferenceVerse(reference: reference, fontSize: translationFontSize, characterDirection: translation.characterDirection) } } -struct TranslatorTextView: View { - @ScaledMetric var bottomPadding = 10 - let translator: TranslatorText +extension TranslatorText: View { var body: some View { - Text(verbatim: "- \(translator.translation.translationName)") - .foregroundColor(.secondaryLabel) - .font(.body) - .dynamicTypeSize(translator.translationFontSize.dynamicTypeSize) - .textAlignment(follows: translator.translation.characterDirection) - .padding(.bottom, bottomPadding) - .readableInsetsPadding(.horizontal) + QuranTranslatorName(name: translation.translationName, fontSize: translationFontSize, characterDirection: translation.characterDirection) } } @@ -190,18 +88,18 @@ extension TranslationItem: View { pageHeader case .pageFooter(let pageFooter): pageFooter - case .verseSeparator(let separator, _): - separator + case .verseSeparator: + QuranVerseSeparator() case .suraName(let suraName, _): - TranslationSuraNameView(suraName: suraName) + suraName case .arabicText(let arabicText, _): - TranslationArabicTextView(arabicText: arabicText) + arabicText case .translationTextChunk(let translationTextChunk, _): - TranslationTextChunkView(chunk: translationTextChunk) + translationTextChunk case .translationReferenceVerse(let translationReferenceVerse, _): - TranslationReferenceVerseView(referenceVerse: translationReferenceVerse) + translationReferenceVerse case .translatorText(let translatorText, _): - TranslatorTextView(translator: translatorText) + translatorText } } .font(.footnote) diff --git a/UI/NoorUI/Features/Quran/QuranArabicText.swift b/UI/NoorUI/Features/Quran/QuranArabicText.swift new file mode 100644 index 00000000..b71c8fad --- /dev/null +++ b/UI/NoorUI/Features/Quran/QuranArabicText.swift @@ -0,0 +1,46 @@ +// +// QuranArabicText.swift +// +// +// Created by Mohamed Afifi on 2024-02-10. +// + +import Localization +import QuranKit +import QuranText +import SwiftUI + +public struct QuranArabicText: View { + @ScaledMetric var bottomPadding = 5 + @ScaledMetric var topPadding = 10 + + let verse: AyahNumber + let text: String + let fontSize: FontSize + + public init(verse: AyahNumber, text: String, fontSize: FontSize) { + self.verse = verse + self.text = text + self.fontSize = fontSize + } + + public var body: some View { + VStack(alignment: .leading, spacing: 0) { + Text(lFormat("translation.text.ayah-number", verse.sura.suraNumber, verse.ayah)) + .padding(8) + .foregroundColor(.secondaryLabel) + .background( + RoundedRectangle(cornerRadius: 6) + .fill(Color.systemGray5.opacity(0.5)) + ) + + Text(text) + .font(.quran()) + .dynamicTypeSize(fontSize.dynamicTypeSize) + .textAlignment(follows: .rightToLeft) + } + .padding(.bottom, bottomPadding) + .padding(.top, topPadding) + .readableInsetsPadding(.horizontal) + } +} diff --git a/UI/NoorUI/Features/Quran/QuranPageFooter.swift b/UI/NoorUI/Features/Quran/QuranPageFooter.swift new file mode 100644 index 00000000..5a564f65 --- /dev/null +++ b/UI/NoorUI/Features/Quran/QuranPageFooter.swift @@ -0,0 +1,26 @@ +// +// QuranPageFooter.swift +// +// +// Created by Mohamed Afifi on 2024-02-10. +// + +import SwiftUI + +public struct QuranPageFooter: View { + let page: String + + public init(page: String) { + self.page = page + } + + public var body: some View { + HStack { + Spacer() + Text(page) + Spacer() + } + .padding(.top, ContentDimension.interSpacing) + .readableInsetsPadding([.bottom, .horizontal]) + } +} diff --git a/UI/NoorUI/Features/Quran/QuranPageHeader.swift b/UI/NoorUI/Features/Quran/QuranPageHeader.swift new file mode 100644 index 00000000..841603ca --- /dev/null +++ b/UI/NoorUI/Features/Quran/QuranPageHeader.swift @@ -0,0 +1,29 @@ +// +// QuranPageHeader.swift +// +// +// Created by Mohamed Afifi on 2024-02-10. +// + +import SwiftUI + +public struct QuranPageHeader: View { + private let quarterName: String + private let suraNames: MultipartText + + public init(quarterName: String, suraNames: MultipartText) { + self.quarterName = quarterName + self.suraNames = suraNames + } + + public var body: some View { + HStack { + Text(quarterName) + Spacer() + suraNames + .view(ofSize: .footnote, alignment: .trailing) + } + .readableInsetsPadding([.top, .horizontal]) + .padding(.bottom, ContentDimension.interSpacing) + } +} diff --git a/UI/NoorUI/Pager/QuranPageSeparators.swift b/UI/NoorUI/Features/Quran/QuranPageSeparators.swift similarity index 100% rename from UI/NoorUI/Pager/QuranPageSeparators.swift rename to UI/NoorUI/Features/Quran/QuranPageSeparators.swift diff --git a/UI/NoorUI/Features/Quran/QuranSuraName.swift b/UI/NoorUI/Features/Quran/QuranSuraName.swift new file mode 100644 index 00000000..fd175c65 --- /dev/null +++ b/UI/NoorUI/Features/Quran/QuranSuraName.swift @@ -0,0 +1,43 @@ +// +// QuranSuraName.swift +// +// +// Created by Mohamed Afifi on 2024-02-10. +// + +import QuranText +import SwiftUI + +public struct QuranSuraName: View { + @ScaledMetric var bottomPadding = 5 + @ScaledMetric var topPadding = 10 + + let suraName: String + let besmAllah: String + let besmAllahFontSize: FontSize + + public init(suraName: String, besmAllah: String, besmAllahFontSize: FontSize) { + self.suraName = suraName + self.besmAllah = besmAllah + self.besmAllahFontSize = besmAllahFontSize + } + + public var body: some View { + VStack { + NoorImage.suraHeader.image.resizable() + .aspectRatio(contentMode: .fit) + .overlay { + Text(suraName) + .font(.title3) + .lineLimit(1) + .minimumScaleFactor(0.3) + } + Text(besmAllah) + .font(.quran()) + .dynamicTypeSize(besmAllahFontSize.dynamicTypeSize) + } + .padding(.bottom, bottomPadding) + .padding(.top, topPadding) + .readableInsetsPadding(.horizontal) + } +} diff --git a/UI/NoorUI/Features/Quran/QuranTranslationReferenceVerse.swift b/UI/NoorUI/Features/Quran/QuranTranslationReferenceVerse.swift new file mode 100644 index 00000000..0941a3c5 --- /dev/null +++ b/UI/NoorUI/Features/Quran/QuranTranslationReferenceVerse.swift @@ -0,0 +1,35 @@ +// +// QuranTranslationReferenceVerse.swift +// +// +// Created by Mohamed Afifi on 2024-02-10. +// + +import Localization +import QuranKit +import QuranText +import SwiftUI +import UIx + +public struct QuranTranslationReferenceVerse: View { + @ScaledMetric var topPadding = 10 + + let reference: AyahNumber + let fontSize: FontSize + let characterDirection: Locale.LanguageDirection + + public init(reference: AyahNumber, fontSize: FontSize, characterDirection: Locale.LanguageDirection) { + self.reference = reference + self.fontSize = fontSize + self.characterDirection = characterDirection + } + + public var body: some View { + Text(lFormat("translation.text.see-referenced-verse", reference.ayah)) + .font(.body) + .dynamicTypeSize(fontSize.dynamicTypeSize) + .textAlignment(follows: characterDirection) + .padding(.top, topPadding) + .readableInsetsPadding(.horizontal) + } +} diff --git a/UI/NoorUI/Features/Quran/QuranTranslationTextChunk.swift b/UI/NoorUI/Features/Quran/QuranTranslationTextChunk.swift new file mode 100644 index 00000000..80506807 --- /dev/null +++ b/UI/NoorUI/Features/Quran/QuranTranslationTextChunk.swift @@ -0,0 +1,80 @@ +// +// QuranTranslationTextChunk.swift +// +// +// Created by Mohamed Afifi on 2024-02-10. +// + +import Localization +import QuranText +import SwiftUI +import UIx + +public struct QuranTranslationTextChunk: View { + @ScaledMetric var topPadding = 10 + @ScaledMetric var baselineOffset = 5 + + let text: String + let chunk: Range + let footnoteRanges: [Range] + let quranRanges: [Range] + + let firstChunk: Bool + let readMoreURL: URL? + let footnoteURL: (Int) -> URL + + let font: Font + let fontSize: FontSize + let characterDirection: Locale.LanguageDirection + + public init(text: String, chunk: Range, footnoteRanges: [Range], quranRanges: [Range], firstChunk: Bool, readMoreURL: URL?, footnoteURL: @escaping (Int) -> URL, font: Font, fontSize: FontSize, characterDirection: Locale.LanguageDirection) { + self.text = text + self.chunk = chunk + self.footnoteRanges = footnoteRanges + self.quranRanges = quranRanges + self.firstChunk = firstChunk + self.readMoreURL = readMoreURL + self.footnoteURL = footnoteURL + self.font = font + self.fontSize = fontSize + self.characterDirection = characterDirection + } + + public var body: some View { + Text(string) + .font(font) + .dynamicTypeSize(fontSize.dynamicTypeSize) + .textAlignment(follows: characterDirection) + .padding(.top, firstChunk ? topPadding : 0) + .readableInsetsPadding(.horizontal) + } + + private var string: AttributedString { + let chunkText = text[chunk] + + var string = AttributedString(chunkText) + for (index, footnoteRange) in footnoteRanges.enumerated() { + if let range = string.range(from: footnoteRange, overallRange: chunk, overallText: text) { + string[range].link = footnoteURL(index) + string[range].font = .footnote + string[range].baselineOffset = baselineOffset + } + } + + for quranRange in quranRanges { + if let range = string.range(from: quranRange, overallRange: chunk, overallText: text) { + string[range].foregroundColor = .accentColor + } + } + + if let readMoreURL { + var readMore = AttributedString("\n\(l("translation.text.read-more"))") + readMore.foregroundColor = .accentColor + readMore.link = readMoreURL + readMore.font = .body + string.append(readMore) + } + + return string + } +} diff --git a/UI/NoorUI/Features/Quran/QuranTranslatorName.swift b/UI/NoorUI/Features/Quran/QuranTranslatorName.swift new file mode 100644 index 00000000..065795b6 --- /dev/null +++ b/UI/NoorUI/Features/Quran/QuranTranslatorName.swift @@ -0,0 +1,34 @@ +// +// QuranTranslatorName.swift +// +// +// Created by Mohamed Afifi on 2024-02-10. +// + +import QuranText +import SwiftUI +import UIx + +public struct QuranTranslatorName: View { + @ScaledMetric var bottomPadding = 10 + + let name: String + let fontSize: FontSize + let characterDirection: Locale.LanguageDirection + + public init(name: String, fontSize: FontSize, characterDirection: Locale.LanguageDirection) { + self.name = name + self.fontSize = fontSize + self.characterDirection = characterDirection + } + + public var body: some View { + Text(verbatim: "- \(name)") + .foregroundColor(.secondaryLabel) + .font(.body) + .dynamicTypeSize(fontSize.dynamicTypeSize) + .textAlignment(follows: characterDirection) + .padding(.bottom, bottomPadding) + .readableInsetsPadding(.horizontal) + } +} diff --git a/UI/NoorUI/Features/Quran/QuranVerseSeparator.swift b/UI/NoorUI/Features/Quran/QuranVerseSeparator.swift new file mode 100644 index 00000000..a6881d88 --- /dev/null +++ b/UI/NoorUI/Features/Quran/QuranVerseSeparator.swift @@ -0,0 +1,20 @@ +// +// QuranVerseSeparator.swift +// +// +// Created by Mohamed Afifi on 2024-02-10. +// + +import SwiftUI + +public struct QuranVerseSeparator: View { + public init() { } + + public var body: some View { + Rectangle() + .fill(Color.systemGray4) + .frame(height: 1) + .frame(maxWidth: .infinity, alignment: .trailing) + .padding(.top, ContentDimension.interSpacing) + } +} diff --git a/Features/QuranTranslationFeature/TranslationReadableInsets.swift b/UI/NoorUI/Miscellaneous/ReadableInsetsViewModifier.swift similarity index 56% rename from Features/QuranTranslationFeature/TranslationReadableInsets.swift rename to UI/NoorUI/Miscellaneous/ReadableInsetsViewModifier.swift index 9bd986d7..46c32fce 100644 --- a/Features/QuranTranslationFeature/TranslationReadableInsets.swift +++ b/UI/NoorUI/Miscellaneous/ReadableInsetsViewModifier.swift @@ -1,36 +1,35 @@ // -// TranslationReadableInsets.swift +// ReadableInsetsViewModifier.swift // // // Created by Mohamed Afifi on 2024-01-19. // -import NoorUI import SwiftUI private extension EnvironmentValues { - var translationReadableInsets: EdgeInsets { - get { self[TranslationReadableInsetsKey.self] } - set { self[TranslationReadableInsetsKey.self] = newValue } + var readableInsets: EdgeInsets { + get { self[ReadableInsetsKey.self] } + set { self[ReadableInsetsKey.self] = newValue } } } -private struct TranslationReadableInsetsKey: EnvironmentKey { +private struct ReadableInsetsKey: EnvironmentKey { static let defaultValue = EdgeInsets() } -private struct TranslationReadableInsetsModifier: ViewModifier { +private struct ReadableInsetsModifier: ViewModifier { @State var windowSafeAreaInsets: EdgeInsets = .zero func body(content: Content) -> some View { content .readWindowSafeAreaInsets($windowSafeAreaInsets) - .environment(\.translationReadableInsets, ContentDimension.readableInsets(of: windowSafeAreaInsets)) + .environment(\.readableInsets, ContentDimension.readableInsets(of: windowSafeAreaInsets)) } } private struct ReadableInsetsPadding: ViewModifier { - @Environment(\.translationReadableInsets) var readableInsets + @Environment(\.readableInsets) var readableInsets let edges: Edge.Set func body(content: Content) -> some View { @@ -43,11 +42,11 @@ private struct ReadableInsetsPadding: ViewModifier { } extension View { - func populateReadableInsets() -> some View { - modifier(TranslationReadableInsetsModifier()) + public func populateReadableInsets() -> some View { + modifier(ReadableInsetsModifier()) } - func readableInsetsPadding(_ edges: Edge.Set = .all) -> some View { + public func readableInsetsPadding(_ edges: Edge.Set = .all) -> some View { modifier(ReadableInsetsPadding(edges: edges)) } }