diff --git a/features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderImpl.kt b/features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderImpl.kt index 3a743d15f..725432110 100644 --- a/features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderImpl.kt +++ b/features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderImpl.kt @@ -40,6 +40,7 @@ import com.pspdfkit.document.download.DownloadJob import com.pspdfkit.document.download.DownloadRequest import com.pspdfkit.ui.PdfActivityIntentBuilder import dagger.hilt.android.qualifiers.ApplicationContext +import io.adventech.blockkit.model.resource.PdfAux import kotlinx.coroutines.withContext import ss.foundation.coroutines.DispatcherProvider import ss.libraries.circuit.navigation.PdfScreen @@ -115,7 +116,7 @@ internal class PdfReaderImpl @Inject constructor( } } - override fun isDownloaded(pdf: LessonPdf): Boolean { + override fun isDownloaded(pdf: PdfAux): Boolean { return File(context.getDir(FILE_DIRECTORY, Context.MODE_PRIVATE), "${pdf.id}.pdf").exists() } diff --git a/features/resource/build.gradle.kts b/features/resource/build.gradle.kts index 87b7cb46c..91fc04a15 100644 --- a/features/resource/build.gradle.kts +++ b/features/resource/build.gradle.kts @@ -35,6 +35,7 @@ android { foundry { features { compose() } + android { features { snapshotTests() } } } ksp { diff --git a/features/resource/src/main/kotlin/ss/resource/ResourcePresenter.kt b/features/resource/src/main/kotlin/ss/resource/ResourcePresenter.kt index 797db9c0a..253dffe2d 100644 --- a/features/resource/src/main/kotlin/ss/resource/ResourcePresenter.kt +++ b/features/resource/src/main/kotlin/ss/resource/ResourcePresenter.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue +import app.ss.models.OfflineState import com.slack.circuit.codegen.annotations.CircuitInject import com.slack.circuit.retained.produceRetainedState import com.slack.circuit.retained.rememberRetained @@ -68,9 +69,10 @@ class ResourcePresenter @AssistedInject constructor( val ctaScreen = resourceCtaScreenProducer(resource) val readDocumentTitle = rememberRetained(resource, ctaScreen) { (ctaScreen as? CtaScreenState.Default)?.title?.takeIf { - resource?.progressTracking in setOf(ProgressTracking.AUTOMATIC, ProgressTracking.AUTOMATIC) == true + resource?.progressTracking in setOf(ProgressTracking.AUTOMATIC, ProgressTracking.AUTOMATIC) } ?: "" } + val offlineState by rememberOfflineState() var overlayState by rememberRetained { mutableStateOf(null) } @@ -103,6 +105,7 @@ class ResourcePresenter @AssistedInject constructor( features = features, fontFamilyProvider = fontFamilyProvider, overlayState = overlayState, + offlineState = offlineState, eventSink = eventSink, ) @@ -118,6 +121,11 @@ class ResourcePresenter @AssistedInject constructor( resourcesRepository.resource(screen.index).collect { value = it } } + @Composable + private fun rememberOfflineState() = produceRetainedState(OfflineState.NONE) { + resourcesRepository.resourceOfflineState(screen.index).collect { value = it } + } + @CircuitInject(ResourceScreen::class, SingletonComponent::class) @AssistedFactory interface Factory { diff --git a/features/resource/src/main/kotlin/ss/resource/ResourceState.kt b/features/resource/src/main/kotlin/ss/resource/ResourceState.kt index a366f675b..3e1b9cfa2 100644 --- a/features/resource/src/main/kotlin/ss/resource/ResourceState.kt +++ b/features/resource/src/main/kotlin/ss/resource/ResourceState.kt @@ -22,6 +22,7 @@ package ss.resource +import app.ss.models.OfflineState import com.slack.circuit.runtime.CircuitUiEvent import com.slack.circuit.runtime.CircuitUiState import io.adventech.blockkit.model.resource.Resource @@ -51,6 +52,7 @@ sealed interface State: CircuitUiState { val features: ImmutableList, val fontFamilyProvider: FontFamilyProvider, val overlayState: ResourceOverlayState?, + val offlineState: OfflineState, override val eventSink: (Event) -> Unit ): State diff --git a/features/resource/src/main/kotlin/ss/resource/ResourceUi.kt b/features/resource/src/main/kotlin/ss/resource/ResourceUi.kt index 7e3f47328..dfd08a0bf 100644 --- a/features/resource/src/main/kotlin/ss/resource/ResourceUi.kt +++ b/features/resource/src/main/kotlin/ss/resource/ResourceUi.kt @@ -140,6 +140,7 @@ fun ResourceUi(state: State, modifier: Modifier = Modifier) { readMoreClick = { state.eventSink(Event.OnReadMoreClick) }, + offlineState = state.offlineState, ) } ) diff --git a/features/resource/src/main/kotlin/ss/resource/components/ResourceCoverContent.kt b/features/resource/src/main/kotlin/ss/resource/components/ResourceCoverContent.kt index b15df0047..72bc1a933 100644 --- a/features/resource/src/main/kotlin/ss/resource/components/ResourceCoverContent.kt +++ b/features/resource/src/main/kotlin/ss/resource/components/ResourceCoverContent.kt @@ -22,8 +22,14 @@ package ss.resource.components +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -32,19 +38,32 @@ import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ElevatedButton +import androidx.compose.material3.FilledIconButton +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -52,7 +71,10 @@ import app.ss.design.compose.extensions.color.parse import app.ss.design.compose.theme.SsTheme import app.ss.design.compose.theme.color.SsColors import app.ss.design.compose.widget.button.SsButtonDefaults +import app.ss.design.compose.widget.icon.IconBox +import app.ss.design.compose.widget.icon.ResIcon import app.ss.design.compose.widget.text.ReadMoreText +import app.ss.models.OfflineState import io.adventech.blockkit.model.resource.Resource import io.adventech.blockkit.ui.MarkdownText import io.adventech.blockkit.ui.style.Styler @@ -65,6 +87,7 @@ internal fun ColumnScope.CoverContent( resource: Resource, documentTitle: String?, type: CoverContentType, + offlineState: OfflineState, ctaOnClick: () -> Unit, readMoreClick: () -> Unit ) { @@ -142,6 +165,8 @@ internal fun ColumnScope.CoverContent( text = resource.cta?.text, documentTitle = documentTitle, alignment = alignment, + offlineState = offlineState, + offlineStateClick = { } ) } } @@ -218,11 +243,23 @@ private fun ColumnScope.CtaButton( color: Color, text: String?, documentTitle: String?, - alignment: Alignment.Horizontal + alignment: Alignment.Horizontal, + offlineState: OfflineState, + offlineStateClick: () -> Unit ) { val buttonColors = SsButtonDefaults.colors( containerColor = color ) + val offlineStateIcon = remember(offlineState) { + when (offlineState) { + OfflineState.PARTIAL, + OfflineState.NONE -> ResIcon.Download + + OfflineState.IN_PROGRESS -> null + OfflineState.COMPLETE -> ResIcon.Downloaded + } + } + Row( modifier = Modifier .defaultMinSize(minWidth = 140.dp) @@ -244,6 +281,7 @@ private fun ColumnScope.CtaButton( ) { Text( text = text ?: stringResource(id = L10n.string.ss_lessons_read).uppercase(), + modifier = Modifier, style = SsTheme.typography.titleMedium.copy( fontWeight = FontWeight.Bold ) @@ -264,15 +302,113 @@ private fun ColumnScope.CtaButton( } } } + + Spacer( + Modifier + .size(width = 0.5.dp, height = 48.dp) + .padding(vertical = 4.dp) + .background(color), + ) + + val progressAlpha by animateFloatAsState( + if (offlineState == OfflineState.IN_PROGRESS) 1f else 0f, + label = "progress" + ) + + FilledIconButton( + onClick = offlineStateClick, + modifier = Modifier + .graphicsLayer { translationX = -12f }, + containerColor = color, + contentColor = downloadButtonIconColor + ) { + AnimatedContent( + targetState = offlineStateIcon, + label = "download-icon" + ) { targetIcon -> + + Box(modifier = Modifier, Alignment.Center) { + CircularProgressIndicator( + modifier = Modifier + .padding(4.dp) + .alpha(progressAlpha), + color = downloadButtonIconColor + ) + + targetIcon?.let { IconBox(icon = it) } + } + } + } + } } private val readButtonElevation = 4.dp private val readButtonCornerRadius = 20.dp -private val readButtonShape = RoundedCornerShape(readButtonCornerRadius) +private val readButtonShape = RoundedCornerShape(topStart = readButtonCornerRadius, bottomStart = readButtonCornerRadius) +private val downloadButtonIconColor = Color(0XFFFFFFFF) +private val downloadButtonShape = RoundedCornerShape(topEnd = readButtonCornerRadius, bottomEnd = readButtonCornerRadius) + +@Composable +private fun FilledIconButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + shape: Shape = downloadButtonShape, + containerColor: Color = Color.Unspecified, + contentColor: Color = Color.Unspecified, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + content: @Composable () -> Unit +) = Surface( + onClick = onClick, + modifier = modifier.semantics { role = Role.Button }, + enabled = enabled, + shape = shape, + color = containerColor, + contentColor = contentColor, + shadowElevation = readButtonElevation, + interactionSource = interactionSource +) { + Box( + modifier = Modifier.size(40.dp), + contentAlignment = Alignment.Center + ) { + content() + } +} enum class CoverContentType { PRIMARY, SECONDARY, SECONDARY_LARGE } + +@PreviewLightDark +@Composable +internal fun CtaButtonPreview() { + SsTheme { + Surface { + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { + CtaButton( + onClick = {}, + color = Color(0xFF6200EE), + text = "Read".uppercase(), + documentTitle = "Document Title", + alignment = Alignment.CenterHorizontally, + offlineState = OfflineState.NONE, + offlineStateClick = {} + ) + + CtaButton( + onClick = {}, + color = Color(0xFF6200EE), + text = "Read".uppercase(), + documentTitle = "Document Title", + alignment = Alignment.CenterHorizontally, + offlineState = OfflineState.COMPLETE, + offlineStateClick = {} + ) + } + } + } +} diff --git a/features/resource/src/test/kotlin/ss/resource/components/ResourceCtaTest.kt b/features/resource/src/test/kotlin/ss/resource/components/ResourceCtaTest.kt new file mode 100644 index 000000000..5ca66a68c --- /dev/null +++ b/features/resource/src/test/kotlin/ss/resource/components/ResourceCtaTest.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025. Adventech + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package ss.resource.components + +import app.ss.testing.roborazzi.BaseScreenshotTest +import app.ss.testing.roborazzi.TestLightDark +import org.junit.Test + +class ResourceCtaTest : BaseScreenshotTest() { + @Test + fun testSnapShot() { + snapshot(TestLightDark.LIGHT) { CtaButtonPreview() } + } + + @Test + fun testSnapShotDark() { + snapshot(TestLightDark.DARK) { CtaButtonPreview() } + } +} diff --git a/features/resource/src/test/snapshots/images/ss.resource.components.ResourceCtaTest.testSnapShot.png b/features/resource/src/test/snapshots/images/ss.resource.components.ResourceCtaTest.testSnapShot.png new file mode 100644 index 000000000..f7ac8b48c Binary files /dev/null and b/features/resource/src/test/snapshots/images/ss.resource.components.ResourceCtaTest.testSnapShot.png differ diff --git a/features/resource/src/test/snapshots/images/ss.resource.components.ResourceCtaTest.testSnapShotDark.png b/features/resource/src/test/snapshots/images/ss.resource.components.ResourceCtaTest.testSnapShotDark.png new file mode 100644 index 000000000..2e860967d Binary files /dev/null and b/features/resource/src/test/snapshots/images/ss.resource.components.ResourceCtaTest.testSnapShotDark.png differ diff --git a/libraries/lessons/api/src/main/kotlin/ss/lessons/api/ResourcesApi.kt b/libraries/lessons/api/src/main/kotlin/ss/lessons/api/ResourcesApi.kt index 23da843b2..587ad3a73 100644 --- a/libraries/lessons/api/src/main/kotlin/ss/lessons/api/ResourcesApi.kt +++ b/libraries/lessons/api/src/main/kotlin/ss/lessons/api/ResourcesApi.kt @@ -45,7 +45,10 @@ interface ResourcesApi { suspend fun languages(): Response> @GET("api/v3/{language}/{type}/index.json") - suspend fun feed(@Path("language", encoded = true) language: String, @Path("type", encoded = true) type: String): Response + suspend fun feed( + @Path("language", encoded = true) language: String, + @Path("type", encoded = true) type: String, + ): Response @GET("api/v3/{language}/{type}/feeds/{groupId}/index.json") suspend fun feedGroup( @@ -71,7 +74,7 @@ interface ResourcesApi { @Path("inputType", encoded = true) inputType: String, @Path("documentId", encoded = true) documentId: String, @Path("blockId", encoded = true) blockId: String, - @Body userInput: UserInputRequest + @Body userInput: UserInputRequest, ) @GET("api/v3/{index}/audio.json") diff --git a/libraries/pdf/api/src/main/kotlin/ss/libraries/pdf/api/PdfReader.kt b/libraries/pdf/api/src/main/kotlin/ss/libraries/pdf/api/PdfReader.kt index a76f99128..05e2e7a9c 100644 --- a/libraries/pdf/api/src/main/kotlin/ss/libraries/pdf/api/PdfReader.kt +++ b/libraries/pdf/api/src/main/kotlin/ss/libraries/pdf/api/PdfReader.kt @@ -23,8 +23,8 @@ package ss.libraries.pdf.api import android.content.Intent -import app.ss.models.LessonPdf import app.ss.models.PDFAux +import io.adventech.blockkit.model.resource.PdfAux import ss.libraries.circuit.navigation.PdfScreen /** API for handling pdf lessons. */ @@ -37,5 +37,5 @@ interface PdfReader { suspend fun downloadFiles(pdfs: List): Result> /** Returns true if this [pdf] file is downloaded. */ - fun isDownloaded(pdf: LessonPdf): Boolean + fun isDownloaded(pdf: PdfAux): Boolean } diff --git a/services/resources/api/src/main/kotlin/ss/resources/api/ResourcesRepository.kt b/services/resources/api/src/main/kotlin/ss/resources/api/ResourcesRepository.kt index 977eb2734..cf5f3dfd8 100644 --- a/services/resources/api/src/main/kotlin/ss/resources/api/ResourcesRepository.kt +++ b/services/resources/api/src/main/kotlin/ss/resources/api/ResourcesRepository.kt @@ -23,6 +23,7 @@ package ss.resources.api import app.ss.models.AudioAux +import app.ss.models.OfflineState import app.ss.models.PDFAux import app.ss.models.VideoAux import io.adventech.blockkit.model.feed.FeedGroup @@ -67,4 +68,6 @@ interface ResourcesRepository { fun bibleVersion(): Flow fun saveBibleVersion(version: String) + + fun resourceOfflineState(index: String): Flow } diff --git a/services/resources/api/src/main/kotlin/ss/resources/api/test/FakeResourcesRepository.kt b/services/resources/api/src/main/kotlin/ss/resources/api/test/FakeResourcesRepository.kt index 86e539421..a48c34ee4 100644 --- a/services/resources/api/src/main/kotlin/ss/resources/api/test/FakeResourcesRepository.kt +++ b/services/resources/api/src/main/kotlin/ss/resources/api/test/FakeResourcesRepository.kt @@ -24,6 +24,7 @@ package ss.resources.api.test import androidx.annotation.VisibleForTesting import app.ss.models.AudioAux +import app.ss.models.OfflineState import app.ss.models.PDFAux import app.ss.models.VideoAux import io.adventech.blockkit.model.feed.FeedGroup @@ -35,6 +36,7 @@ import io.adventech.blockkit.model.resource.ResourceDocument import io.adventech.blockkit.model.resource.Segment import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flow import ss.resources.api.ResourcesRepository import ss.resources.model.FeedModel import ss.resources.model.FontModel @@ -111,4 +113,8 @@ class FakeResourcesRepository( override fun saveBibleVersion(version: String) { TODO("Not yet implemented") } + + override fun resourceOfflineState(index: String): Flow { + return flow { emit(OfflineState.NONE) } + } } diff --git a/services/resources/impl/build.gradle.kts b/services/resources/impl/build.gradle.kts index f1d823877..4a57302ef 100644 --- a/services/resources/impl/build.gradle.kts +++ b/services/resources/impl/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { implementation(projects.common.network) implementation(projects.libraries.foundation.android) implementation(projects.libraries.lessons.api) + implementation(projects.libraries.pdf.api) implementation(projects.libraries.prefs.api) implementation(projects.libraries.storage.api) diff --git a/services/resources/impl/src/main/kotlin/ss/resources/impl/ResourcesRepositoryImpl.kt b/services/resources/impl/src/main/kotlin/ss/resources/impl/ResourcesRepositoryImpl.kt index cb7db38f4..862b11e4d 100644 --- a/services/resources/impl/src/main/kotlin/ss/resources/impl/ResourcesRepositoryImpl.kt +++ b/services/resources/impl/src/main/kotlin/ss/resources/impl/ResourcesRepositoryImpl.kt @@ -24,6 +24,7 @@ package ss.resources.impl import android.content.Context import app.ss.models.AudioAux +import app.ss.models.OfflineState import app.ss.models.PDFAux import app.ss.models.VideoAux import app.ss.network.NetworkResource @@ -36,19 +37,26 @@ import io.adventech.blockkit.model.input.UserInput import io.adventech.blockkit.model.input.UserInputRequest import io.adventech.blockkit.model.resource.Resource import io.adventech.blockkit.model.resource.ResourceDocument +import io.adventech.blockkit.model.resource.ResourceSection import io.adventech.blockkit.model.resource.Segment +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onEmpty import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.withContext import ss.foundation.android.connectivity.ConnectivityHelper import ss.foundation.coroutines.DispatcherProvider import ss.lessons.api.ResourcesApi +import ss.libraries.pdf.api.PdfReader import ss.libraries.storage.api.dao.AudioDao import ss.libraries.storage.api.dao.BibleVersionDao import ss.libraries.storage.api.dao.DocumentsDao @@ -92,10 +100,11 @@ internal class ResourcesRepositoryImpl @Inject constructor( private val dispatcherProvider: DispatcherProvider, private val connectivityHelper: ConnectivityHelper, private val ssPrefs: Lazy, + private val pdfReader: Lazy, ) : ResourcesRepository { override fun languages(query: String?): Flow> { - return return (if (query.isNullOrEmpty()) { + return (if (query.isNullOrEmpty()) { languagesDao.get().onStart { syncHelper.syncLanguages() } } else { languagesDao.search("%$query%") @@ -230,8 +239,8 @@ internal class ResourcesRepositoryImpl @Inject constructor( } is NetworkResource.Success -> { - resource.value.body()?.let { - Result.success(it.filter { it.target == documentIndex }) + resource.value.body()?.let { pdfs -> + Result.success(pdfs.filter { it.target == documentIndex }) } ?: Result.failure(Throwable("Failed to fetch PDFs, body is null")) } } @@ -259,4 +268,64 @@ internal class ResourcesRepositoryImpl @Inject constructor( override fun saveBibleVersion(version: String) = syncHelper.saveBibleVersion(ssPrefs.get().getLanguageCode(), version) + + @OptIn(ExperimentalCoroutinesApi::class) + override fun resourceOfflineState(index: String): Flow { + return resourcesDao.get(index) + .flatMapLatest { entity -> + val sections = entity?.sections ?: emptyList() + + if (sections.isEmpty()) { + flowOf(OfflineState.NONE) + } else { + combine(sections.map { sectionState(it) }) { states -> + when { + states.all { it == OfflineState.COMPLETE } -> OfflineState.COMPLETE + states.any { it == OfflineState.PARTIAL } -> OfflineState.PARTIAL + else -> OfflineState.NONE + } + } + } + } + .catch { Timber.e(it) } + .flowOn(dispatcherProvider.io) + + } + + private fun sectionState(section: ResourceSection): Flow { + val flows = section.documents.map { document -> + documentsDao.get(document.index) + .filterNotNull() + .map { docEntity -> + docEntity.segments?.map { segmentState(it) } + ?.let { segments -> + if (segments.all { it == OfflineState.COMPLETE }) { + OfflineState.COMPLETE + } else if (segments.any { it == OfflineState.PARTIAL }) { + OfflineState.PARTIAL + } else { + OfflineState.NONE + } + } ?: OfflineState.NONE + } + } + return combine(flows) { states -> + when { + states.all { it == OfflineState.COMPLETE } -> OfflineState.COMPLETE + states.any { it == OfflineState.COMPLETE } -> OfflineState.PARTIAL + else -> OfflineState.NONE + } + } + } + + private fun segmentState(segment: Segment): OfflineState { + return segment.pdf?.let { pdfs -> + // Check if all PDFs are downloaded + if (pdfs.all { pdfReader.get().isDownloaded(it) } ) { + OfflineState.COMPLETE + } else { + OfflineState.PARTIAL + } + } ?: OfflineState.COMPLETE + } }