From 6397bac83e1042bfc3bd1bb9f8c1e1098861e6d6 Mon Sep 17 00:00:00 2001 From: Tinashe Mzondiwa Date: Wed, 25 Feb 2026 13:43:40 +0200 Subject: [PATCH 01/23] Migrate pdfkit to nutrient # Conflicts: # gradle/libs.versions.toml --- features/pdf/build.gradle.kts | 6 +----- .../main/kotlin/app/ss/pdf/PdfReaderImpl.kt | 16 ++++++++-------- .../kotlin/app/ss/pdf/ui/PdfDocumentView.kt | 2 -- .../kotlin/app/ss/pdf/ui/ReadPdfViewModel.kt | 6 +++--- .../kotlin/app/ss/pdf/ui/SSReadPdfActivity.kt | 18 +++++++++--------- features/pdf/src/main/res/values/themes.xml | 1 - gradle/libs.versions.toml | 4 ++-- settings.gradle.kts | 2 +- 8 files changed, 24 insertions(+), 31 deletions(-) diff --git a/features/pdf/build.gradle.kts b/features/pdf/build.gradle.kts index 7520f7840..a1f2bf526 100644 --- a/features/pdf/build.gradle.kts +++ b/features/pdf/build.gradle.kts @@ -62,11 +62,7 @@ dependencies { implementation(libs.androidx.preference) implementation(libs.google.hilt.android) implementation(libs.google.material) - implementation(libs.pdfkit) { - // We don't need these transitive dependencies - exclude(group = "com.google.android.material", module = "material") - exclude(group = "androidx.compose.runtime", module = "runtime") // imported as aar and breaks the build - } + implementation(libs.nutrient) implementation(libs.timber) implementation(projects.common.core) implementation(projects.common.design) 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..a5de69aab 100644 --- a/features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderImpl.kt +++ b/features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderImpl.kt @@ -75,14 +75,14 @@ internal class PdfReaderImpl @Inject constructor( @Suppress("DEPRECATION") val config = PdfActivityConfiguration.Builder(context) - .hidePageLabels() - .hideDocumentTitleOverlay() - .disableDocumentInfoView() - .hidePageNumberOverlay() - .hideThumbnailGrid() - .disableSearch() - .disablePrinting() - .disableOutline() + // .hidePageLabels() + // .hideDocumentTitleOverlay() + // .disableDocumentInfoView() + // .hidePageNumberOverlay() + // .hideThumbnailGrid() + // .disableSearch() + // .disablePrinting() + // .disableOutline() .fitMode(PageFitMode.FIT_TO_WIDTH) .animateScrollOnEdgeTaps(true) .excludedAnnotationTypes(excludedAnnotationTypes) diff --git a/features/pdf/src/main/kotlin/app/ss/pdf/ui/PdfDocumentView.kt b/features/pdf/src/main/kotlin/app/ss/pdf/ui/PdfDocumentView.kt index e6ca358a5..e710cf87e 100644 --- a/features/pdf/src/main/kotlin/app/ss/pdf/ui/PdfDocumentView.kt +++ b/features/pdf/src/main/kotlin/app/ss/pdf/ui/PdfDocumentView.kt @@ -26,10 +26,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import app.ss.pdf.model.PdfDocumentSpec import com.pspdfkit.jetpack.compose.interactors.rememberDocumentState -import com.pspdfkit.jetpack.compose.utilities.ExperimentalPSPDFKitApi import com.pspdfkit.jetpack.compose.views.DocumentView -@OptIn(ExperimentalPSPDFKitApi::class) @Composable fun PdfDocumentView( spec: PdfDocumentSpec, diff --git a/features/pdf/src/main/kotlin/app/ss/pdf/ui/ReadPdfViewModel.kt b/features/pdf/src/main/kotlin/app/ss/pdf/ui/ReadPdfViewModel.kt index 09d9d08ad..734e60655 100644 --- a/features/pdf/src/main/kotlin/app/ss/pdf/ui/ReadPdfViewModel.kt +++ b/features/pdf/src/main/kotlin/app/ss/pdf/ui/ReadPdfViewModel.kt @@ -88,7 +88,7 @@ class ReadPdfViewModel @Inject constructor( }.toMap() } .catch { Timber.e(it) } - .stateIn(viewModelScope, emptyMap>()) + .stateIn(viewModelScope, emptyMap()) init { checkMediaAvailability() @@ -113,7 +113,7 @@ class ReadPdfViewModel @Inject constructor( _pdfFiles.update { files } } - fun saveAnnotations(document: PdfDocument, docIndex: Int) { + suspend fun saveAnnotations(document: PdfDocument, docIndex: Int) { val pdfs = savedStateHandle.screen?.pdfs ?: return val documentId = savedStateHandle.screen?.documentId ?: return val pdfId = pdfs.getOrNull(docIndex)?.id ?: return @@ -141,7 +141,7 @@ class ReadPdfViewModel @Inject constructor( private fun invalidInstantJson(json: String) = json != "null" } -fun PdfDocument.annotations(): List { +suspend fun PdfDocument.annotations(): List { val allAnnotations = mutableListOf() for (i in 0 until pageCount) { val annotations = annotationProvider.getAnnotations(i) diff --git a/features/pdf/src/main/kotlin/app/ss/pdf/ui/SSReadPdfActivity.kt b/features/pdf/src/main/kotlin/app/ss/pdf/ui/SSReadPdfActivity.kt index 8f231adfb..f2c8fc1c8 100644 --- a/features/pdf/src/main/kotlin/app/ss/pdf/ui/SSReadPdfActivity.kt +++ b/features/pdf/src/main/kotlin/app/ss/pdf/ui/SSReadPdfActivity.kt @@ -159,20 +159,20 @@ class SSReadPdfActivity : PdfActivity() { private fun loadAnnotations(document: PdfDocument, annotations: List) { if (annotations.isEmpty()) return - with(document.annotationProvider) { - document.annotations() - .forEach { removeAnnotationFromPage(it) } - - annotations - .flatMap { it.annotations } - .forEach { createAnnotationFromInstantJson(it) } - } +// with(document.annotationProvider) { +// document.annotations() +// .forEach { removeAnnotationFromPage(it) } +// +// annotations +// .flatMap { it.annotations } +// .forEach { createAnnotationFromInstantJson(it) } +// } } override fun onStop() { val documents = loadedDocuments.mapNotNull { it.document } documents.forEachIndexed { index, pdfDocument -> - viewModel.saveAnnotations(pdfDocument, index) + // viewModel.saveAnnotations(pdfDocument, index) } readerPrefs.saveConfiguration(configuration.configuration) super.onStop() diff --git a/features/pdf/src/main/res/values/themes.xml b/features/pdf/src/main/res/values/themes.xml index cb1cacde7..8dec82ddf 100644 --- a/features/pdf/src/main/res/values/themes.xml +++ b/features/pdf/src/main/res/values/themes.xml @@ -37,7 +37,6 @@ @style/Widget.SS.Pdf.OutlineStyle @style/Widget.SS.Pdf.ContextualToolbarStyle @style/Widget.SS.Pdf.SettingsModePickerItemStyle - @style/Widget.SS.Pdf.SettingsModeLineSeparatorStyle @style/Widget.SS.Pdf.TabBarStyle @style/Widget.SS.Pdf.TextSelectionStyle diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9f4fa57a3..e7ea1cbdc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -46,7 +46,7 @@ ksp = "2.3.6" markwon = "4.6.2" material3-adaptive-navigation-suite = "1.4.0" moshix = "0.35.0" -pdfkit = "2024.9.1" +nutrient = "11.0.0" robolectric = "4.16.1" saket-extendedspans = "1.4.0" saket-telephoto = "0.18.0" @@ -146,6 +146,7 @@ google-material = { module = "com.google.android.material:material", version.ref haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" } haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" } material3-adaptive-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite", version.ref = "material3-adaptive-navigation-suite" } +nutrient = { module = "io.nutrient:nutrient", version.ref = "nutrient" } saket-extendedspans = { module = "me.saket.extendedspans:extendedspans", version.ref = "saket-extendedspans" } saket-telephoto = { module = "me.saket.telephoto:zoomable", version.ref = "saket-telephoto" } saket-telephoto-flick = { module = "me.saket.telephoto:flick", version.ref = "saket-telephoto" } @@ -157,7 +158,6 @@ kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines kotlin-coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlin-coroutines" } kotlinx-collectionsImmutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinx-immutable" } markwon-core = { module = "io.noties.markwon:core", version.ref = "markwon" } -pdfkit = { module = "com.pspdfkit:pspdfkit", version.ref = "pdfkit" } snapper = { module = "dev.chrisbanes.snapper:snapper", version.ref = "snapper" } square-moshi-adapters = { module = "com.squareup.moshi:moshi-adapters", version.ref = "square-moshi" } square-moshi-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "square-moshi" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 95b66949a..d61eae87d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,7 +33,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() - maven("https://customers.pspdfkit.com/maven") + maven("https://my.nutrient.io/maven") } } From 151a7963454c7efc587e08b08c299bc3d4d3bd79 Mon Sep 17 00:00:00 2001 From: Tinashe Mzondiwa Date: Sun, 29 Mar 2026 17:25:15 +0200 Subject: [PATCH 02/23] Add new key --- release/ss_public_keys.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release/ss_public_keys.properties b/release/ss_public_keys.properties index 9c8e46ec2..01c249019 100644 --- a/release/ss_public_keys.properties +++ b/release/ss_public_keys.properties @@ -19,5 +19,5 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # -PSPDFKIT_LICENSE=NsLZ8g4nrh749EjVs99CQH_HBIeHSM60cyApThDSXT14C5wtN9AlwGSxyoP-mxqo1Gmj1xW2S3N3F-6-J0YqfI4MGA08GFyP-UF5gioDJ6CFtXN4Rixo05eqejVouSN8dFnrTZ0dlFvIeaG2ODIdGmGuAB0qn2pE0aFY20hHoGpOMy86_kt1KnPGw1tduF9I8Pt3Emb0BZgZjRXPKmGWUbMxlST0go4tnqvgeznu__Mc641I9S5V-F8TEm5y-O1lJar1j-Eg5cptCSNl7Sq3M87-_uv_W1eMPuvbPbTikeP0M9m5CDKOA3cn51MTeEUA7BGRAXYIKttX4MCIz2u0V43eM0WS-uZKTM8nbwSM101bhJhICttwd0ILTkwjazWJwdd4yntrJdJCGNDYVszEzMcuoFd2YBlQKp9b1tYxvjOti6ecHO2MhqSkF49DmQwlB6G2u6SQykulI9wLPfTALN6ItjABWUK3T4jOaAXWVXI19Oz3Bj-bQf7NTcDz-6gl_-dfnufWfOOeMxLC8Ki4Ame1zlpluoU37daWijaVsGgqy63CcTTdPNBiZyVicaUyMg9EiI5IPCnj-hhom1YLUi-v-UnHPIYyH3rXwEYlV4A= +PSPDFKIT_LICENSE=b0qUi6gbdNdpC3Z0-pteQOq6p7opt_2oFC6OXcLEMpze6zSJjUM-yaakE_ZENcy2IjT-IyvoSdPfFU6pAWGjErBvEvlwp0vL6kqEo6JlSDdSKVH7GiXQtPay-HPKsXwTE0ethyHHT65IAF5NzaK8qVyQ9LWuA2sjhh15ixzwjVCWkdJ3gU_FfYhWEEMZuOluvr5jhouaA_2etvk69dxX7yLOKpLxlHPDCX5Eb_vy8EBmubGK9bOS4HBmMcz72zub6-IZSuJsig0V1STOJVFDc6-b6Qm8TojhZS4_fOlPFwGZOsGs-jcgqfSY9-8_wmnSkxh7IktNsImQRr9eG589IeehxcwXGf6syMRFjtpY8roI7Jy8Wz_A_9kqrb6ZiUVE9QVBwKtujJMe90r1IYYuFm4mTOpugyX88pKZFTGNxbTmdUsg_5lqysvXANwzpnTCwc_R_MQGViOqLX-C7rULrB9154uWJWSMlXI4x9K6O6VyAGamg696S420ys-vh0dOOmZW9tbP_Y7WfHU9k-AVfCk7C7OxwuPdZhqeKwSsclXdiLszjc6k7jeNPiBtayP3NVnrrGo8u3GxowIfX5_JPfAcsTBJDJDXTL0jU_MkkfQ= WEB_CLIENT_ID=443920152945-rgptd4sa4vqv7qlpq070pq9pifv48k1j.apps.googleusercontent.com \ No newline at end of file From 3a9b9f0e36252067b1e36b48f5be3418eec2b55b Mon Sep 17 00:00:00 2001 From: Tinashe Mzondiwa Date: Sun, 29 Mar 2026 18:50:46 +0200 Subject: [PATCH 03/23] Implement pdf ui --- .../sabbathschool/ui/home/HomeActivity.kt | 4 +- common/design/src/main/res/values/colors.xml | 1 + features/document/build.gradle.kts | 2 + .../kotlin/ss/document/DocumentPresenter.kt | 8 +- .../kotlin/ss/document/DocumentScreenUi.kt | 3 +- .../document/segment/components/pdf/PdfUi.kt | 94 ++++++++++++++ .../components}/pdf/ReadPdfPresenter.kt | 24 +++- .../segment/components}/pdf/ReadPdfState.kt | 7 +- .../segment/components}/pdf/ReadPdfUi.kt | 34 +++-- .../src/main/res/color/ss_pdf_item_tint.xml | 27 ++++ .../src/main/res/color/ss_pdf_tab_color.xml | 27 ++++ .../src/main/res/color/ss_pdf_text_color.xml | 26 ++++ .../main/res/drawable/ic_pdf_annotations.xml | 10 ++ .../src/main/res/drawable/ic_pdf_bookmark.xml | 32 +++++ .../src/main/res/drawable/ic_pdf_settings.xml | 32 +++++ .../document/src/main/res/values/colors.xml | 30 +++++ .../document/src/main/res/values/themes.xml | 118 ++++++++++++++++++ .../content/ResourceSectionsStateProducer.kt | 17 +-- 18 files changed, 460 insertions(+), 36 deletions(-) create mode 100644 features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt rename features/{pdf/src/main/kotlin/app/ss => document/src/main/kotlin/ss/document/segment/components}/pdf/ReadPdfPresenter.kt (72%) rename features/{pdf/src/main/kotlin/app/ss => document/src/main/kotlin/ss/document/segment/components}/pdf/ReadPdfState.kt (84%) rename features/{pdf/src/main/kotlin/app/ss => document/src/main/kotlin/ss/document/segment/components}/pdf/ReadPdfUi.kt (65%) create mode 100644 features/document/src/main/res/color/ss_pdf_item_tint.xml create mode 100644 features/document/src/main/res/color/ss_pdf_tab_color.xml create mode 100644 features/document/src/main/res/color/ss_pdf_text_color.xml create mode 100644 features/document/src/main/res/drawable/ic_pdf_annotations.xml create mode 100644 features/document/src/main/res/drawable/ic_pdf_bookmark.xml create mode 100644 features/document/src/main/res/drawable/ic_pdf_settings.xml create mode 100644 features/document/src/main/res/values/colors.xml create mode 100644 features/document/src/main/res/values/themes.xml diff --git a/app/src/main/java/com/cryart/sabbathschool/ui/home/HomeActivity.kt b/app/src/main/java/com/cryart/sabbathschool/ui/home/HomeActivity.kt index 1a9dad747..8041f4f81 100644 --- a/app/src/main/java/com/cryart/sabbathschool/ui/home/HomeActivity.kt +++ b/app/src/main/java/com/cryart/sabbathschool/ui/home/HomeActivity.kt @@ -23,9 +23,9 @@ package com.cryart.sabbathschool.ui.home import android.os.Bundle -import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.appcompat.app.AppCompatActivity import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.remember @@ -47,7 +47,7 @@ import ss.services.circuit.impl.interceptor.AndroidSupportingInterceptor import javax.inject.Inject @AndroidEntryPoint -class HomeActivity : ComponentActivity() { +class HomeActivity : AppCompatActivity() { @Inject lateinit var circuit: Circuit diff --git a/common/design/src/main/res/values/colors.xml b/common/design/src/main/res/values/colors.xml index 277b5466a..35b1aae40 100644 --- a/common/design/src/main/res/values/colors.xml +++ b/common/design/src/main/res/values/colors.xml @@ -38,6 +38,7 @@ #FFFFFF #000000 #66000000 + #FDF4E6 #80D7D7D7 #EFEFEF diff --git a/features/document/build.gradle.kts b/features/document/build.gradle.kts index 5db54e568..e0ef30869 100644 --- a/features/document/build.gradle.kts +++ b/features/document/build.gradle.kts @@ -49,7 +49,9 @@ dependencies { implementation(libs.google.hilt.android) implementation(libs.joda.time) implementation(libs.kotlinx.collectionsImmutable) + implementation(libs.nutrient) implementation(libs.timber) + implementation(projects.common.design) implementation(projects.common.designCompose) implementation(projects.common.misc) implementation(projects.common.translations) diff --git a/features/document/src/main/kotlin/ss/document/DocumentPresenter.kt b/features/document/src/main/kotlin/ss/document/DocumentPresenter.kt index 2b88d7217..ed7c9b61a 100644 --- a/features/document/src/main/kotlin/ss/document/DocumentPresenter.kt +++ b/features/document/src/main/kotlin/ss/document/DocumentPresenter.kt @@ -93,7 +93,7 @@ class DocumentPresenter @AssistedInject constructor( private val readerStyleStateProducer: ReaderStyleStateProducer, private val segmentOverlayStateProducer: SegmentOverlayStateProducer, private val userInputStateProducer: UserInputStateProducer, - private val pdfReader: PdfReader, + // private val pdfReader: PdfReader, private val playbackConnection: PlaybackConnection, private val mediaNavigation: MediaNavigation, private val mediaPlayer: SSMediaPlayer, @@ -111,7 +111,7 @@ class DocumentPresenter @AssistedInject constructor( val resourceDocument = response - LaunchedEffect(resourceDocument) { checkPdfOnlySegment(resourceDocument) } + // LaunchedEffect(resourceDocument) { checkPdfOnlySegment(resourceDocument) } val actionsState = resourceDocument?.let { actionsProducer( @@ -317,8 +317,8 @@ class DocumentPresenter @AssistedInject constructor( }, ) Snapshot.withMutableSnapshot { - navigator.pop() - navigator.goTo(IntentScreen(pdfReader.launchIntent(screen))) + // navigator.pop() + // navigator.goTo(IntentScreen(pdfReader.launchIntent(screen))) } } } diff --git a/features/document/src/main/kotlin/ss/document/DocumentScreenUi.kt b/features/document/src/main/kotlin/ss/document/DocumentScreenUi.kt index d6f4bb127..5a4a4b8bf 100644 --- a/features/document/src/main/kotlin/ss/document/DocumentScreenUi.kt +++ b/features/document/src/main/kotlin/ss/document/DocumentScreenUi.kt @@ -291,11 +291,10 @@ private fun State.showTopBar(collapsed: Boolean): Boolean = when (this) { is State.Success -> when (selectedSegment?.type) { SegmentType.VIDEO -> false SegmentType.STORY -> collapsed - SegmentType.PDF, SegmentType.UNKNOWN, SegmentType.BLOCK, - null -> true + SegmentType.PDF -> false } } diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt new file mode 100644 index 000000000..cf1f84ebe --- /dev/null +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2026. 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.document.segment.components.pdf + +import android.content.Context +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import com.pspdfkit.configuration.activity.PdfActivityConfiguration +import com.pspdfkit.configuration.activity.UserInterfaceViewMode +import com.pspdfkit.configuration.theming.ThemeMode +import com.pspdfkit.jetpack.compose.interactors.getDefaultDocumentManager +import com.pspdfkit.jetpack.compose.interactors.rememberDocumentState +import com.pspdfkit.jetpack.compose.views.DocumentView +import io.adventech.blockkit.ui.style.LocalReaderStyle +import io.adventech.blockkit.ui.style.ReaderStyle +import ss.libraries.pdf.api.LocalFile +import ss.document.R as DocumentR + +@Composable +fun PdfUi(document: LocalFile, modifier: Modifier = Modifier) { + val context = LocalContext.current + val documentUri = document.uri + val readerStyle = LocalReaderStyle.current + // 1. Resolve the mapped theme ID for UI + val themeResId = readerStyle.theme.toPdfThemeResId() + + // 2. Resolve ThemeMode for PSPDFKit's built-in document inversion + val themeMode = when (readerStyle.theme) { + ReaderStyle.Theme.Dark -> ThemeMode.NIGHT + ReaderStyle.Theme.Auto -> if (isSystemInDarkTheme()) ThemeMode.NIGHT else ThemeMode.DEFAULT + else -> ThemeMode.DEFAULT + } + val pdfActivityConfiguration = rememberPdfConfiguration(context, document, themeResId, themeMode) + + val documentState = rememberDocumentState(documentUri, pdfActivityConfiguration) + + DocumentView( + documentState = documentState, + modifier = modifier, + documentManager = getDefaultDocumentManager() + ) +} + +@Composable +fun ReaderStyle.Theme.toPdfThemeResId(): Int { + return when (this) { + ReaderStyle.Theme.Light -> DocumentR.style.Theme_SS_Pdf_Light + ReaderStyle.Theme.Dark -> DocumentR.style.Theme_SS_Pdf_Dark + ReaderStyle.Theme.Sepia -> DocumentR.style.Theme_SS_Pdf_Sepia + ReaderStyle.Theme.Auto -> { + if (isSystemInDarkTheme()) DocumentR.style.Theme_SS_Pdf_Dark else DocumentR.style.Theme_SS_Pdf_Light + } + } +} + + +@Composable +private fun rememberPdfConfiguration( + context: Context, + file: LocalFile, + themeResId: Int, // Passed mapped theme based on LocalReaderStyle + themeMode: ThemeMode // Used to handle actual PDF color inversion (Night Mode) +) = remember { + PdfActivityConfiguration + .Builder(context) + .setUserInterfaceViewMode(UserInterfaceViewMode.USER_INTERFACE_VIEW_MODE_VISIBLE) + .title(file.title) + .themeMode(themeMode) // Maps to ThemeMode.DEFAULT or ThemeMode.NIGHT + .theme(themeResId) // Sets the custom Android UI theme for the UI and background + .build() +} diff --git a/features/pdf/src/main/kotlin/app/ss/pdf/ReadPdfPresenter.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt similarity index 72% rename from features/pdf/src/main/kotlin/app/ss/pdf/ReadPdfPresenter.kt rename to features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt index 7d7f3e998..7261a61f8 100644 --- a/features/pdf/src/main/kotlin/app/ss/pdf/ReadPdfPresenter.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025. Adventech + * Copyright (c) 2026. 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 @@ -20,10 +20,13 @@ * THE SOFTWARE. */ -package app.ss.pdf +package ss.document.segment.components.pdf import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue import com.slack.circuit.codegen.annotations.CircuitInject +import com.slack.circuit.retained.produceRetainedState import com.slack.circuit.runtime.Navigator import com.slack.circuit.runtime.presenter.Presenter import com.slack.circuitx.android.IntentScreen @@ -31,7 +34,11 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.components.SingletonComponent +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import ss.libraries.circuit.navigation.PdfScreen +import ss.libraries.pdf.api.LocalFile import ss.libraries.pdf.api.PdfReader class ReadPdfPresenter @AssistedInject constructor( @@ -42,7 +49,10 @@ class ReadPdfPresenter @AssistedInject constructor( @Composable override fun present(): ReadPdfState { + val documents by rememberFiles() + return ReadPdfState( + documents = documents, eventSink = { event -> when (event) { ReadPdfEvent.OpenPdf -> { @@ -52,6 +62,16 @@ class ReadPdfPresenter @AssistedInject constructor( }) } + @Composable + private fun rememberFiles(): State> = produceRetainedState>(persistentListOf()) { + val result = pdfReader.downloadFiles(screen.pdfs) + value = if (result.isSuccess) { + result.getOrDefault(emptyList()).toImmutableList() + } else { + persistentListOf() + } + } + @CircuitInject(PdfScreen::class, SingletonComponent::class) @AssistedFactory interface Factory { diff --git a/features/pdf/src/main/kotlin/app/ss/pdf/ReadPdfState.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt similarity index 84% rename from features/pdf/src/main/kotlin/app/ss/pdf/ReadPdfState.kt rename to features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt index 8d57d897b..1e696e5da 100644 --- a/features/pdf/src/main/kotlin/app/ss/pdf/ReadPdfState.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025. Adventech + * Copyright (c) 2026. 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 @@ -20,11 +20,14 @@ * THE SOFTWARE. */ -package app.ss.pdf +package ss.document.segment.components.pdf import com.slack.circuit.runtime.CircuitUiState +import kotlinx.collections.immutable.ImmutableList +import ss.libraries.pdf.api.LocalFile data class ReadPdfState( + val documents: ImmutableList, val eventSink: (ReadPdfEvent) -> Unit, ) : CircuitUiState diff --git a/features/pdf/src/main/kotlin/app/ss/pdf/ReadPdfUi.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt similarity index 65% rename from features/pdf/src/main/kotlin/app/ss/pdf/ReadPdfUi.kt rename to features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt index 8b7d3e616..beb36bcc1 100644 --- a/features/pdf/src/main/kotlin/app/ss/pdf/ReadPdfUi.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025. Adventech + * Copyright (c) 2026. 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 @@ -20,28 +20,38 @@ * THE SOFTWARE. */ -package app.ss.pdf +package ss.document.segment.components.pdf -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import app.ss.design.compose.widget.button.ButtonSpec -import app.ss.design.compose.widget.button.SsButton import com.slack.circuit.codegen.annotations.CircuitInject import dagger.hilt.components.SingletonComponent +import io.adventech.blockkit.ui.style.LocalReaderStyle +import io.adventech.blockkit.ui.style.background import ss.libraries.circuit.navigation.PdfScreen @CircuitInject(PdfScreen::class, SingletonComponent::class) @Composable fun ReadPdfUi(state: ReadPdfState, modifier: Modifier = Modifier) { - Box(modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - SsButton( - spec = ButtonSpec( - text = "Open PDF", - onClick = { state.eventSink(ReadPdfEvent.OpenPdf) } - ) - ) + val pagerState = rememberPagerState( + pageCount = { state.documents.size }, + ) + val readerStyle = LocalReaderStyle.current + + HorizontalPager( + state = pagerState, + modifier = modifier + .fillMaxSize() + .background(readerStyle.theme.background()), + verticalAlignment = Alignment.Top, + beyondViewportPageCount = 2, + ) { page -> + val document = state.documents[page] + PdfUi(document, Modifier) } } diff --git a/features/document/src/main/res/color/ss_pdf_item_tint.xml b/features/document/src/main/res/color/ss_pdf_item_tint.xml new file mode 100644 index 000000000..2ce77767c --- /dev/null +++ b/features/document/src/main/res/color/ss_pdf_item_tint.xml @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file diff --git a/features/document/src/main/res/color/ss_pdf_tab_color.xml b/features/document/src/main/res/color/ss_pdf_tab_color.xml new file mode 100644 index 000000000..96040f64d --- /dev/null +++ b/features/document/src/main/res/color/ss_pdf_tab_color.xml @@ -0,0 +1,27 @@ + + + + + + + \ No newline at end of file diff --git a/features/document/src/main/res/color/ss_pdf_text_color.xml b/features/document/src/main/res/color/ss_pdf_text_color.xml new file mode 100644 index 000000000..76ef35524 --- /dev/null +++ b/features/document/src/main/res/color/ss_pdf_text_color.xml @@ -0,0 +1,26 @@ + + + + + + \ No newline at end of file diff --git a/features/document/src/main/res/drawable/ic_pdf_annotations.xml b/features/document/src/main/res/drawable/ic_pdf_annotations.xml new file mode 100644 index 000000000..9fa1be03f --- /dev/null +++ b/features/document/src/main/res/drawable/ic_pdf_annotations.xml @@ -0,0 +1,10 @@ + + + diff --git a/features/document/src/main/res/drawable/ic_pdf_bookmark.xml b/features/document/src/main/res/drawable/ic_pdf_bookmark.xml new file mode 100644 index 000000000..9e95b201b --- /dev/null +++ b/features/document/src/main/res/drawable/ic_pdf_bookmark.xml @@ -0,0 +1,32 @@ + + + + + diff --git a/features/document/src/main/res/drawable/ic_pdf_settings.xml b/features/document/src/main/res/drawable/ic_pdf_settings.xml new file mode 100644 index 000000000..0efbe55ac --- /dev/null +++ b/features/document/src/main/res/drawable/ic_pdf_settings.xml @@ -0,0 +1,32 @@ + + + + + diff --git a/features/document/src/main/res/values/colors.xml b/features/document/src/main/res/values/colors.xml new file mode 100644 index 000000000..e595ae883 --- /dev/null +++ b/features/document/src/main/res/values/colors.xml @@ -0,0 +1,30 @@ + + + + @color/black + @color/pspdf__color_gray_light + + @color/ss_icon_tint + + @color/ss_theme_color_background + \ No newline at end of file diff --git a/features/document/src/main/res/values/themes.xml b/features/document/src/main/res/values/themes.xml new file mode 100644 index 000000000..61d101588 --- /dev/null +++ b/features/document/src/main/res/values/themes.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/features/resource/src/main/kotlin/ss/resource/components/content/ResourceSectionsStateProducer.kt b/features/resource/src/main/kotlin/ss/resource/components/content/ResourceSectionsStateProducer.kt index 34944a238..f900cd59d 100644 --- a/features/resource/src/main/kotlin/ss/resource/components/content/ResourceSectionsStateProducer.kt +++ b/features/resource/src/main/kotlin/ss/resource/components/content/ResourceSectionsStateProducer.kt @@ -31,7 +31,6 @@ import androidx.compose.ui.unit.dp import com.slack.circuit.retained.rememberRetained import com.slack.circuit.runtime.CircuitUiState import com.slack.circuit.runtime.Navigator -import com.slack.circuitx.android.IntentScreen import io.adventech.blockkit.model.feed.FeedResourceKind import io.adventech.blockkit.model.resource.Resource import io.adventech.blockkit.model.resource.ResourceDocument @@ -43,7 +42,6 @@ import org.joda.time.DateTime import org.joda.time.Interval import ss.libraries.circuit.navigation.CustomTabsIntentScreen import ss.libraries.circuit.navigation.DocumentScreen -import ss.libraries.pdf.api.PdfReader import ss.misc.DateHelper import javax.inject.Inject import kotlin.collections.lastIndex @@ -60,9 +58,7 @@ interface ResourceSectionsStateProducer { operator fun invoke(navigator: Navigator, resource: Resource): ResourceSectionsState } -internal class ResourceSectionsStateProducerImpl @Inject constructor( - private val pdfReader: PdfReader -) : ResourceSectionsStateProducer { +internal class ResourceSectionsStateProducerImpl @Inject constructor() : ResourceSectionsStateProducer { @Composable override fun invoke(navigator: Navigator, resource: Resource): ResourceSectionsState { @@ -72,15 +68,12 @@ internal class ResourceSectionsStateProducerImpl @Inject constructor( navigator.goTo(CustomTabsIntentScreen(it)) } else -> { - val screen = document.pdfScreen()?.let { - IntentScreen(pdfReader.launchIntent(it)) - } ?: DocumentScreen(document.index) - navigator.goTo(screen) + navigator.goTo(DocumentScreen(document.index)) } } } - val specs = buildList { + val specs = buildList { val content = when (resource.sectionView) { ResourceSectionViewType.NORMAL -> rememberNormalViewTypeSpecs(resource, onDocumentClick) ResourceSectionViewType.DROPDOWN -> rememberDropdownViewTypeSpecs(resource, onDocumentClick) @@ -99,7 +92,7 @@ internal class ResourceSectionsStateProducerImpl @Inject constructor( onDocumentClick: (ResourceDocument) -> Unit, ): List = rememberRetained(resource) { buildList { - resource.sections.orEmpty().forEachIndexed { index, section -> + resource.sections.orEmpty().forEach { section -> if (!section.isRoot) { add( HeaderResourceSection( @@ -171,6 +164,6 @@ internal class ResourceSectionsStateProducerImpl @Inject constructor( } } - return sections.orEmpty().first { it.isRoot == false } + return sections.orEmpty().first { !it.isRoot } } } From 3325300662458e6f82b2a149e5b9229bf271f377 Mon Sep 17 00:00:00 2001 From: Tinashe Mzondiwa Date: Sun, 29 Mar 2026 19:27:26 +0200 Subject: [PATCH 04/23] Show custom toolbar --- .../document/segment/components/pdf/PdfUi.kt | 101 +++++++++++++++--- .../components/pdf/ReadPdfPresenter.kt | 5 +- .../segment/components/pdf/ReadPdfState.kt | 2 +- .../segment/components/pdf/ReadPdfUi.kt | 10 +- 4 files changed, 92 insertions(+), 26 deletions(-) diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt index cf1f84ebe..69ebab649 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt @@ -23,11 +23,26 @@ package ss.document.segment.components.pdf import android.content.Context +import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import app.ss.design.compose.widget.scaffold.LocalNavbarController import com.pspdfkit.configuration.activity.PdfActivityConfiguration import com.pspdfkit.configuration.activity.UserInterfaceViewMode import com.pspdfkit.configuration.theming.ThemeMode @@ -36,18 +51,24 @@ import com.pspdfkit.jetpack.compose.interactors.rememberDocumentState import com.pspdfkit.jetpack.compose.views.DocumentView import io.adventech.blockkit.ui.style.LocalReaderStyle import io.adventech.blockkit.ui.style.ReaderStyle +import io.adventech.blockkit.ui.style.background +import io.adventech.blockkit.ui.style.primaryForeground import ss.libraries.pdf.api.LocalFile import ss.document.R as DocumentR +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun PdfUi(document: LocalFile, modifier: Modifier = Modifier) { +fun PdfUi( + document: LocalFile, + modifier: Modifier = Modifier, + eventSink: (ReadPdfEvent) -> Unit = {}, +) { val context = LocalContext.current val documentUri = document.uri val readerStyle = LocalReaderStyle.current - // 1. Resolve the mapped theme ID for UI val themeResId = readerStyle.theme.toPdfThemeResId() - // 2. Resolve ThemeMode for PSPDFKit's built-in document inversion + // Resolve ThemeMode for PSPDFKit's built-in document inversion val themeMode = when (readerStyle.theme) { ReaderStyle.Theme.Dark -> ThemeMode.NIGHT ReaderStyle.Theme.Auto -> if (isSystemInDarkTheme()) ThemeMode.NIGHT else ThemeMode.DEFAULT @@ -57,11 +78,63 @@ fun PdfUi(document: LocalFile, modifier: Modifier = Modifier) { val documentState = rememberDocumentState(documentUri, pdfActivityConfiguration) - DocumentView( - documentState = documentState, - modifier = modifier, - documentManager = getDefaultDocumentManager() - ) + val bottomPadding by animateDpAsState(if (LocalNavbarController.current.enabled) 80.dp else 0.dp) + + Column( + modifier = modifier.fillMaxSize(), + ) { + TopAppBar( + title = {}, + navigationIcon = { + IconButton(onClick = { eventSink(ReadPdfEvent.OnNavBack) }) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = "Back" + ) + } + }, + actions = { + IconButton(onClick = { + documentState.toggleView(com.pspdfkit.R.id.pspdf__menu_option_edit_annotations) + }) { + Icon( + painter = painterResource(DocumentR.drawable.ic_pdf_annotations), + contentDescription = "Annotations", + ) + } + IconButton(onClick = { + documentState.toggleView(com.pspdfkit.R.id.pspdf__menu_option_outline) + }) { + Icon( + painter = painterResource(DocumentR.drawable.ic_pdf_bookmark), + contentDescription = "Bookmarks", + ) + } + IconButton(onClick = { + documentState.toggleView(com.pspdfkit.R.id.pspdf__menu_option_settings) + }) { + Icon( + painter = painterResource(DocumentR.drawable.ic_pdf_settings), + contentDescription = "Settings", + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = readerStyle.theme.background(), + navigationIconContentColor = readerStyle.theme.primaryForeground(), + actionIconContentColor = readerStyle.theme.primaryForeground(), + titleContentColor = readerStyle.theme.primaryForeground(), + ) + ) + + DocumentView( + documentState = documentState, + modifier = Modifier + .weight(1f) + .padding(bottom = bottomPadding), + documentManager = getDefaultDocumentManager(), + ) + } } @Composable @@ -76,19 +149,19 @@ fun ReaderStyle.Theme.toPdfThemeResId(): Int { } } - @Composable private fun rememberPdfConfiguration( context: Context, file: LocalFile, - themeResId: Int, // Passed mapped theme based on LocalReaderStyle - themeMode: ThemeMode // Used to handle actual PDF color inversion (Night Mode) -) = remember { + themeResId: Int, + themeMode: ThemeMode, +) = remember(themeResId, themeMode) { PdfActivityConfiguration .Builder(context) .setUserInterfaceViewMode(UserInterfaceViewMode.USER_INTERFACE_VIEW_MODE_VISIBLE) + .defaultToolbarEnabled(false) .title(file.title) - .themeMode(themeMode) // Maps to ThemeMode.DEFAULT or ThemeMode.NIGHT - .theme(themeResId) // Sets the custom Android UI theme for the UI and background + .themeMode(themeMode) + .theme(themeResId) .build() } diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt index 7261a61f8..b572ce5bf 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt @@ -29,7 +29,6 @@ import com.slack.circuit.codegen.annotations.CircuitInject import com.slack.circuit.retained.produceRetainedState import com.slack.circuit.runtime.Navigator import com.slack.circuit.runtime.presenter.Presenter -import com.slack.circuitx.android.IntentScreen import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -55,8 +54,8 @@ class ReadPdfPresenter @AssistedInject constructor( documents = documents, eventSink = { event -> when (event) { - ReadPdfEvent.OpenPdf -> { - navigator.goTo(IntentScreen(pdfReader.launchIntent(screen))) + ReadPdfEvent.OnNavBack -> { + navigator.pop() } } }) diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt index 1e696e5da..790713737 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt @@ -32,6 +32,6 @@ data class ReadPdfState( ) : CircuitUiState sealed interface ReadPdfEvent { - data object OpenPdf : ReadPdfEvent + data object OnNavBack : ReadPdfEvent } diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt index beb36bcc1..9adcb1f97 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt @@ -22,7 +22,6 @@ package ss.document.segment.components.pdf -import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState @@ -31,8 +30,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.slack.circuit.codegen.annotations.CircuitInject import dagger.hilt.components.SingletonComponent -import io.adventech.blockkit.ui.style.LocalReaderStyle -import io.adventech.blockkit.ui.style.background import ss.libraries.circuit.navigation.PdfScreen @CircuitInject(PdfScreen::class, SingletonComponent::class) @@ -41,17 +38,14 @@ fun ReadPdfUi(state: ReadPdfState, modifier: Modifier = Modifier) { val pagerState = rememberPagerState( pageCount = { state.documents.size }, ) - val readerStyle = LocalReaderStyle.current HorizontalPager( state = pagerState, - modifier = modifier - .fillMaxSize() - .background(readerStyle.theme.background()), + modifier = modifier.fillMaxSize(), verticalAlignment = Alignment.Top, beyondViewportPageCount = 2, ) { page -> val document = state.documents[page] - PdfUi(document, Modifier) + PdfUi(document, Modifier, state.eventSink) } } From 4a79f66df67b622a43ddfa5ff0b7f2b0412fd597 Mon Sep 17 00:00:00 2001 From: Tinashe Mzondiwa Date: Sun, 29 Mar 2026 19:36:37 +0200 Subject: [PATCH 05/23] Go to Pdf screen --- .../document/producer/TopAppbarActionsProducer.kt | 2 +- .../content/ResourceSectionsStateProducer.kt | 4 ++-- .../resource/producer/ResourceCtaScreenProducer.kt | 14 +++++--------- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/features/document/src/main/kotlin/ss/document/producer/TopAppbarActionsProducer.kt b/features/document/src/main/kotlin/ss/document/producer/TopAppbarActionsProducer.kt index 98f060446..3a2323407 100644 --- a/features/document/src/main/kotlin/ss/document/producer/TopAppbarActionsProducer.kt +++ b/features/document/src/main/kotlin/ss/document/producer/TopAppbarActionsProducer.kt @@ -184,7 +184,7 @@ internal class TopAppbarActionsProducerImpl @Inject constructor( ) }, ) - navigator.goTo(IntentScreen(pdfReader.launchIntent(screen))) + navigator.goTo(screen) } DocumentTopAppBarAction.DisplayOptions -> { bottomSheetState = BottomSheet( diff --git a/features/resource/src/main/kotlin/ss/resource/components/content/ResourceSectionsStateProducer.kt b/features/resource/src/main/kotlin/ss/resource/components/content/ResourceSectionsStateProducer.kt index f900cd59d..3d6646b4d 100644 --- a/features/resource/src/main/kotlin/ss/resource/components/content/ResourceSectionsStateProducer.kt +++ b/features/resource/src/main/kotlin/ss/resource/components/content/ResourceSectionsStateProducer.kt @@ -44,7 +44,6 @@ import ss.libraries.circuit.navigation.CustomTabsIntentScreen import ss.libraries.circuit.navigation.DocumentScreen import ss.misc.DateHelper import javax.inject.Inject -import kotlin.collections.lastIndex data class ResourceSectionsState( val specs: ImmutableList, @@ -68,7 +67,8 @@ internal class ResourceSectionsStateProducerImpl @Inject constructor() : Resourc navigator.goTo(CustomTabsIntentScreen(it)) } else -> { - navigator.goTo(DocumentScreen(document.index)) + val screen = document.pdfScreen() ?: DocumentScreen(document.index) + navigator.goTo(screen) } } } diff --git a/features/resource/src/main/kotlin/ss/resource/producer/ResourceCtaScreenProducer.kt b/features/resource/src/main/kotlin/ss/resource/producer/ResourceCtaScreenProducer.kt index 3264a4f71..30c1ad65c 100644 --- a/features/resource/src/main/kotlin/ss/resource/producer/ResourceCtaScreenProducer.kt +++ b/features/resource/src/main/kotlin/ss/resource/producer/ResourceCtaScreenProducer.kt @@ -27,14 +27,12 @@ import androidx.compose.runtime.Stable import com.slack.circuit.retained.rememberRetained import com.slack.circuit.runtime.CircuitUiState import com.slack.circuit.runtime.screen.Screen -import com.slack.circuitx.android.IntentScreen import io.adventech.blockkit.model.feed.FeedType import io.adventech.blockkit.model.resource.Resource import org.joda.time.DateTime import org.joda.time.DateTimeConstants import org.joda.time.Interval import ss.libraries.circuit.navigation.DocumentScreen -import ss.libraries.pdf.api.PdfReader import ss.misc.DateHelper import ss.resource.components.content.pdfScreen import javax.inject.Inject @@ -55,9 +53,7 @@ interface ResourceCtaScreenProducer { operator fun invoke(resource: Resource?): CtaScreenState } -internal class ResourceCtaScreenProducerImpl @Inject constructor( - private val pdfReader: PdfReader -) : ResourceCtaScreenProducer { +internal class ResourceCtaScreenProducerImpl @Inject constructor() : ResourceCtaScreenProducer { @Composable override fun invoke(resource: Resource?): CtaScreenState { @@ -77,15 +73,15 @@ internal class ResourceCtaScreenProducerImpl @Inject constructor( } sections.forEach { section -> - section.documents.forEachIndexed { index, document -> - val startDate = document.startDate?.let { DateHelper.parseDate(it) } ?: return@forEachIndexed - val endDate = document.endDate?.let { DateHelper.parseDate(it) } ?: return@forEachIndexed + section.documents.forEach { document -> + val startDate = document.startDate?.let { DateHelper.parseDate(it) } ?: return@forEach + val endDate = document.endDate?.let { DateHelper.parseDate(it) } ?: return@forEach val fallsBetween = Interval(startDate, endDate.plusDays(1)).contains(dateTime) if (fallsBetween) { document.pdfScreen()?.let { - return CtaScreenState.Default(IntentScreen(pdfReader.launchIntent(it)), null) + return CtaScreenState.Default(it, null) } return CtaScreenState.Default(DocumentScreen(document.index), document.title) From 281f31aa1dc7fa2055b289d798d5a675d9aec22a Mon Sep 17 00:00:00 2001 From: Tinashe Mzondiwa Date: Sun, 29 Mar 2026 20:53:07 +0200 Subject: [PATCH 06/23] Show document title --- .../document/segment/components/pdf/PdfUi.kt | 4 +- .../segment/components/pdf/ReadPdfUi.kt | 80 ++++++++++++++++++- 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt index 69ebab649..a302fa7ef 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt @@ -33,6 +33,7 @@ import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable @@ -61,6 +62,7 @@ import ss.document.R as DocumentR fun PdfUi( document: LocalFile, modifier: Modifier = Modifier, + title: @Composable () -> Unit = { Text(document.title) }, eventSink: (ReadPdfEvent) -> Unit = {}, ) { val context = LocalContext.current @@ -84,7 +86,7 @@ fun PdfUi( modifier = modifier.fillMaxSize(), ) { TopAppBar( - title = {}, + title = title, navigationIcon = { IconButton(onClick = { eventSink(ReadPdfEvent.OnNavBack) }) { Icon( diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt index 9adcb1f97..df69ad8c1 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt @@ -22,14 +22,36 @@ package ss.document.segment.components.pdf +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import app.ss.design.compose.extensions.haptics.LocalSsHapticFeedback +import app.ss.design.compose.widget.icon.IconBox +import app.ss.design.compose.widget.icon.Icons import com.slack.circuit.codegen.annotations.CircuitInject import dagger.hilt.components.SingletonComponent +import io.adventech.blockkit.ui.style.LocalReaderStyle +import io.adventech.blockkit.ui.style.primaryForeground +import kotlinx.coroutines.launch import ss.libraries.circuit.navigation.PdfScreen @CircuitInject(PdfScreen::class, SingletonComponent::class) @@ -38,6 +60,9 @@ fun ReadPdfUi(state: ReadPdfState, modifier: Modifier = Modifier) { val pagerState = rememberPagerState( pageCount = { state.documents.size }, ) + val readerStyle = LocalReaderStyle.current + val coroutineScope = rememberCoroutineScope() + val hapticFeedback = LocalSsHapticFeedback.current HorizontalPager( state = pagerState, @@ -46,6 +71,59 @@ fun ReadPdfUi(state: ReadPdfState, modifier: Modifier = Modifier) { beyondViewportPageCount = 2, ) { page -> val document = state.documents[page] - PdfUi(document, Modifier, state.eventSink) + var expanded by remember { mutableStateOf(false) } + + PdfUi( + document = document, + modifier = Modifier, + title = { + val hasMultipleDocs = state.documents.size > 1 + Row( + modifier = if (hasMultipleDocs) { + Modifier + .sizeIn(minHeight = 48.dp) + .padding(horizontal = 6.dp, vertical = 2.dp) + .clip(RoundedCornerShape(12.dp)) + .clickable { + expanded = true + hapticFeedback.performClick() + } + .padding(horizontal = 12.dp, vertical = 12.dp) + } else { + Modifier + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text(document.title) + + if (hasMultipleDocs) { + + IconBox( + icon = Icons.ArrowDropDown, + contentColor = readerStyle.theme.primaryForeground(), + ) + + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + state.documents.forEachIndexed { index, doc -> + DropdownMenuItem( + text = { Text(doc.title) }, + onClick = { + expanded = false + coroutineScope.launch { + pagerState.animateScrollToPage(index) + } + } + ) + } + } + } + } + }, + eventSink = state.eventSink, + ) } } From c38baf0234f03553260fc51abe43e6749d9cf819 Mon Sep 17 00:00:00 2001 From: Tinashe Mzondiwa Date: Wed, 1 Apr 2026 14:15:47 +0200 Subject: [PATCH 07/23] Attempt to override colors --- .../res/color/ss_pdf_item_tint_custom.xml | 8 ++ .../main/res/color/ss_pdf_item_tint_dark.xml | 8 ++ .../main/res/color/ss_pdf_item_tint_light.xml | 8 ++ .../main/res/color/ss_pdf_item_tint_sepia.xml | 8 ++ .../src/main/res/values-night/colors.xml | 5 +- .../document/src/main/res/values/colors.xml | 15 +++ .../document/src/main/res/values/themes.xml | 127 ++++++++++++++---- features/pdf/src/main/AndroidManifest.xml | 1 - .../kotlin/app/ss/pdf/ui/SSReadPdfActivity.kt | 2 +- .../src/main/res/color/ss_pdf_item_tint.xml | 27 ---- .../src/main/res/color/ss_pdf_tab_color.xml | 27 ---- .../src/main/res/color/ss_pdf_text_color.xml | 26 ---- features/pdf/src/main/res/values/colors.xml | 30 ----- features/pdf/src/main/res/values/themes.xml | 109 --------------- 14 files changed, 150 insertions(+), 251 deletions(-) create mode 100644 features/document/src/main/res/color/ss_pdf_item_tint_custom.xml create mode 100644 features/document/src/main/res/color/ss_pdf_item_tint_dark.xml create mode 100644 features/document/src/main/res/color/ss_pdf_item_tint_light.xml create mode 100644 features/document/src/main/res/color/ss_pdf_item_tint_sepia.xml rename features/{pdf => document}/src/main/res/values-night/colors.xml (83%) delete mode 100644 features/pdf/src/main/res/color/ss_pdf_item_tint.xml delete mode 100644 features/pdf/src/main/res/color/ss_pdf_tab_color.xml delete mode 100644 features/pdf/src/main/res/color/ss_pdf_text_color.xml delete mode 100644 features/pdf/src/main/res/values/colors.xml delete mode 100644 features/pdf/src/main/res/values/themes.xml diff --git a/features/document/src/main/res/color/ss_pdf_item_tint_custom.xml b/features/document/src/main/res/color/ss_pdf_item_tint_custom.xml new file mode 100644 index 000000000..fc8d080f0 --- /dev/null +++ b/features/document/src/main/res/color/ss_pdf_item_tint_custom.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/features/document/src/main/res/color/ss_pdf_item_tint_dark.xml b/features/document/src/main/res/color/ss_pdf_item_tint_dark.xml new file mode 100644 index 000000000..3831dd171 --- /dev/null +++ b/features/document/src/main/res/color/ss_pdf_item_tint_dark.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/features/document/src/main/res/color/ss_pdf_item_tint_light.xml b/features/document/src/main/res/color/ss_pdf_item_tint_light.xml new file mode 100644 index 000000000..695222812 --- /dev/null +++ b/features/document/src/main/res/color/ss_pdf_item_tint_light.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/features/document/src/main/res/color/ss_pdf_item_tint_sepia.xml b/features/document/src/main/res/color/ss_pdf_item_tint_sepia.xml new file mode 100644 index 000000000..6edf3933a --- /dev/null +++ b/features/document/src/main/res/color/ss_pdf_item_tint_sepia.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/features/pdf/src/main/res/values-night/colors.xml b/features/document/src/main/res/values-night/colors.xml similarity index 83% rename from features/pdf/src/main/res/values-night/colors.xml rename to features/document/src/main/res/values-night/colors.xml index 3b66fa058..65b7b6d7d 100644 --- a/features/pdf/src/main/res/values-night/colors.xml +++ b/features/document/src/main/res/values-night/colors.xml @@ -1,5 +1,5 @@ @color/ss_theme_color_background + + + #FFFFFF + #F9FAFB + #001328 + + #000000 + #1f262e + #F9FAFB + + #FDF4E6 + #E8DAC4 + #3E3634 + + @color/ss_pdf_item_tint \ No newline at end of file diff --git a/features/document/src/main/res/values/themes.xml b/features/document/src/main/res/values/themes.xml index 61d101588..463367363 100644 --- a/features/document/src/main/res/values/themes.xml +++ b/features/document/src/main/res/values/themes.xml @@ -21,7 +21,7 @@ --> - + + + + + + + - + - + - - - \ No newline at end of file diff --git a/features/pdf/src/main/AndroidManifest.xml b/features/pdf/src/main/AndroidManifest.xml index 63768c818..07f2758de 100644 --- a/features/pdf/src/main/AndroidManifest.xml +++ b/features/pdf/src/main/AndroidManifest.xml @@ -25,7 +25,6 @@ - - - - - - \ No newline at end of file diff --git a/features/pdf/src/main/res/color/ss_pdf_tab_color.xml b/features/pdf/src/main/res/color/ss_pdf_tab_color.xml deleted file mode 100644 index 96040f64d..000000000 --- a/features/pdf/src/main/res/color/ss_pdf_tab_color.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/features/pdf/src/main/res/color/ss_pdf_text_color.xml b/features/pdf/src/main/res/color/ss_pdf_text_color.xml deleted file mode 100644 index 76ef35524..000000000 --- a/features/pdf/src/main/res/color/ss_pdf_text_color.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/features/pdf/src/main/res/values/colors.xml b/features/pdf/src/main/res/values/colors.xml deleted file mode 100644 index e595ae883..000000000 --- a/features/pdf/src/main/res/values/colors.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - @color/black - @color/pspdf__color_gray_light - - @color/ss_icon_tint - - @color/ss_theme_color_background - \ No newline at end of file diff --git a/features/pdf/src/main/res/values/themes.xml b/features/pdf/src/main/res/values/themes.xml deleted file mode 100644 index 8dec82ddf..000000000 --- a/features/pdf/src/main/res/values/themes.xml +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From 8cd09b4f59ebab6b5565d567be818ac386b97fa4 Mon Sep 17 00:00:00 2001 From: Tinashe Mzondiwa Date: Mon, 6 Apr 2026 06:52:51 -0400 Subject: [PATCH 08/23] Configure PDF topbar menu --- .../src/main/res/values/strings.xml | 1 + .../document/segment/components/pdf/PdfUi.kt | 110 +++++++++++------- 2 files changed, 66 insertions(+), 45 deletions(-) diff --git a/common/translations/src/main/res/values/strings.xml b/common/translations/src/main/res/values/strings.xml index 99492f295..e003ff45e 100644 --- a/common/translations/src/main/res/values/strings.xml +++ b/common/translations/src/main/res/values/strings.xml @@ -151,4 +151,5 @@ Share Downloading… Off + Annotations \ No newline at end of file diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt index a302fa7ef..0562deece 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt @@ -42,22 +42,26 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.ss.design.compose.widget.scaffold.LocalNavbarController import com.pspdfkit.configuration.activity.PdfActivityConfiguration import com.pspdfkit.configuration.activity.UserInterfaceViewMode import com.pspdfkit.configuration.theming.ThemeMode +import com.pspdfkit.jetpack.compose.interactors.DocumentState import com.pspdfkit.jetpack.compose.interactors.getDefaultDocumentManager import com.pspdfkit.jetpack.compose.interactors.rememberDocumentState import com.pspdfkit.jetpack.compose.views.DocumentView import io.adventech.blockkit.ui.style.LocalReaderStyle import io.adventech.blockkit.ui.style.ReaderStyle +import io.adventech.blockkit.ui.style.ReaderStyleConfig import io.adventech.blockkit.ui.style.background import io.adventech.blockkit.ui.style.primaryForeground import ss.libraries.pdf.api.LocalFile +import app.ss.translations.R as L10nR +import com.pspdfkit.R as PspdfR import ss.document.R as DocumentR -@OptIn(ExperimentalMaterial3Api::class) @Composable fun PdfUi( document: LocalFile, @@ -82,51 +86,12 @@ fun PdfUi( val bottomPadding by animateDpAsState(if (LocalNavbarController.current.enabled) 80.dp else 0.dp) - Column( - modifier = modifier.fillMaxSize(), - ) { - TopAppBar( + Column(modifier = modifier.fillMaxSize()) { + PdfTopAppBar( title = title, - navigationIcon = { - IconButton(onClick = { eventSink(ReadPdfEvent.OnNavBack) }) { - Icon( - imageVector = Icons.AutoMirrored.Rounded.ArrowBack, - contentDescription = "Back" - ) - } - }, - actions = { - IconButton(onClick = { - documentState.toggleView(com.pspdfkit.R.id.pspdf__menu_option_edit_annotations) - }) { - Icon( - painter = painterResource(DocumentR.drawable.ic_pdf_annotations), - contentDescription = "Annotations", - ) - } - IconButton(onClick = { - documentState.toggleView(com.pspdfkit.R.id.pspdf__menu_option_outline) - }) { - Icon( - painter = painterResource(DocumentR.drawable.ic_pdf_bookmark), - contentDescription = "Bookmarks", - ) - } - IconButton(onClick = { - documentState.toggleView(com.pspdfkit.R.id.pspdf__menu_option_settings) - }) { - Icon( - painter = painterResource(DocumentR.drawable.ic_pdf_settings), - contentDescription = "Settings", - ) - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = readerStyle.theme.background(), - navigationIconContentColor = readerStyle.theme.primaryForeground(), - actionIconContentColor = readerStyle.theme.primaryForeground(), - titleContentColor = readerStyle.theme.primaryForeground(), - ) + documentState = documentState, + readerStyle = readerStyle, + eventSink = eventSink, ) DocumentView( @@ -139,6 +104,61 @@ fun PdfUi( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun PdfTopAppBar( + title: @Composable () -> Unit, + documentState: DocumentState, + readerStyle: ReaderStyleConfig, + eventSink: (ReadPdfEvent) -> Unit, + modifier: Modifier = Modifier, +) { + TopAppBar( + title = title, + modifier = modifier, + navigationIcon = { + IconButton(onClick = { eventSink(ReadPdfEvent.OnNavBack) }) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, + contentDescription = stringResource(L10nR.string.ss_action_back), + ) + } + }, + actions = { + IconButton(onClick = { + documentState.toggleView(PspdfR.id.pspdf__menu_option_edit_annotations) + }) { + Icon( + painter = painterResource(DocumentR.drawable.ic_pdf_annotations), + contentDescription = stringResource(L10nR.string.ss_annotations), + ) + } + IconButton(onClick = { + documentState.toggleView(PspdfR.id.pspdf__menu_option_outline) + }) { + Icon( + painter = painterResource(DocumentR.drawable.ic_pdf_bookmark), + contentDescription = stringResource(PspdfR.string.pspdf__activity_menu_outline), + ) + } + IconButton(onClick = { + documentState.toggleView(PspdfR.id.pspdf__menu_option_settings) + }) { + Icon( + painter = painterResource(DocumentR.drawable.ic_pdf_settings), + contentDescription = stringResource(PspdfR.string.pspdf__activity_menu_settings), + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = readerStyle.theme.background(), + navigationIconContentColor = readerStyle.theme.primaryForeground(), + actionIconContentColor = readerStyle.theme.primaryForeground(), + titleContentColor = readerStyle.theme.primaryForeground(), + ) + ) +} + @Composable fun ReaderStyle.Theme.toPdfThemeResId(): Int { return when (this) { From a62fd48391a5bfeab3624b397a28a0731b21cf62 Mon Sep 17 00:00:00 2001 From: Tinashe Mzondiwa Date: Mon, 6 Apr 2026 08:28:12 -0400 Subject: [PATCH 09/23] Clean up --- .../document/src/main/kotlin/ss/document/DocumentPresenter.kt | 1 - .../kotlin/ss/document/producer/TopAppbarActionsProducer.kt | 3 --- 2 files changed, 4 deletions(-) diff --git a/features/document/src/main/kotlin/ss/document/DocumentPresenter.kt b/features/document/src/main/kotlin/ss/document/DocumentPresenter.kt index ed7c9b61a..7803d6ad0 100644 --- a/features/document/src/main/kotlin/ss/document/DocumentPresenter.kt +++ b/features/document/src/main/kotlin/ss/document/DocumentPresenter.kt @@ -93,7 +93,6 @@ class DocumentPresenter @AssistedInject constructor( private val readerStyleStateProducer: ReaderStyleStateProducer, private val segmentOverlayStateProducer: SegmentOverlayStateProducer, private val userInputStateProducer: UserInputStateProducer, - // private val pdfReader: PdfReader, private val playbackConnection: PlaybackConnection, private val mediaNavigation: MediaNavigation, private val mediaPlayer: SSMediaPlayer, diff --git a/features/document/src/main/kotlin/ss/document/producer/TopAppbarActionsProducer.kt b/features/document/src/main/kotlin/ss/document/producer/TopAppbarActionsProducer.kt index 3a2323407..29c730bc5 100644 --- a/features/document/src/main/kotlin/ss/document/producer/TopAppbarActionsProducer.kt +++ b/features/document/src/main/kotlin/ss/document/producer/TopAppbarActionsProducer.kt @@ -34,7 +34,6 @@ import com.slack.circuit.retained.produceRetainedState import com.slack.circuit.retained.rememberRetained import com.slack.circuit.runtime.CircuitUiState import com.slack.circuit.runtime.Navigator -import com.slack.circuitx.android.IntentScreen import dagger.Lazy import io.adventech.blockkit.model.resource.Segment import io.adventech.blockkit.model.resource.SegmentType @@ -52,7 +51,6 @@ import ss.libraries.circuit.navigation.AudioPlayerScreen import ss.libraries.circuit.navigation.PdfScreen import ss.libraries.circuit.navigation.ShareOptionsScreen import ss.libraries.circuit.navigation.VideosScreen -import ss.libraries.pdf.api.PdfReader import ss.resources.api.ResourcesRepository import javax.inject.Inject @@ -92,7 +90,6 @@ interface TopAppbarActionsProducer { internal class TopAppbarActionsProducerImpl @Inject constructor( private val repository: ResourcesRepository, - private val pdfReader: PdfReader, private val shareIntentHelper: Lazy, ) : TopAppbarActionsProducer { From 7508e26a577a9e1a593ba0aece72a435ea6c4c93 Mon Sep 17 00:00:00 2001 From: Tinashe Mzondiwa Date: Mon, 6 Apr 2026 08:29:10 -0400 Subject: [PATCH 10/23] Show media icon buttons --- .../document/segment/components/pdf/PdfUi.kt | 45 ++++++++-- .../components/pdf/ReadPdfPresenter.kt | 83 ++++++++++++++++++- .../segment/components/pdf/ReadPdfState.kt | 33 ++++++++ .../segment/components/pdf/ReadPdfUi.kt | 38 ++++++++- 4 files changed, 189 insertions(+), 10 deletions(-) diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt index 0562deece..7ca7987ba 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt @@ -45,18 +45,18 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import app.ss.design.compose.widget.scaffold.LocalNavbarController +import app.ss.models.media.MediaAvailability import com.pspdfkit.configuration.activity.PdfActivityConfiguration import com.pspdfkit.configuration.activity.UserInterfaceViewMode import com.pspdfkit.configuration.theming.ThemeMode -import com.pspdfkit.jetpack.compose.interactors.DocumentState import com.pspdfkit.jetpack.compose.interactors.getDefaultDocumentManager import com.pspdfkit.jetpack.compose.interactors.rememberDocumentState import com.pspdfkit.jetpack.compose.views.DocumentView import io.adventech.blockkit.ui.style.LocalReaderStyle import io.adventech.blockkit.ui.style.ReaderStyle -import io.adventech.blockkit.ui.style.ReaderStyleConfig import io.adventech.blockkit.ui.style.background import io.adventech.blockkit.ui.style.primaryForeground +import ss.document.components.DocumentTopAppBarAction import ss.libraries.pdf.api.LocalFile import app.ss.translations.R as L10nR import com.pspdfkit.R as PspdfR @@ -65,6 +65,7 @@ import ss.document.R as DocumentR @Composable fun PdfUi( document: LocalFile, + mediaAvailability: MediaAvailability, modifier: Modifier = Modifier, title: @Composable () -> Unit = { Text(document.title) }, eventSink: (ReadPdfEvent) -> Unit = {}, @@ -89,8 +90,11 @@ fun PdfUi( Column(modifier = modifier.fillMaxSize()) { PdfTopAppBar( title = title, - documentState = documentState, - readerStyle = readerStyle, + state = PdfTopAppBarState( + mediaAvailability = mediaAvailability, + documentState = documentState, + readerStyle = readerStyle, + ), eventSink = eventSink, ) @@ -108,11 +112,12 @@ fun PdfUi( @Composable private fun PdfTopAppBar( title: @Composable () -> Unit, - documentState: DocumentState, - readerStyle: ReaderStyleConfig, + state: PdfTopAppBarState, eventSink: (ReadPdfEvent) -> Unit, modifier: Modifier = Modifier, ) { + val (mediaAvailability, documentState, readerStyle) = state + TopAppBar( title = title, modifier = modifier, @@ -125,6 +130,20 @@ private fun PdfTopAppBar( } }, actions = { + if (mediaAvailability.audio) { + val action = DocumentTopAppBarAction.Audio + MediaIcon( + action = action, + onClick = { eventSink(ReadPdfEvent.OnTopAppBarAction(action)) }, + ) + } + if (mediaAvailability.video) { + val action = DocumentTopAppBarAction.Video + MediaIcon( + action = action, + onClick = { eventSink(ReadPdfEvent.OnTopAppBarAction(action)) }, + ) + } IconButton(onClick = { documentState.toggleView(PspdfR.id.pspdf__menu_option_edit_annotations) }) { @@ -187,3 +206,17 @@ private fun rememberPdfConfiguration( .theme(themeResId) .build() } + +@Composable +private fun MediaIcon( + action: DocumentTopAppBarAction, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + IconButton(onClick = onClick, modifier = modifier) { + Icon( + painter = painterResource(action.iconRes), + contentDescription = stringResource(action.title), + ) + } +} diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt index b572ce5bf..fa4d9f76a 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt @@ -25,8 +25,14 @@ package ss.document.segment.components.pdf import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import app.ss.models.media.MediaAvailability import com.slack.circuit.codegen.annotations.CircuitInject +import com.slack.circuit.foundation.NavEvent +import com.slack.circuit.foundation.onNavEvent import com.slack.circuit.retained.produceRetainedState +import com.slack.circuit.retained.rememberRetained import com.slack.circuit.runtime.Navigator import com.slack.circuit.runtime.presenter.Presenter import dagger.assisted.Assisted @@ -36,26 +42,79 @@ import dagger.hilt.components.SingletonComponent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import ss.document.components.DocumentTopAppBarAction +import ss.libraries.circuit.navigation.AudioPlayerScreen +import ss.libraries.circuit.navigation.ExpandedAudioPlayerScreen import ss.libraries.circuit.navigation.PdfScreen +import ss.libraries.circuit.navigation.VideosScreen import ss.libraries.pdf.api.LocalFile import ss.libraries.pdf.api.PdfReader +import ss.resources.api.ResourcesRepository class ReadPdfPresenter @AssistedInject constructor( @Assisted private val navigator: Navigator, @Assisted private val screen: PdfScreen, - private val pdfReader: PdfReader + private val pdfReader: PdfReader, + private val resourcesRepository: ResourcesRepository, ) : Presenter { @Composable override fun present(): ReadPdfState { val documents by rememberFiles() + val mediaAvailability by rememberMediaAvailability() + var overlayState by rememberRetained { mutableStateOf(ReadPdfOverlayState.None) } + + fun showAudioScreen() { + overlayState = ReadPdfOverlayState.BottomSheet( + screen = AudioPlayerScreen(resourceId = screen.resourceId, segmentId = screen.segmentId), + skipPartiallyExpanded = true, + onResult = { _ -> + overlayState = ReadPdfOverlayState.None + } + ) + } + + fun showVideoScreen() { + overlayState = ReadPdfOverlayState.BottomSheet( + screen = VideosScreen(documentIndex = screen.documentIndex, documentId = screen.documentId), + skipPartiallyExpanded = true, + onResult = { _ -> + overlayState = ReadPdfOverlayState.None + } + ) + } return ReadPdfState( documents = documents, + mediaAvailability = mediaAvailability, + overlayState = overlayState, eventSink = { event -> when (event) { - ReadPdfEvent.OnNavBack -> { - navigator.pop() + ReadPdfEvent.OnNavBack -> navigator.pop() + is ReadPdfEvent.OnNavEvent -> { + when (val navEvent = event.event) { + is NavEvent.GoTo -> { + if (navEvent.screen is ExpandedAudioPlayerScreen) { + showAudioScreen() + } else { + navigator.goTo(navEvent.screen) + } + } + + else -> navigator.onNavEvent(navEvent) + } + } + + is ReadPdfEvent.OnTopAppBarAction -> { + when (event.action) { + DocumentTopAppBarAction.Audio -> showAudioScreen() + DocumentTopAppBarAction.Video -> showVideoScreen() + // Everything else is not handled here + else -> Unit + } } } }) @@ -71,6 +130,24 @@ class ReadPdfPresenter @AssistedInject constructor( } } + @Composable + private fun rememberMediaAvailability(): State = produceRetainedState(MediaAvailability()) { + val documentIndex = screen.documentIndex + val resourceIndex = screen.resourceIndex + + val audioDeferred = contentDeferred { resourcesRepository.audio(resourceIndex, documentIndex) } + val videoDeferred = contentDeferred { resourcesRepository.video(resourceIndex, documentIndex) } + + value = MediaAvailability( + audio = audioDeferred.await(), + video = videoDeferred.await(), + ) + } + + private fun CoroutineScope.contentDeferred(content: suspend () -> Result>): Deferred { + return async { content().getOrDefault(emptyList()).isNotEmpty() } + } + @CircuitInject(PdfScreen::class, SingletonComponent::class) @AssistedFactory interface Factory { diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt index 790713737..6918a645a 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt @@ -22,16 +22,49 @@ package ss.document.segment.components.pdf +import android.content.Context +import androidx.compose.runtime.Immutable +import app.ss.models.media.MediaAvailability +import com.pspdfkit.jetpack.compose.interactors.DocumentState +import com.slack.circuit.foundation.NavEvent import com.slack.circuit.runtime.CircuitUiState +import com.slack.circuit.runtime.screen.Screen +import io.adventech.blockkit.ui.style.ReaderStyleConfig import kotlinx.collections.immutable.ImmutableList +import ss.document.components.DocumentTopAppBarAction +import ss.libraries.circuit.overlay.BottomSheetOverlay import ss.libraries.pdf.api.LocalFile data class ReadPdfState( val documents: ImmutableList, + val mediaAvailability: MediaAvailability, + val overlayState: ReadPdfOverlayState, val eventSink: (ReadPdfEvent) -> Unit, ) : CircuitUiState sealed interface ReadPdfEvent { data object OnNavBack : ReadPdfEvent + data class OnNavEvent(val event: NavEvent, val context: Context) : ReadPdfEvent + data class OnTopAppBarAction(val action: DocumentTopAppBarAction): ReadPdfEvent } +sealed interface ReadPdfOverlayState : CircuitUiState { + + data object None : ReadPdfOverlayState + + /** Overlay state for a bottom sheet. */ + @Immutable + data class BottomSheet( + val screen: Screen, + val skipPartiallyExpanded: Boolean, + val onResult: (BottomSheetOverlay.Result) -> Unit, + ) : ReadPdfOverlayState +} + +@Immutable +data class PdfTopAppBarState( + val mediaAvailability: MediaAvailability, + val documentState: DocumentState, + val readerStyle: ReaderStyleConfig, +) + diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt index df69ad8c1..1e50965a4 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt @@ -43,16 +43,23 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import app.ss.design.compose.extensions.haptics.LocalSsHapticFeedback import app.ss.design.compose.widget.icon.IconBox import app.ss.design.compose.widget.icon.Icons import com.slack.circuit.codegen.annotations.CircuitInject +import com.slack.circuit.foundation.CircuitContent +import com.slack.circuit.foundation.NavEvent +import com.slack.circuit.overlay.ContentWithOverlays +import com.slack.circuit.overlay.OverlayEffect import dagger.hilt.components.SingletonComponent import io.adventech.blockkit.ui.style.LocalReaderStyle import io.adventech.blockkit.ui.style.primaryForeground import kotlinx.coroutines.launch import ss.libraries.circuit.navigation.PdfScreen +import ss.libraries.circuit.overlay.BottomSheetOverlay @CircuitInject(PdfScreen::class, SingletonComponent::class) @Composable @@ -63,6 +70,7 @@ fun ReadPdfUi(state: ReadPdfState, modifier: Modifier = Modifier) { val readerStyle = LocalReaderStyle.current val coroutineScope = rememberCoroutineScope() val hapticFeedback = LocalSsHapticFeedback.current + val context = LocalContext.current HorizontalPager( state = pagerState, @@ -75,6 +83,7 @@ fun ReadPdfUi(state: ReadPdfState, modifier: Modifier = Modifier) { PdfUi( document = document, + mediaAvailability = state.mediaAvailability, modifier = Modifier, title = { val hasMultipleDocs = state.documents.size > 1 @@ -95,7 +104,11 @@ fun ReadPdfUi(state: ReadPdfState, modifier: Modifier = Modifier) { verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp) ) { - Text(document.title) + Text( + text = document.title, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) if (hasMultipleDocs) { @@ -126,4 +139,27 @@ fun ReadPdfUi(state: ReadPdfState, modifier: Modifier = Modifier) { eventSink = state.eventSink, ) } + + ReadPdfOverlay(state.overlayState) { state.eventSink(ReadPdfEvent.OnNavEvent(it, context)) } +} + +@Composable +private fun ReadPdfOverlay(state: ReadPdfOverlayState, onNavEvent: (event: NavEvent) -> Unit,) { + OverlayEffect(state::class.simpleName) { + when (state) { + is ReadPdfOverlayState.BottomSheet -> state.onResult( + show(BottomSheetOverlay( + skipPartiallyExpanded = state.skipPartiallyExpanded, + ) { + ContentWithOverlays { + CircuitContent( + screen = state.screen, + onNavEvent = onNavEvent, + ) + } + }) + ) + ReadPdfOverlayState.None -> Unit + } + } } From 1fd58a6823aee38aad596862983a385dd8b7e716 Mon Sep 17 00:00:00 2001 From: Tinashe Mzondiwa Date: Mon, 6 Apr 2026 14:41:38 -0400 Subject: [PATCH 11/23] Add loading state --- .../components/pdf/ReadPdfPresenter.kt | 62 +++++++++---------- .../segment/components/pdf/ReadPdfState.kt | 16 +++-- .../segment/components/pdf/ReadPdfUi.kt | 27 +++++++- 3 files changed, 65 insertions(+), 40 deletions(-) diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt index fa4d9f76a..0ad96e5a7 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt @@ -71,9 +71,7 @@ class ReadPdfPresenter @AssistedInject constructor( overlayState = ReadPdfOverlayState.BottomSheet( screen = AudioPlayerScreen(resourceId = screen.resourceId, segmentId = screen.segmentId), skipPartiallyExpanded = true, - onResult = { _ -> - overlayState = ReadPdfOverlayState.None - } + onResult = { _ -> overlayState = ReadPdfOverlayState.None } ) } @@ -81,43 +79,45 @@ class ReadPdfPresenter @AssistedInject constructor( overlayState = ReadPdfOverlayState.BottomSheet( screen = VideosScreen(documentIndex = screen.documentIndex, documentId = screen.documentId), skipPartiallyExpanded = true, - onResult = { _ -> - overlayState = ReadPdfOverlayState.None - } + onResult = { _ -> overlayState = ReadPdfOverlayState.None } ) } - return ReadPdfState( - documents = documents, - mediaAvailability = mediaAvailability, - overlayState = overlayState, - eventSink = { event -> - when (event) { - ReadPdfEvent.OnNavBack -> navigator.pop() - is ReadPdfEvent.OnNavEvent -> { - when (val navEvent = event.event) { - is NavEvent.GoTo -> { - if (navEvent.screen is ExpandedAudioPlayerScreen) { - showAudioScreen() - } else { - navigator.goTo(navEvent.screen) + return when { + documents.isNotEmpty() -> ReadPdfState.Success( + documents = documents, + mediaAvailability = mediaAvailability, + overlayState = overlayState, + eventSink = { event -> + when (event) { + ReadPdfEvent.OnNavBack -> navigator.pop() + is ReadPdfEvent.OnNavEvent -> { + when (val navEvent = event.event) { + is NavEvent.GoTo -> { + if (navEvent.screen is ExpandedAudioPlayerScreen) { + showAudioScreen() + } else { + navigator.goTo(navEvent.screen) + } } - } - else -> navigator.onNavEvent(navEvent) + else -> navigator.onNavEvent(navEvent) + } } - } - is ReadPdfEvent.OnTopAppBarAction -> { - when (event.action) { - DocumentTopAppBarAction.Audio -> showAudioScreen() - DocumentTopAppBarAction.Video -> showVideoScreen() - // Everything else is not handled here - else -> Unit + is ReadPdfEvent.OnTopAppBarAction -> { + when (event.action) { + DocumentTopAppBarAction.Audio -> showAudioScreen() + DocumentTopAppBarAction.Video -> showVideoScreen() + // Everything else is not handled here + else -> Unit + } } } - } - }) + }, + ) + else -> ReadPdfState.Loading + } } @Composable diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt index 6918a645a..ae637912b 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt @@ -35,12 +35,16 @@ import ss.document.components.DocumentTopAppBarAction import ss.libraries.circuit.overlay.BottomSheetOverlay import ss.libraries.pdf.api.LocalFile -data class ReadPdfState( - val documents: ImmutableList, - val mediaAvailability: MediaAvailability, - val overlayState: ReadPdfOverlayState, - val eventSink: (ReadPdfEvent) -> Unit, -) : CircuitUiState +sealed interface ReadPdfState : CircuitUiState { + data object Loading : ReadPdfState + + data class Success( + val documents: ImmutableList, + val mediaAvailability: MediaAvailability, + val overlayState: ReadPdfOverlayState, + val eventSink: (ReadPdfEvent) -> Unit, + ) : ReadPdfState +} sealed interface ReadPdfEvent { data object OnNavBack : ReadPdfEvent diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt index 1e50965a4..651bc6a86 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt @@ -24,6 +24,7 @@ package ss.document.segment.components.pdf import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -31,8 +32,10 @@ import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -45,8 +48,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import app.ss.design.compose.extensions.haptics.LocalSsHapticFeedback +import app.ss.design.compose.theme.SsTheme import app.ss.design.compose.widget.icon.IconBox import app.ss.design.compose.widget.icon.Icons import com.slack.circuit.codegen.annotations.CircuitInject @@ -64,6 +69,18 @@ import ss.libraries.circuit.overlay.BottomSheetOverlay @CircuitInject(PdfScreen::class, SingletonComponent::class) @Composable fun ReadPdfUi(state: ReadPdfState, modifier: Modifier = Modifier) { + when (state) { + ReadPdfState.Loading -> Surface { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } + is ReadPdfState.Success -> ReadPdfSuccessUi(state, modifier) + } +} + +@Composable +private fun ReadPdfSuccessUi(state: ReadPdfState.Success, modifier: Modifier = Modifier) { val pagerState = rememberPagerState( pageCount = { state.documents.size }, ) @@ -126,9 +143,7 @@ fun ReadPdfUi(state: ReadPdfState, modifier: Modifier = Modifier) { text = { Text(doc.title) }, onClick = { expanded = false - coroutineScope.launch { - pagerState.animateScrollToPage(index) - } + coroutineScope.launch { pagerState.animateScrollToPage(index) } } ) } @@ -163,3 +178,9 @@ private fun ReadPdfOverlay(state: ReadPdfOverlayState, onNavEvent: (event: NavEv } } } + +@PreviewLightDark +@Composable +private fun PreviewLoading() { + SsTheme { ReadPdfUi(ReadPdfState.Loading) } +} From 6e56223aaa794cfb2fc7000d2749c7f439a42718 Mon Sep 17 00:00:00 2001 From: Tinashe Mzondiwa Date: Mon, 6 Apr 2026 21:20:58 -0400 Subject: [PATCH 12/23] Save PdfConfiguration to prefs --- .../document/segment/components/pdf/PdfUi.kt | 110 +++++++++++------- .../components/pdf/ReadPdfPresenter.kt | 20 ++++ .../segment/components/pdf/ReadPdfState.kt | 17 ++- .../segment/components/pdf/ReadPdfUi.kt | 1 + .../main/kotlin/app/ss/pdf/PdfReaderImpl.kt | 7 +- ...dfReaderPrefs.kt => PdfReaderPrefsImpl.kt} | 13 +-- .../kotlin/app/ss/pdf/di/BindingsModule.kt | 2 +- .../kotlin/app/ss/pdf/ui/SSReadPdfActivity.kt | 5 +- libraries/pdf/api/build.gradle.kts | 1 + .../ss/libraries/pdf/api/PdfReaderPrefs.kt | 41 +++++++ 10 files changed, 156 insertions(+), 61 deletions(-) rename features/pdf/src/main/kotlin/app/ss/pdf/{PdfReaderPrefs.kt => PdfReaderPrefsImpl.kt} (90%) create mode 100644 libraries/pdf/api/src/main/kotlin/ss/libraries/pdf/api/PdfReaderPrefs.kt diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt index 7ca7987ba..171492c13 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt @@ -24,7 +24,6 @@ package ss.document.segment.components.pdf import android.content.Context import androidx.compose.animation.core.animateDpAsState -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -40,24 +39,32 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onVisibilityChanged import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.FragmentManager import app.ss.design.compose.widget.scaffold.LocalNavbarController import app.ss.models.media.MediaAvailability +import com.pspdfkit.annotations.AnnotationType import com.pspdfkit.configuration.activity.PdfActivityConfiguration +import com.pspdfkit.configuration.activity.ThumbnailBarMode import com.pspdfkit.configuration.activity.UserInterfaceViewMode -import com.pspdfkit.configuration.theming.ThemeMode +import com.pspdfkit.configuration.page.PageFitMode +import com.pspdfkit.configuration.settings.SettingsMenuItemType +import com.pspdfkit.configuration.sharing.ShareFeatures +import com.pspdfkit.jetpack.compose.interactors.DocumentListener import com.pspdfkit.jetpack.compose.interactors.getDefaultDocumentManager import com.pspdfkit.jetpack.compose.interactors.rememberDocumentState import com.pspdfkit.jetpack.compose.views.DocumentView import io.adventech.blockkit.ui.style.LocalReaderStyle -import io.adventech.blockkit.ui.style.ReaderStyle import io.adventech.blockkit.ui.style.background import io.adventech.blockkit.ui.style.primaryForeground import ss.document.components.DocumentTopAppBarAction import ss.libraries.pdf.api.LocalFile +import java.util.EnumSet import app.ss.translations.R as L10nR import com.pspdfkit.R as PspdfR import ss.document.R as DocumentR @@ -66,26 +73,18 @@ import ss.document.R as DocumentR fun PdfUi( document: LocalFile, mediaAvailability: MediaAvailability, + config: PdfReaderConfig, modifier: Modifier = Modifier, title: @Composable () -> Unit = { Text(document.title) }, eventSink: (ReadPdfEvent) -> Unit = {}, ) { val context = LocalContext.current val documentUri = document.uri - val readerStyle = LocalReaderStyle.current - val themeResId = readerStyle.theme.toPdfThemeResId() - - // Resolve ThemeMode for PSPDFKit's built-in document inversion - val themeMode = when (readerStyle.theme) { - ReaderStyle.Theme.Dark -> ThemeMode.NIGHT - ReaderStyle.Theme.Auto -> if (isSystemInDarkTheme()) ThemeMode.NIGHT else ThemeMode.DEFAULT - else -> ThemeMode.DEFAULT - } - val pdfActivityConfiguration = rememberPdfConfiguration(context, document, themeResId, themeMode) + val pdfActivityConfiguration = rememberPdfConfiguration(context, document, config) val documentState = rememberDocumentState(documentUri, pdfActivityConfiguration) - val bottomPadding by animateDpAsState(if (LocalNavbarController.current.enabled) 80.dp else 0.dp) + val bottomPadding by animateDpAsState(if (LocalNavbarController.current.enabled) 120.dp else 0.dp) Column(modifier = modifier.fillMaxSize()) { PdfTopAppBar( @@ -93,17 +92,30 @@ fun PdfUi( state = PdfTopAppBarState( mediaAvailability = mediaAvailability, documentState = documentState, - readerStyle = readerStyle, ), eventSink = eventSink, ) + val activity = LocalContext.current as? FragmentActivity + DocumentView( documentState = documentState, modifier = Modifier .weight(1f) - .padding(bottom = bottomPadding), - documentManager = getDefaultDocumentManager(), + .padding(bottom = bottomPadding) + .onVisibilityChanged { visible -> + if (!visible) { // Save configuration once the document is not visible + val pdfFragment = activity?.supportFragmentManager?.findPdfFragment() + // todo: Also save annotations + pdfFragment?.let { eventSink(ReadPdfEvent.OnConfigurationChanged(it.configuration)) } + } + }, + documentManager = getDefaultDocumentManager( + documentListener = DocumentListener(onDocumentLoaded = { + // set annotations + // documentState.documentConnection.addAnnotationToPage() + }), + ), ) } } @@ -116,7 +128,8 @@ private fun PdfTopAppBar( eventSink: (ReadPdfEvent) -> Unit, modifier: Modifier = Modifier, ) { - val (mediaAvailability, documentState, readerStyle) = state + val readerTheme = LocalReaderStyle.current.theme + val (mediaAvailability, documentState) = state TopAppBar( title = title, @@ -170,43 +183,62 @@ private fun PdfTopAppBar( } }, colors = TopAppBarDefaults.topAppBarColors( - containerColor = readerStyle.theme.background(), - navigationIconContentColor = readerStyle.theme.primaryForeground(), - actionIconContentColor = readerStyle.theme.primaryForeground(), - titleContentColor = readerStyle.theme.primaryForeground(), + containerColor = readerTheme.background(), + navigationIconContentColor = readerTheme.primaryForeground(), + actionIconContentColor = readerTheme.primaryForeground(), + titleContentColor = readerTheme.primaryForeground(), ) ) } -@Composable -fun ReaderStyle.Theme.toPdfThemeResId(): Int { - return when (this) { - ReaderStyle.Theme.Light -> DocumentR.style.Theme_SS_Pdf_Light - ReaderStyle.Theme.Dark -> DocumentR.style.Theme_SS_Pdf_Dark - ReaderStyle.Theme.Sepia -> DocumentR.style.Theme_SS_Pdf_Sepia - ReaderStyle.Theme.Auto -> { - if (isSystemInDarkTheme()) DocumentR.style.Theme_SS_Pdf_Dark else DocumentR.style.Theme_SS_Pdf_Light - } - } -} - @Composable private fun rememberPdfConfiguration( context: Context, file: LocalFile, - themeResId: Int, - themeMode: ThemeMode, -) = remember(themeResId, themeMode) { + config: PdfReaderConfig, +) = remember(file, config) { + val excludedAnnotationTypes = ArrayList(EnumSet.allOf(AnnotationType::class.java)) + allowedAnnotations.forEach { excludedAnnotationTypes.remove(it) } + PdfActivityConfiguration .Builder(context) .setUserInterfaceViewMode(UserInterfaceViewMode.USER_INTERFACE_VIEW_MODE_VISIBLE) .defaultToolbarEnabled(false) .title(file.title) - .themeMode(themeMode) - .theme(themeResId) + .scrollMode(config.scrollMode) + .layoutMode(config.layoutMode) + .scrollDirection(config.scrollDirection) + .themeMode(config.themeMode) + .fitMode(PageFitMode.FIT_TO_WIDTH) + .animateScrollOnEdgeTaps(true) + .excludedAnnotationTypes(excludedAnnotationTypes) + .setEnabledShareFeatures(EnumSet.noneOf(ShareFeatures::class.java)) + .setThumbnailBarMode(ThumbnailBarMode.THUMBNAIL_BAR_MODE_NONE) + .setSettingsMenuItems(EnumSet.allOf(SettingsMenuItemType::class.java)) .build() } +private val allowedAnnotations = listOf( + AnnotationType.HIGHLIGHT, + AnnotationType.INK, + AnnotationType.NOTE, + AnnotationType.WATERMARK, + AnnotationType.STRIKEOUT, + AnnotationType.FREETEXT, +) + +// I know :-( +private fun FragmentManager.findPdfFragment(): com.pspdfkit.ui.PdfFragment? { + for (fragment in fragments) { + if (fragment is com.pspdfkit.ui.PdfFragment) return fragment + + // Recursively search child fragments + val child = fragment.childFragmentManager.findPdfFragment() + if (child != null) return child + } + return null +} + @Composable private fun MediaIcon( action: DocumentTopAppBarAction, diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt index 0ad96e5a7..965ae3fcd 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt @@ -52,12 +52,14 @@ import ss.libraries.circuit.navigation.PdfScreen import ss.libraries.circuit.navigation.VideosScreen import ss.libraries.pdf.api.LocalFile import ss.libraries.pdf.api.PdfReader +import ss.libraries.pdf.api.PdfReaderPrefs import ss.resources.api.ResourcesRepository class ReadPdfPresenter @AssistedInject constructor( @Assisted private val navigator: Navigator, @Assisted private val screen: PdfScreen, private val pdfReader: PdfReader, + private val pdfReaderPrefs: PdfReaderPrefs, private val resourcesRepository: ResourcesRepository, ) : Presenter { @@ -65,6 +67,7 @@ class ReadPdfPresenter @AssistedInject constructor( override fun present(): ReadPdfState { val documents by rememberFiles() val mediaAvailability by rememberMediaAvailability() + val config by rememberPdfReaderConfig() var overlayState by rememberRetained { mutableStateOf(ReadPdfOverlayState.None) } fun showAudioScreen() { @@ -87,6 +90,7 @@ class ReadPdfPresenter @AssistedInject constructor( documents.isNotEmpty() -> ReadPdfState.Success( documents = documents, mediaAvailability = mediaAvailability, + config = config, overlayState = overlayState, eventSink = { event -> when (event) { @@ -113,6 +117,10 @@ class ReadPdfPresenter @AssistedInject constructor( else -> Unit } } + + is ReadPdfEvent.OnConfigurationChanged -> { + pdfReaderPrefs.saveConfiguration(event.config) + } } }, ) @@ -148,6 +156,18 @@ class ReadPdfPresenter @AssistedInject constructor( return async { content().getOrDefault(emptyList()).isNotEmpty() } } + @Composable + private fun rememberPdfReaderConfig(): State = rememberRetained { + mutableStateOf( + PdfReaderConfig( + scrollMode = pdfReaderPrefs.scrollMode(), + layoutMode = pdfReaderPrefs.pageLayoutMode(), + scrollDirection = pdfReaderPrefs.scrollDirection(), + themeMode = pdfReaderPrefs.themeMode(), + ) + ) + } + @CircuitInject(PdfScreen::class, SingletonComponent::class) @AssistedFactory interface Factory { diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt index ae637912b..afd544d24 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt @@ -25,11 +25,15 @@ package ss.document.segment.components.pdf import android.content.Context import androidx.compose.runtime.Immutable import app.ss.models.media.MediaAvailability +import com.pspdfkit.configuration.PdfConfiguration +import com.pspdfkit.configuration.page.PageLayoutMode +import com.pspdfkit.configuration.page.PageScrollDirection +import com.pspdfkit.configuration.page.PageScrollMode +import com.pspdfkit.configuration.theming.ThemeMode import com.pspdfkit.jetpack.compose.interactors.DocumentState import com.slack.circuit.foundation.NavEvent import com.slack.circuit.runtime.CircuitUiState import com.slack.circuit.runtime.screen.Screen -import io.adventech.blockkit.ui.style.ReaderStyleConfig import kotlinx.collections.immutable.ImmutableList import ss.document.components.DocumentTopAppBarAction import ss.libraries.circuit.overlay.BottomSheetOverlay @@ -41,6 +45,7 @@ sealed interface ReadPdfState : CircuitUiState { data class Success( val documents: ImmutableList, val mediaAvailability: MediaAvailability, + val config: PdfReaderConfig, val overlayState: ReadPdfOverlayState, val eventSink: (ReadPdfEvent) -> Unit, ) : ReadPdfState @@ -50,6 +55,7 @@ sealed interface ReadPdfEvent { data object OnNavBack : ReadPdfEvent data class OnNavEvent(val event: NavEvent, val context: Context) : ReadPdfEvent data class OnTopAppBarAction(val action: DocumentTopAppBarAction): ReadPdfEvent + data class OnConfigurationChanged(val config: PdfConfiguration) : ReadPdfEvent } sealed interface ReadPdfOverlayState : CircuitUiState { @@ -69,6 +75,13 @@ sealed interface ReadPdfOverlayState : CircuitUiState { data class PdfTopAppBarState( val mediaAvailability: MediaAvailability, val documentState: DocumentState, - val readerStyle: ReaderStyleConfig, +) + +@Immutable +data class PdfReaderConfig( + val scrollMode: PageScrollMode, + val layoutMode: PageLayoutMode, + val scrollDirection: PageScrollDirection, + val themeMode: ThemeMode, ) diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt index 651bc6a86..3108cea4e 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt @@ -101,6 +101,7 @@ private fun ReadPdfSuccessUi(state: ReadPdfState.Success, modifier: Modifier = M PdfUi( document = document, mediaAvailability = state.mediaAvailability, + config = state.config, modifier = Modifier, title = { val hasMultipleDocs = state.documents.size > 1 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 a5de69aab..3f300c30b 100644 --- a/features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderImpl.kt +++ b/features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderImpl.kt @@ -40,18 +40,19 @@ import com.pspdfkit.document.download.DownloadJob import com.pspdfkit.document.download.DownloadRequest import com.pspdfkit.ui.PdfActivityIntentBuilder import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import ss.foundation.coroutines.DispatcherProvider import ss.libraries.circuit.navigation.PdfScreen import ss.libraries.pdf.api.LocalFile import ss.libraries.pdf.api.PdfReader +import ss.libraries.pdf.api.PdfReaderPrefs import timber.log.Timber import java.io.File import java.util.EnumSet import javax.inject.Inject import javax.inject.Singleton import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine @Singleton internal class PdfReaderImpl @Inject constructor( @@ -122,11 +123,11 @@ internal class PdfReaderImpl @Inject constructor( private suspend fun downloadFile( context: Context, pdf: PDFAux - ): LocalFile? = suspendCoroutine { continuation -> + ): LocalFile? = suspendCancellableCoroutine { continuation -> val outputFile = File(context.getDir(FILE_DIRECTORY, Context.MODE_PRIVATE), "${pdf.id}.pdf") if (outputFile.exists()) { continuation.resume(LocalFile(pdf.title, Uri.fromFile(outputFile))) - return@suspendCoroutine + return@suspendCancellableCoroutine } val request: DownloadRequest = DownloadRequest.Builder(context) diff --git a/features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderPrefs.kt b/features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderPrefsImpl.kt similarity index 90% rename from features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderPrefs.kt rename to features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderPrefsImpl.kt index f4ef68876..82fb9e837 100644 --- a/features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderPrefs.kt +++ b/features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderPrefsImpl.kt @@ -32,21 +32,10 @@ import com.pspdfkit.configuration.page.PageScrollDirection import com.pspdfkit.configuration.page.PageScrollMode import com.pspdfkit.configuration.theming.ThemeMode import dagger.hilt.android.qualifiers.ApplicationContext +import ss.libraries.pdf.api.PdfReaderPrefs import javax.inject.Inject import javax.inject.Singleton -interface PdfReaderPrefs { - fun scrollMode(): PageScrollMode - fun setScrollMode(mode: PageScrollMode) - fun pageLayoutMode(): PageLayoutMode - fun setPageLayoutMode(mode: PageLayoutMode) - fun scrollDirection(): PageScrollDirection - fun setScrollDirection(direction: PageScrollDirection) - fun themeMode(): ThemeMode - fun setThemeMode(mode: ThemeMode) - fun saveConfiguration(configuration: PdfConfiguration) -} - @Singleton internal class PdfReaderPrefsImpl @Inject constructor( @param:ApplicationContext private val context: Context, diff --git a/features/pdf/src/main/kotlin/app/ss/pdf/di/BindingsModule.kt b/features/pdf/src/main/kotlin/app/ss/pdf/di/BindingsModule.kt index 52ad7d470..3c9a3ad5c 100644 --- a/features/pdf/src/main/kotlin/app/ss/pdf/di/BindingsModule.kt +++ b/features/pdf/src/main/kotlin/app/ss/pdf/di/BindingsModule.kt @@ -23,13 +23,13 @@ package app.ss.pdf.di import app.ss.pdf.PdfReaderImpl -import app.ss.pdf.PdfReaderPrefs import app.ss.pdf.PdfReaderPrefsImpl import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import ss.libraries.pdf.api.PdfReader +import ss.libraries.pdf.api.PdfReaderPrefs @Module @InstallIn(SingletonComponent::class) diff --git a/features/pdf/src/main/kotlin/app/ss/pdf/ui/SSReadPdfActivity.kt b/features/pdf/src/main/kotlin/app/ss/pdf/ui/SSReadPdfActivity.kt index 5f57d6960..363e5031e 100644 --- a/features/pdf/src/main/kotlin/app/ss/pdf/ui/SSReadPdfActivity.kt +++ b/features/pdf/src/main/kotlin/app/ss/pdf/ui/SSReadPdfActivity.kt @@ -30,12 +30,8 @@ import android.widget.ProgressBar import androidx.activity.viewModels import androidx.annotation.DrawableRes import androidx.annotation.StringRes -import androidx.core.content.ContextCompat import app.ss.media.playback.ui.nowPlaying.showNowPlaying import app.ss.media.playback.ui.video.showVideoList -import app.ss.pdf.PdfReaderPrefs -import app.ss.pdf.R -import com.cryart.sabbathschool.core.extensions.view.tint import com.pspdfkit.document.DocumentSource import com.pspdfkit.document.PdfDocument import com.pspdfkit.ui.DocumentDescriptor @@ -44,6 +40,7 @@ import com.pspdfkit.ui.tabs.PdfTabBarCloseMode import dagger.hilt.android.AndroidEntryPoint import io.adventech.blockkit.model.input.PDFAuxAnnotations import ss.foundation.coroutines.flow.collectIn +import ss.libraries.pdf.api.PdfReaderPrefs import javax.inject.Inject import app.ss.translations.R as L10n import ss.libraries.media.resources.R as MediaR diff --git a/libraries/pdf/api/build.gradle.kts b/libraries/pdf/api/build.gradle.kts index ef6abb2a8..76eafd79c 100644 --- a/libraries/pdf/api/build.gradle.kts +++ b/libraries/pdf/api/build.gradle.kts @@ -4,6 +4,7 @@ plugins { } dependencies { + implementation(libs.nutrient) implementation(projects.common.models) implementation(projects.libraries.circuit.api) } diff --git a/libraries/pdf/api/src/main/kotlin/ss/libraries/pdf/api/PdfReaderPrefs.kt b/libraries/pdf/api/src/main/kotlin/ss/libraries/pdf/api/PdfReaderPrefs.kt new file mode 100644 index 000000000..dd98a12c0 --- /dev/null +++ b/libraries/pdf/api/src/main/kotlin/ss/libraries/pdf/api/PdfReaderPrefs.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2026. 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.libraries.pdf.api + +import com.pspdfkit.configuration.PdfConfiguration +import com.pspdfkit.configuration.page.PageLayoutMode +import com.pspdfkit.configuration.page.PageScrollDirection +import com.pspdfkit.configuration.page.PageScrollMode +import com.pspdfkit.configuration.theming.ThemeMode + +interface PdfReaderPrefs { + fun scrollMode(): PageScrollMode + fun setScrollMode(mode: PageScrollMode) + fun pageLayoutMode(): PageLayoutMode + fun setPageLayoutMode(mode: PageLayoutMode) + fun scrollDirection(): PageScrollDirection + fun setScrollDirection(direction: PageScrollDirection) + fun themeMode(): ThemeMode + fun setThemeMode(mode: ThemeMode) + fun saveConfiguration(configuration: PdfConfiguration) +} From 6099cbb1bd1dcc2ee3fe8ac19d98e24ecb449110 Mon Sep 17 00:00:00 2001 From: Tinashe Mzondiwa Date: Mon, 6 Apr 2026 22:05:09 -0400 Subject: [PATCH 13/23] Save annotations --- .../document/segment/components/pdf/PdfUi.kt | 26 +++++++++++--- .../components/pdf/ReadPdfPresenter.kt | 36 ++++++++++++++++++- .../segment/components/pdf/ReadPdfState.kt | 9 ++++- 3 files changed, 64 insertions(+), 7 deletions(-) diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt index 171492c13..8743e524b 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt @@ -38,6 +38,7 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onVisibilityChanged import androidx.compose.ui.platform.LocalContext @@ -62,6 +63,7 @@ import com.pspdfkit.jetpack.compose.views.DocumentView import io.adventech.blockkit.ui.style.LocalReaderStyle import io.adventech.blockkit.ui.style.background import io.adventech.blockkit.ui.style.primaryForeground +import kotlinx.coroutines.launch import ss.document.components.DocumentTopAppBarAction import ss.libraries.pdf.api.LocalFile import java.util.EnumSet @@ -71,16 +73,17 @@ import ss.document.R as DocumentR @Composable fun PdfUi( - document: LocalFile, + document: PdfDocumentState, mediaAvailability: MediaAvailability, config: PdfReaderConfig, modifier: Modifier = Modifier, - title: @Composable () -> Unit = { Text(document.title) }, + title: @Composable () -> Unit = { Text(document.file.title) }, eventSink: (ReadPdfEvent) -> Unit = {}, ) { val context = LocalContext.current - val documentUri = document.uri - val pdfActivityConfiguration = rememberPdfConfiguration(context, document, config) + val coroutineScope = rememberCoroutineScope() + val documentUri = document.file.uri + val pdfActivityConfiguration = rememberPdfConfiguration(context, document.file, config) val documentState = rememberDocumentState(documentUri, pdfActivityConfiguration) @@ -111,9 +114,22 @@ fun PdfUi( } }, documentManager = getDefaultDocumentManager( - documentListener = DocumentListener(onDocumentLoaded = { + documentListener = DocumentListener(onDocumentLoaded = { pdfDoc -> // set annotations // documentState.documentConnection.addAnnotationToPage() + with(pdfDoc.annotationProvider) { + + } + if (document.annotations.isNotEmpty()) { + coroutineScope.launch { + val annotations = pdfDoc.annotationProvider + .getAllAnnotationsOfType(allowedAnnotations.toSet()) + annotations.forEach { pdfDoc.annotationProvider.removeAnnotationFromPage(it) } + } + + document.annotations.flatMap { it.annotations } + // .map { create } + } }), ), ) diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt index 965ae3fcd..6f7b0f1ed 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt @@ -39,12 +39,16 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.components.SingletonComponent +import io.adventech.blockkit.model.input.PDFAuxAnnotations +import io.adventech.blockkit.model.input.UserInput import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.async +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map import ss.document.components.DocumentTopAppBarAction import ss.libraries.circuit.navigation.AudioPlayerScreen import ss.libraries.circuit.navigation.ExpandedAudioPlayerScreen @@ -54,6 +58,7 @@ import ss.libraries.pdf.api.LocalFile import ss.libraries.pdf.api.PdfReader import ss.libraries.pdf.api.PdfReaderPrefs import ss.resources.api.ResourcesRepository +import timber.log.Timber class ReadPdfPresenter @AssistedInject constructor( @Assisted private val navigator: Navigator, @@ -66,9 +71,20 @@ class ReadPdfPresenter @AssistedInject constructor( @Composable override fun present(): ReadPdfState { val documents by rememberFiles() + val annotationsMap by rememberDocumentAnnotations() val mediaAvailability by rememberMediaAvailability() val config by rememberPdfReaderConfig() var overlayState by rememberRetained { mutableStateOf(ReadPdfOverlayState.None) } + val documentsState by rememberRetained(documents, annotationsMap) { + mutableStateOf( + documents.mapIndexed { index, file -> + PdfDocumentState( + file = file, + annotations = annotationsMap.getOrDefault(index, emptyList()).toImmutableList(), + ) + }.toImmutableList() + ) + } fun showAudioScreen() { overlayState = ReadPdfOverlayState.BottomSheet( @@ -88,7 +104,7 @@ class ReadPdfPresenter @AssistedInject constructor( return when { documents.isNotEmpty() -> ReadPdfState.Success( - documents = documents, + documents = documentsState, mediaAvailability = mediaAvailability, config = config, overlayState = overlayState, @@ -168,6 +184,24 @@ class ReadPdfPresenter @AssistedInject constructor( ) } + @Composable + private fun rememberDocumentAnnotations(): State>> = produceRetainedState(emptyMap()) { + resourcesRepository.documentInput(screen.documentId) + .map { userInputs -> + userInputs.asSequence() + .mapNotNull { it as? UserInput.Annotation } + .toList() + } + .map { input -> + val pdfs = screen.pdfs + pdfs.mapIndexed { index, pdf -> + index to input.filter { it.pdfId == pdf.id }.flatMap { it.data } + }.toMap() + } + .catch { Timber.e(it) } + .collect { value = it } + } + @CircuitInject(PdfScreen::class, SingletonComponent::class) @AssistedFactory interface Factory { diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt index afd544d24..08d1e4c77 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt @@ -34,6 +34,7 @@ import com.pspdfkit.jetpack.compose.interactors.DocumentState import com.slack.circuit.foundation.NavEvent import com.slack.circuit.runtime.CircuitUiState import com.slack.circuit.runtime.screen.Screen +import io.adventech.blockkit.model.input.PDFAuxAnnotations import kotlinx.collections.immutable.ImmutableList import ss.document.components.DocumentTopAppBarAction import ss.libraries.circuit.overlay.BottomSheetOverlay @@ -43,7 +44,7 @@ sealed interface ReadPdfState : CircuitUiState { data object Loading : ReadPdfState data class Success( - val documents: ImmutableList, + val documents: ImmutableList, val mediaAvailability: MediaAvailability, val config: PdfReaderConfig, val overlayState: ReadPdfOverlayState, @@ -71,6 +72,12 @@ sealed interface ReadPdfOverlayState : CircuitUiState { ) : ReadPdfOverlayState } +@Immutable +data class PdfDocumentState( + val file: LocalFile, + val annotations: ImmutableList +) + @Immutable data class PdfTopAppBarState( val mediaAvailability: MediaAvailability, From 1cd605c2e65878caa90f4fb01a7105413957a9a8 Mon Sep 17 00:00:00 2001 From: Tinashe Mzondiwa Date: Mon, 6 Apr 2026 23:04:48 -0400 Subject: [PATCH 14/23] Save annotations --- .../document/segment/components/pdf/PdfUi.kt | 54 +++++++++++++------ .../components/pdf/ReadPdfPresenter.kt | 43 +++++++++++++-- .../segment/components/pdf/ReadPdfState.kt | 10 +++- .../segment/components/pdf/ReadPdfUi.kt | 4 +- 4 files changed, 88 insertions(+), 23 deletions(-) diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt index 8743e524b..f1913f7d9 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt @@ -36,9 +36,12 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onVisibilityChanged import androidx.compose.ui.platform.LocalContext @@ -56,13 +59,16 @@ import com.pspdfkit.configuration.activity.UserInterfaceViewMode import com.pspdfkit.configuration.page.PageFitMode import com.pspdfkit.configuration.settings.SettingsMenuItemType import com.pspdfkit.configuration.sharing.ShareFeatures +import com.pspdfkit.document.PdfDocument import com.pspdfkit.jetpack.compose.interactors.DocumentListener import com.pspdfkit.jetpack.compose.interactors.getDefaultDocumentManager import com.pspdfkit.jetpack.compose.interactors.rememberDocumentState import com.pspdfkit.jetpack.compose.views.DocumentView +import io.adventech.blockkit.model.input.PDFAuxAnnotations import io.adventech.blockkit.ui.style.LocalReaderStyle import io.adventech.blockkit.ui.style.background import io.adventech.blockkit.ui.style.primaryForeground +import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.launch import ss.document.components.DocumentTopAppBarAction import ss.libraries.pdf.api.LocalFile @@ -84,6 +90,7 @@ fun PdfUi( val coroutineScope = rememberCoroutineScope() val documentUri = document.file.uri val pdfActivityConfiguration = rememberPdfConfiguration(context, document.file, config) + var loadedDocument by remember { mutableStateOf(null) } val documentState = rememberDocumentState(documentUri, pdfActivityConfiguration) @@ -109,31 +116,47 @@ fun PdfUi( .onVisibilityChanged { visible -> if (!visible) { // Save configuration once the document is not visible val pdfFragment = activity?.supportFragmentManager?.findPdfFragment() - // todo: Also save annotations - pdfFragment?.let { eventSink(ReadPdfEvent.OnConfigurationChanged(it.configuration)) } + coroutineScope.launch { + val annotations = pdfFragment?.document?.annotationProvider?.getAllAnnotationsOfType(allowedAnnotations.toSet()) + pdfFragment?.let { + eventSink( + ReadPdfEvent.OnConfigurationChanged( + config = it.configuration, + annotations = annotations, + pdfId = document.pdfId, + ) + ) + } + } } }, documentManager = getDefaultDocumentManager( documentListener = DocumentListener(onDocumentLoaded = { pdfDoc -> // set annotations - // documentState.documentConnection.addAnnotationToPage() - with(pdfDoc.annotationProvider) { + coroutineScope.launch { pdfDoc.loadAnnotations(document.annotations) } - } - if (document.annotations.isNotEmpty()) { - coroutineScope.launch { - val annotations = pdfDoc.annotationProvider - .getAllAnnotationsOfType(allowedAnnotations.toSet()) - annotations.forEach { pdfDoc.annotationProvider.removeAnnotationFromPage(it) } - } - - document.annotations.flatMap { it.annotations } - // .map { create } - } + loadedDocument = pdfDoc }), ), ) } + + LaunchedEffect(document.annotations, loadedDocument) { + loadedDocument?.loadAnnotations(document.annotations) + } +} + +private suspend fun PdfDocument.loadAnnotations(annotations: ImmutableList) { + with(annotationProvider) { + // Remove existing + val existingAnnotations = annotationProvider + .getAllAnnotationsOfType(allowedAnnotations.toSet()) + existingAnnotations.forEach { removeAnnotationFromPage(it) } + + // Add annotations + annotations.flatMap { it.annotations } + .forEach { createAnnotationFromInstantJson(it) } + } } @OptIn(ExperimentalMaterial3Api::class) @@ -241,6 +264,7 @@ private val allowedAnnotations = listOf( AnnotationType.WATERMARK, AnnotationType.STRIKEOUT, AnnotationType.FREETEXT, + AnnotationType.UNDERLINE, ) // I know :-( diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt index 6f7b0f1ed..773560a2e 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import app.ss.models.media.MediaAvailability +import com.pspdfkit.annotations.Annotation import com.slack.circuit.codegen.annotations.CircuitInject import com.slack.circuit.foundation.NavEvent import com.slack.circuit.foundation.onNavEvent @@ -41,6 +42,7 @@ import dagger.assisted.AssistedInject import dagger.hilt.components.SingletonComponent import io.adventech.blockkit.model.input.PDFAuxAnnotations import io.adventech.blockkit.model.input.UserInput +import io.adventech.blockkit.model.input.UserInputRequest import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -48,8 +50,10 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import ss.document.components.DocumentTopAppBarAction +import ss.foundation.coroutines.DispatcherProvider import ss.libraries.circuit.navigation.AudioPlayerScreen import ss.libraries.circuit.navigation.ExpandedAudioPlayerScreen import ss.libraries.circuit.navigation.PdfScreen @@ -66,6 +70,7 @@ class ReadPdfPresenter @AssistedInject constructor( private val pdfReader: PdfReader, private val pdfReaderPrefs: PdfReaderPrefs, private val resourcesRepository: ResourcesRepository, + private val dispatcherProvider: DispatcherProvider, ) : Presenter { @Composable @@ -75,15 +80,15 @@ class ReadPdfPresenter @AssistedInject constructor( val mediaAvailability by rememberMediaAvailability() val config by rememberPdfReaderConfig() var overlayState by rememberRetained { mutableStateOf(ReadPdfOverlayState.None) } - val documentsState by rememberRetained(documents, annotationsMap) { - mutableStateOf( + val documentsState = rememberRetained(documents, annotationsMap) { documents.mapIndexed { index, file -> + val pdfId = screen.pdfs.getOrNull(index)?.id.orEmpty() PdfDocumentState( + pdfId = pdfId, file = file, annotations = annotationsMap.getOrDefault(index, emptyList()).toImmutableList(), ) }.toImmutableList() - ) } fun showAudioScreen() { @@ -136,6 +141,8 @@ class ReadPdfPresenter @AssistedInject constructor( is ReadPdfEvent.OnConfigurationChanged -> { pdfReaderPrefs.saveConfiguration(event.config) + + event.annotations?.let { saveAnnotations(pdfId = event.pdfId, annotations = it) } } } }, @@ -193,15 +200,43 @@ class ReadPdfPresenter @AssistedInject constructor( .toList() } .map { input -> + val inputByPdfId = input.groupBy { it.pdfId } val pdfs = screen.pdfs pdfs.mapIndexed { index, pdf -> - index to input.filter { it.pdfId == pdf.id }.flatMap { it.data } + index to (inputByPdfId[pdf.id]?.flatMap { it.data } ?: emptyList()) }.toMap() } .catch { Timber.e(it) } + .flowOn(dispatcherProvider.default) .collect { value = it } } + private fun saveAnnotations(pdfId: String, annotations: List) { + if (pdfId.isEmpty()) return + + val documentId = screen.documentId + val syncAnnotations = annotations.toSync() + + val userInput = UserInputRequest.Annotation( + blockId = pdfId, + pdfId = pdfId, + data = syncAnnotations + ) + + resourcesRepository.saveDocumentInput(documentId, userInput) + } + + private fun List.toSync(): List { + val groupedAnnotations = groupBy { it.pageIndex } + return groupedAnnotations.map { (pageIndex, list) -> + val annotations = list.map { it.toInstantJson() }.filter(::isValidInstantJson) + PDFAuxAnnotations(pageIndex, annotations) + } + } + + + private fun isValidInstantJson(json: String) = json != "null" + @CircuitInject(PdfScreen::class, SingletonComponent::class) @AssistedFactory interface Factory { diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt index 08d1e4c77..de8d87631 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt @@ -39,6 +39,7 @@ import kotlinx.collections.immutable.ImmutableList import ss.document.components.DocumentTopAppBarAction import ss.libraries.circuit.overlay.BottomSheetOverlay import ss.libraries.pdf.api.LocalFile +import com.pspdfkit.annotations.Annotation sealed interface ReadPdfState : CircuitUiState { data object Loading : ReadPdfState @@ -56,7 +57,11 @@ sealed interface ReadPdfEvent { data object OnNavBack : ReadPdfEvent data class OnNavEvent(val event: NavEvent, val context: Context) : ReadPdfEvent data class OnTopAppBarAction(val action: DocumentTopAppBarAction): ReadPdfEvent - data class OnConfigurationChanged(val config: PdfConfiguration) : ReadPdfEvent + data class OnConfigurationChanged( + val config: PdfConfiguration, + val annotations: List?, + val pdfId: String, + ) : ReadPdfEvent } sealed interface ReadPdfOverlayState : CircuitUiState { @@ -74,8 +79,9 @@ sealed interface ReadPdfOverlayState : CircuitUiState { @Immutable data class PdfDocumentState( + val pdfId: String, val file: LocalFile, - val annotations: ImmutableList + val annotations: ImmutableList, ) @Immutable diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt index 3108cea4e..a318f9948 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt @@ -123,7 +123,7 @@ private fun ReadPdfSuccessUi(state: ReadPdfState.Success, modifier: Modifier = M horizontalArrangement = Arrangement.spacedBy(4.dp) ) { Text( - text = document.title, + text = document.file.title, maxLines = 1, overflow = TextOverflow.Ellipsis, ) @@ -141,7 +141,7 @@ private fun ReadPdfSuccessUi(state: ReadPdfState.Success, modifier: Modifier = M ) { state.documents.forEachIndexed { index, doc -> DropdownMenuItem( - text = { Text(doc.title) }, + text = { Text(doc.file.title) }, onClick = { expanded = false coroutineScope.launch { pagerState.animateScrollToPage(index) } From f97fb01ed0d99610e40d2ffab60c74b88c941574 Mon Sep 17 00:00:00 2001 From: Tinashe Mzondiwa Date: Fri, 10 Apr 2026 21:23:51 -0400 Subject: [PATCH 15/23] Refactor topAppBar to re-use existing composable --- .../document/components/DocumentTopAppBar.kt | 55 ++++++---- .../kotlin/ss/document/di/BindingsModule.kt | 5 + .../producer/TopAppbarActionsProducer.kt | 4 +- .../pdf/PdfTopAppBarStateProducer.kt | 96 +++++++++++++++++ .../document/segment/components/pdf/PdfUi.kt | 100 ++++-------------- .../components/pdf/ReadPdfPresenter.kt | 27 +---- .../segment/components/pdf/ReadPdfState.kt | 12 +-- .../segment/components/pdf/ReadPdfUi.kt | 5 +- .../src/main/res/drawable/ic_more_vert.xml | 9 ++ 9 files changed, 177 insertions(+), 136 deletions(-) create mode 100644 features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfTopAppBarStateProducer.kt create mode 100644 features/document/src/main/res/drawable/ic_more_vert.xml diff --git a/features/document/src/main/kotlin/ss/document/components/DocumentTopAppBar.kt b/features/document/src/main/kotlin/ss/document/components/DocumentTopAppBar.kt index 8f5a8de3d..6ddb979ae 100644 --- a/features/document/src/main/kotlin/ss/document/components/DocumentTopAppBar.kt +++ b/features/document/src/main/kotlin/ss/document/components/DocumentTopAppBar.kt @@ -42,7 +42,6 @@ import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.rounded.MoreVert import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api @@ -52,6 +51,7 @@ import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarColors import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable @@ -73,14 +73,13 @@ import app.ss.design.compose.extensions.haptics.LocalSsHapticFeedback import app.ss.design.compose.theme.SsTheme import app.ss.design.compose.widget.icon.IconBox import app.ss.design.compose.widget.icon.IconButtonResSlot -import app.ss.design.compose.widget.icon.IconButtonSlot import app.ss.design.compose.widget.icon.Icons import io.adventech.blockkit.model.resource.Segment import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import ss.misc.DateHelper -import androidx.compose.material.icons.Icons as MaterialIcons import app.ss.translations.R as L10nR +import com.pspdfkit.R as PspdfR import ss.document.R as DocumentR import ss.libraries.media.resources.R as MediaR @@ -113,6 +112,21 @@ enum class DocumentTopAppBarAction( iconRes = DocumentR.drawable.ic_text_format, title = L10nR.string.ss_settings_display_options, primary = false, + ), + Annotations( + iconRes = DocumentR.drawable.ic_pdf_annotations, + title = L10nR.string.ss_annotations, + primary = true, + ), + Outline( + iconRes = DocumentR.drawable.ic_pdf_bookmark, + title = PspdfR.string.pspdf__activity_menu_outline, + primary = true, + ), + Settings( + iconRes = DocumentR.drawable.ic_pdf_settings, + title = PspdfR.string.pspdf__activity_menu_settings, + primary = true, ) } @@ -126,11 +140,21 @@ internal fun DocumentTopAppBar( contentColor: Color = SsTheme.colors.primaryForeground, scrollBehavior: TopAppBarScrollBehavior? = null, actions: ImmutableList = persistentListOf(), + colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors( + containerColor = Color.Transparent, + scrolledContainerColor = Color.Transparent, + ), onNavBack: () -> Unit = {}, onActionClick: (DocumentTopAppBarAction) -> Unit = {}, ) { val hapticFeedback = LocalSsHapticFeedback.current var expanded by remember { mutableStateOf(false) } + + val primaryActions = remember(actions) { actions.filter { it.primary } } + val nonPrimaryActions = remember(actions) { actions.filter { !it.primary } } + val visibleActions = remember(primaryActions) { primaryActions.take(2) } + val overflowActions = remember(primaryActions, nonPrimaryActions) { primaryActions.drop(2) + nonPrimaryActions } + Box( modifier = Modifier .fillMaxWidth() @@ -144,7 +168,7 @@ internal fun DocumentTopAppBar( shape = RoundedCornerShape(16.dp), containerColor = SsTheme.colors.primaryBackground, ) { - actions.filter { !it.primary }.forEach { action -> + overflowActions.forEach { action -> DropdownMenuItem( text = { Text( @@ -219,7 +243,8 @@ internal fun DocumentTopAppBar( }, actions = { buildList { - actions.filter { it.primary }.forEach { action -> + // Add the visible primary actions (up to 2) + visibleActions.forEach { action -> add( IconButtonResSlot( iconRes = action.iconRes, @@ -228,10 +253,12 @@ internal fun DocumentTopAppBar( ) ) } - if (actions.any { !it.primary }) { + + // Add the "More" icon if there are any secondary or overflowed primary actions + if (overflowActions.isNotEmpty()) { add( - IconButtonSlot( - imageVector = MaterialIcons.Rounded.MoreVert, + IconButtonResSlot( + iconRes = DocumentR.drawable.ic_more_vert, contentDescription = stringResource(L10nR.string.ss_more), onClick = { expanded = true @@ -242,12 +269,7 @@ internal fun DocumentTopAppBar( } }.forEach { icon -> val iconColor by topAppBarContentColor(collapsible, collapsed, contentColor) - val onClick = (icon as? IconButtonSlot)?.onClick ?: (icon as? IconButtonResSlot)?.onClick - IconButton( - onClick = { - onClick?.invoke() - }, - ) { + IconButton(onClick = { icon.onClick() }) { IconBox( icon = icon, contentColor = iconColor, @@ -256,10 +278,7 @@ internal fun DocumentTopAppBar( } }, scrollBehavior = scrollBehavior, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = Color.Transparent, - scrolledContainerColor = Color.Transparent, - ) + colors = colors ) } diff --git a/features/document/src/main/kotlin/ss/document/di/BindingsModule.kt b/features/document/src/main/kotlin/ss/document/di/BindingsModule.kt index 133f81bbe..0acaf16ab 100644 --- a/features/document/src/main/kotlin/ss/document/di/BindingsModule.kt +++ b/features/document/src/main/kotlin/ss/document/di/BindingsModule.kt @@ -34,6 +34,8 @@ import ss.document.producer.TopAppbarActionsProducer import ss.document.producer.TopAppbarActionsProducerImpl import ss.document.producer.UserInputStateProducer import ss.document.producer.UserInputStateProducerImpl +import ss.document.segment.components.pdf.PdfTopAppBarStateProducer +import ss.document.segment.components.pdf.PdfTopAppBarStateProducerImpl import ss.document.segment.producer.OverlayStateProducerImpl import ss.document.segment.producer.SegmentOverlayStateProducer @@ -54,4 +56,7 @@ internal abstract class BindingsModule { @Binds internal abstract fun bindUserInputStateProducer(impl: UserInputStateProducerImpl): UserInputStateProducer + + @Binds + internal abstract fun bindPdfTopAppBarStateProducer(impl: PdfTopAppBarStateProducerImpl): PdfTopAppBarStateProducer } diff --git a/features/document/src/main/kotlin/ss/document/producer/TopAppbarActionsProducer.kt b/features/document/src/main/kotlin/ss/document/producer/TopAppbarActionsProducer.kt index 29c730bc5..6f3664900 100644 --- a/features/document/src/main/kotlin/ss/document/producer/TopAppbarActionsProducer.kt +++ b/features/document/src/main/kotlin/ss/document/producer/TopAppbarActionsProducer.kt @@ -27,7 +27,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import app.ss.models.PDFAux import com.slack.circuit.retained.produceRetainedState @@ -115,7 +114,7 @@ internal class TopAppbarActionsProducerImpl @Inject constructor( if (segment?.type == SegmentType.PDF) return@produceRetainedState value = repository.pdf(resourceIndex, documentIndex).getOrNull().orEmpty() } - val actions = remember(audio, video, pdfs, segment, shareOptions) { + val actions = rememberRetained(audio, video, pdfs, segment, shareOptions) { buildList { if (audio.isNotEmpty()) { add(DocumentTopAppBarAction.Audio) @@ -214,6 +213,7 @@ internal class TopAppbarActionsProducerImpl @Inject constructor( } } } + else -> Unit } } } diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfTopAppBarStateProducer.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfTopAppBarStateProducer.kt new file mode 100644 index 000000000..39c03290a --- /dev/null +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfTopAppBarStateProducer.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2026. 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.document.segment.components.pdf + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import app.ss.models.media.MediaAvailability +import com.slack.circuit.retained.produceRetainedState +import com.slack.circuit.runtime.CircuitUiState +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import ss.document.components.DocumentTopAppBarAction +import ss.libraries.circuit.navigation.PdfScreen +import ss.resources.api.ResourcesRepository +import javax.inject.Inject + +@Stable +interface PdfTopAppBarStateProducer { + @Composable + operator fun invoke(screen: PdfScreen): PdfTopAppBarState +} + +@Immutable +data class PdfTopAppBarState( + val actions: ImmutableList, +): CircuitUiState + +class PdfTopAppBarStateProducerImpl @Inject constructor( + private val resourcesRepository: ResourcesRepository, +) : PdfTopAppBarStateProducer { + @Composable + override fun invoke(screen: PdfScreen): PdfTopAppBarState { + val mediaAvailability by rememberMediaAvailability(screen) + + val actions by produceRetainedState(persistentListOf(), mediaAvailability) { + value = buildList { + if (mediaAvailability.audio) { + add(DocumentTopAppBarAction.Audio) + } + if (mediaAvailability.video) { + add(DocumentTopAppBarAction.Video) + } + add(DocumentTopAppBarAction.Annotations) + add(DocumentTopAppBarAction.Outline) + add(DocumentTopAppBarAction.Settings) + }.toImmutableList() + } + + return PdfTopAppBarState(actions) + } + + @Composable + private fun rememberMediaAvailability(screen: PdfScreen): State = produceRetainedState(MediaAvailability()) { + val documentIndex = screen.documentIndex + val resourceIndex = screen.resourceIndex + + val audioDeferred = contentDeferred { resourcesRepository.audio(resourceIndex, documentIndex) } + val videoDeferred = contentDeferred { resourcesRepository.video(resourceIndex, documentIndex) } + + value = MediaAvailability( + audio = audioDeferred.await(), + video = videoDeferred.await(), + ) + } + + private fun CoroutineScope.contentDeferred(content: suspend () -> Result>): Deferred { + return async { content().getOrDefault(emptyList()).isNotEmpty() } + } +} diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt index f1913f7d9..b013d62c6 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt @@ -27,13 +27,8 @@ import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -45,13 +40,10 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onVisibilityChanged import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentManager import app.ss.design.compose.widget.scaffold.LocalNavbarController -import app.ss.models.media.MediaAvailability import com.pspdfkit.annotations.AnnotationType import com.pspdfkit.configuration.activity.PdfActivityConfiguration import com.pspdfkit.configuration.activity.ThumbnailBarMode @@ -61,6 +53,7 @@ import com.pspdfkit.configuration.settings.SettingsMenuItemType import com.pspdfkit.configuration.sharing.ShareFeatures import com.pspdfkit.document.PdfDocument import com.pspdfkit.jetpack.compose.interactors.DocumentListener +import com.pspdfkit.jetpack.compose.interactors.DocumentState import com.pspdfkit.jetpack.compose.interactors.getDefaultDocumentManager import com.pspdfkit.jetpack.compose.interactors.rememberDocumentState import com.pspdfkit.jetpack.compose.views.DocumentView @@ -70,17 +63,16 @@ import io.adventech.blockkit.ui.style.background import io.adventech.blockkit.ui.style.primaryForeground import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.launch +import ss.document.components.DocumentTopAppBar import ss.document.components.DocumentTopAppBarAction import ss.libraries.pdf.api.LocalFile import java.util.EnumSet -import app.ss.translations.R as L10nR import com.pspdfkit.R as PspdfR -import ss.document.R as DocumentR @Composable fun PdfUi( document: PdfDocumentState, - mediaAvailability: MediaAvailability, + topAppBarState: PdfTopAppBarState, config: PdfReaderConfig, modifier: Modifier = Modifier, title: @Composable () -> Unit = { Text(document.file.title) }, @@ -99,10 +91,8 @@ fun PdfUi( Column(modifier = modifier.fillMaxSize()) { PdfTopAppBar( title = title, - state = PdfTopAppBarState( - mediaAvailability = mediaAvailability, - documentState = documentState, - ), + state = topAppBarState, + documentState = documentState, eventSink = eventSink, ) @@ -164,69 +154,33 @@ private suspend fun PdfDocument.loadAnnotations(annotations: ImmutableList Unit, state: PdfTopAppBarState, + documentState: DocumentState, eventSink: (ReadPdfEvent) -> Unit, modifier: Modifier = Modifier, ) { val readerTheme = LocalReaderStyle.current.theme - val (mediaAvailability, documentState) = state - TopAppBar( + DocumentTopAppBar( title = title, modifier = modifier, - navigationIcon = { - IconButton(onClick = { eventSink(ReadPdfEvent.OnNavBack) }) { - Icon( - imageVector = Icons.AutoMirrored.Rounded.ArrowBack, - contentDescription = stringResource(L10nR.string.ss_action_back), - ) - } - }, - actions = { - if (mediaAvailability.audio) { - val action = DocumentTopAppBarAction.Audio - MediaIcon( - action = action, - onClick = { eventSink(ReadPdfEvent.OnTopAppBarAction(action)) }, - ) - } - if (mediaAvailability.video) { - val action = DocumentTopAppBarAction.Video - MediaIcon( - action = action, - onClick = { eventSink(ReadPdfEvent.OnTopAppBarAction(action)) }, - ) - } - IconButton(onClick = { - documentState.toggleView(PspdfR.id.pspdf__menu_option_edit_annotations) - }) { - Icon( - painter = painterResource(DocumentR.drawable.ic_pdf_annotations), - contentDescription = stringResource(L10nR.string.ss_annotations), - ) - } - IconButton(onClick = { - documentState.toggleView(PspdfR.id.pspdf__menu_option_outline) - }) { - Icon( - painter = painterResource(DocumentR.drawable.ic_pdf_bookmark), - contentDescription = stringResource(PspdfR.string.pspdf__activity_menu_outline), - ) - } - IconButton(onClick = { - documentState.toggleView(PspdfR.id.pspdf__menu_option_settings) - }) { - Icon( - painter = painterResource(DocumentR.drawable.ic_pdf_settings), - contentDescription = stringResource(PspdfR.string.pspdf__activity_menu_settings), - ) - } - }, + collapsed = true, + contentColor = readerTheme.primaryForeground(), + actions = state.actions, colors = TopAppBarDefaults.topAppBarColors( containerColor = readerTheme.background(), navigationIconContentColor = readerTheme.primaryForeground(), actionIconContentColor = readerTheme.primaryForeground(), titleContentColor = readerTheme.primaryForeground(), - ) + ), + onNavBack = { eventSink(ReadPdfEvent.OnNavBack) }, + onActionClick = { action -> + when (action) { + DocumentTopAppBarAction.Annotations -> documentState.toggleView(PspdfR.id.pspdf__menu_option_edit_annotations) + DocumentTopAppBarAction.Outline -> documentState.toggleView(PspdfR.id.pspdf__menu_option_outline) + DocumentTopAppBarAction.Settings -> documentState.toggleView(PspdfR.id.pspdf__menu_option_settings) + else -> eventSink(ReadPdfEvent.OnTopAppBarAction(action)) + } + } ) } @@ -278,17 +232,3 @@ private fun FragmentManager.findPdfFragment(): com.pspdfkit.ui.PdfFragment? { } return null } - -@Composable -private fun MediaIcon( - action: DocumentTopAppBarAction, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - IconButton(onClick = onClick, modifier = modifier) { - Icon( - painter = painterResource(action.iconRes), - contentDescription = stringResource(action.title), - ) - } -} diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt index 773560a2e..6be665bf1 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt @@ -27,7 +27,6 @@ import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import app.ss.models.media.MediaAvailability import com.pspdfkit.annotations.Annotation import com.slack.circuit.codegen.annotations.CircuitInject import com.slack.circuit.foundation.NavEvent @@ -46,9 +45,6 @@ import io.adventech.blockkit.model.input.UserInputRequest import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.async import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map @@ -70,6 +66,7 @@ class ReadPdfPresenter @AssistedInject constructor( private val pdfReader: PdfReader, private val pdfReaderPrefs: PdfReaderPrefs, private val resourcesRepository: ResourcesRepository, + private val topAppBarStateProducer: PdfTopAppBarStateProducer, private val dispatcherProvider: DispatcherProvider, ) : Presenter { @@ -77,7 +74,7 @@ class ReadPdfPresenter @AssistedInject constructor( override fun present(): ReadPdfState { val documents by rememberFiles() val annotationsMap by rememberDocumentAnnotations() - val mediaAvailability by rememberMediaAvailability() + val topAppBarState = topAppBarStateProducer(screen) val config by rememberPdfReaderConfig() var overlayState by rememberRetained { mutableStateOf(ReadPdfOverlayState.None) } val documentsState = rememberRetained(documents, annotationsMap) { @@ -110,7 +107,7 @@ class ReadPdfPresenter @AssistedInject constructor( return when { documents.isNotEmpty() -> ReadPdfState.Success( documents = documentsState, - mediaAvailability = mediaAvailability, + topAppBarState = topAppBarState, config = config, overlayState = overlayState, eventSink = { event -> @@ -161,24 +158,6 @@ class ReadPdfPresenter @AssistedInject constructor( } } - @Composable - private fun rememberMediaAvailability(): State = produceRetainedState(MediaAvailability()) { - val documentIndex = screen.documentIndex - val resourceIndex = screen.resourceIndex - - val audioDeferred = contentDeferred { resourcesRepository.audio(resourceIndex, documentIndex) } - val videoDeferred = contentDeferred { resourcesRepository.video(resourceIndex, documentIndex) } - - value = MediaAvailability( - audio = audioDeferred.await(), - video = videoDeferred.await(), - ) - } - - private fun CoroutineScope.contentDeferred(content: suspend () -> Result>): Deferred { - return async { content().getOrDefault(emptyList()).isNotEmpty() } - } - @Composable private fun rememberPdfReaderConfig(): State = rememberRetained { mutableStateOf( diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt index de8d87631..75b861973 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt @@ -24,13 +24,12 @@ package ss.document.segment.components.pdf import android.content.Context import androidx.compose.runtime.Immutable -import app.ss.models.media.MediaAvailability +import com.pspdfkit.annotations.Annotation import com.pspdfkit.configuration.PdfConfiguration import com.pspdfkit.configuration.page.PageLayoutMode import com.pspdfkit.configuration.page.PageScrollDirection import com.pspdfkit.configuration.page.PageScrollMode import com.pspdfkit.configuration.theming.ThemeMode -import com.pspdfkit.jetpack.compose.interactors.DocumentState import com.slack.circuit.foundation.NavEvent import com.slack.circuit.runtime.CircuitUiState import com.slack.circuit.runtime.screen.Screen @@ -39,15 +38,14 @@ import kotlinx.collections.immutable.ImmutableList import ss.document.components.DocumentTopAppBarAction import ss.libraries.circuit.overlay.BottomSheetOverlay import ss.libraries.pdf.api.LocalFile -import com.pspdfkit.annotations.Annotation sealed interface ReadPdfState : CircuitUiState { data object Loading : ReadPdfState data class Success( val documents: ImmutableList, - val mediaAvailability: MediaAvailability, val config: PdfReaderConfig, + val topAppBarState: PdfTopAppBarState, val overlayState: ReadPdfOverlayState, val eventSink: (ReadPdfEvent) -> Unit, ) : ReadPdfState @@ -84,12 +82,6 @@ data class PdfDocumentState( val annotations: ImmutableList, ) -@Immutable -data class PdfTopAppBarState( - val mediaAvailability: MediaAvailability, - val documentState: DocumentState, -) - @Immutable data class PdfReaderConfig( val scrollMode: PageScrollMode, diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt index a318f9948..6513b7931 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfUi.kt @@ -93,14 +93,14 @@ private fun ReadPdfSuccessUi(state: ReadPdfState.Success, modifier: Modifier = M state = pagerState, modifier = modifier.fillMaxSize(), verticalAlignment = Alignment.Top, - beyondViewportPageCount = 2, + beyondViewportPageCount = 0, ) { page -> val document = state.documents[page] var expanded by remember { mutableStateOf(false) } PdfUi( document = document, - mediaAvailability = state.mediaAvailability, + topAppBarState = state.topAppBarState, config = state.config, modifier = Modifier, title = { @@ -124,6 +124,7 @@ private fun ReadPdfSuccessUi(state: ReadPdfState.Success, modifier: Modifier = M ) { Text( text = document.file.title, + modifier = Modifier.weight(1f), maxLines = 1, overflow = TextOverflow.Ellipsis, ) diff --git a/features/document/src/main/res/drawable/ic_more_vert.xml b/features/document/src/main/res/drawable/ic_more_vert.xml new file mode 100644 index 000000000..966e8e94b --- /dev/null +++ b/features/document/src/main/res/drawable/ic_more_vert.xml @@ -0,0 +1,9 @@ + + + From 9628d59b4f95e1d4f059d150d86303294d1231be Mon Sep 17 00:00:00 2001 From: Tinashe Mzondiwa Date: Fri, 10 Apr 2026 21:34:58 -0400 Subject: [PATCH 16/23] rename --- .../src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt | 2 +- .../ss/document/segment/components/pdf/ReadPdfPresenter.kt | 2 +- .../kotlin/ss/document/segment/components/pdf/ReadPdfState.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt index b013d62c6..bf19a354d 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt @@ -110,7 +110,7 @@ fun PdfUi( val annotations = pdfFragment?.document?.annotationProvider?.getAllAnnotationsOfType(allowedAnnotations.toSet()) pdfFragment?.let { eventSink( - ReadPdfEvent.OnConfigurationChanged( + ReadPdfEvent.OnDocumentHidden( config = it.configuration, annotations = annotations, pdfId = document.pdfId, diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt index 6be665bf1..4f862ba41 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt @@ -136,7 +136,7 @@ class ReadPdfPresenter @AssistedInject constructor( } } - is ReadPdfEvent.OnConfigurationChanged -> { + is ReadPdfEvent.OnDocumentHidden -> { pdfReaderPrefs.saveConfiguration(event.config) event.annotations?.let { saveAnnotations(pdfId = event.pdfId, annotations = it) } diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt index 75b861973..abd9beef7 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfState.kt @@ -55,7 +55,7 @@ sealed interface ReadPdfEvent { data object OnNavBack : ReadPdfEvent data class OnNavEvent(val event: NavEvent, val context: Context) : ReadPdfEvent data class OnTopAppBarAction(val action: DocumentTopAppBarAction): ReadPdfEvent - data class OnConfigurationChanged( + data class OnDocumentHidden( val config: PdfConfiguration, val annotations: List?, val pdfId: String, From 7072401527ac1dc4889dc01261f4c5b7f449ce59 Mon Sep 17 00:00:00 2001 From: Tinashe Mzondiwa Date: Fri, 10 Apr 2026 21:57:48 -0400 Subject: [PATCH 17/23] Fix config state --- .../components/pdf/ReadPdfPresenter.kt | 33 +++-- features/pdf/build.gradle.kts | 3 +- .../main/kotlin/app/ss/pdf/PdfReaderImpl.kt | 8 +- .../kotlin/app/ss/pdf/PdfReaderPrefsImpl.kt | 116 ++++++++++++------ .../kotlin/app/ss/pdf/ui/SSReadPdfActivity.kt | 6 +- .../ss/libraries/pdf/api/PdfReaderPrefs.kt | 9 +- 6 files changed, 119 insertions(+), 56 deletions(-) diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt index 4f862ba41..4517d7deb 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/ReadPdfPresenter.kt @@ -28,6 +28,10 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.pspdfkit.annotations.Annotation +import com.pspdfkit.configuration.page.PageLayoutMode +import com.pspdfkit.configuration.page.PageScrollDirection +import com.pspdfkit.configuration.page.PageScrollMode +import com.pspdfkit.configuration.theming.ThemeMode import com.slack.circuit.codegen.annotations.CircuitInject import com.slack.circuit.foundation.NavEvent import com.slack.circuit.foundation.onNavEvent @@ -46,6 +50,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import ss.document.components.DocumentTopAppBarAction @@ -77,7 +82,7 @@ class ReadPdfPresenter @AssistedInject constructor( val topAppBarState = topAppBarStateProducer(screen) val config by rememberPdfReaderConfig() var overlayState by rememberRetained { mutableStateOf(ReadPdfOverlayState.None) } - val documentsState = rememberRetained(documents, annotationsMap) { + val documentsState = rememberRetained(documents, annotationsMap, config) { documents.mapIndexed { index, file -> val pdfId = screen.pdfs.getOrNull(index)?.id.orEmpty() PdfDocumentState( @@ -159,15 +164,27 @@ class ReadPdfPresenter @AssistedInject constructor( } @Composable - private fun rememberPdfReaderConfig(): State = rememberRetained { - mutableStateOf( + private fun rememberPdfReaderConfig(): State = produceRetainedState( + PdfReaderConfig( + scrollMode = PageScrollMode.CONTINUOUS, + layoutMode = PageLayoutMode.SINGLE, + scrollDirection = PageScrollDirection.VERTICAL, + themeMode = ThemeMode.DEFAULT, + ) + ) { + combine( + pdfReaderPrefs.scrollMode(), + pdfReaderPrefs.pageLayoutMode(), + pdfReaderPrefs.scrollDirection(), + pdfReaderPrefs.themeMode() + ) { scrollMode, layoutMode, scrollDirection, themeMode -> PdfReaderConfig( - scrollMode = pdfReaderPrefs.scrollMode(), - layoutMode = pdfReaderPrefs.pageLayoutMode(), - scrollDirection = pdfReaderPrefs.scrollDirection(), - themeMode = pdfReaderPrefs.themeMode(), + scrollMode = scrollMode, + layoutMode = layoutMode, + scrollDirection = scrollDirection, + themeMode = themeMode, ) - ) + }.collect { value = it } } @Composable diff --git a/features/pdf/build.gradle.kts b/features/pdf/build.gradle.kts index a1f2bf526..0f5750127 100644 --- a/features/pdf/build.gradle.kts +++ b/features/pdf/build.gradle.kts @@ -58,6 +58,7 @@ dependencies { implementation(libs.androidx.activity) implementation(libs.androidx.appcompat) implementation(libs.androidx.core) + implementation(libs.androidx.datastore.prefs) implementation(libs.androidx.lifecycle.viewmodel) implementation(libs.androidx.preference) implementation(libs.google.hilt.android) @@ -95,7 +96,7 @@ fun Project.readPropertyValue( val keyProps = Properties().apply { load(FileInputStream(file)) } - return keyProps.getProperty(key, defaultValue) + keyProps.getProperty(key, defaultValue) } else { defaultValue } 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 3f300c30b..c5a409d88 100644 --- a/features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderImpl.kt +++ b/features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderImpl.kt @@ -91,10 +91,10 @@ internal class PdfReaderImpl @Inject constructor( .setThumbnailBarMode(ThumbnailBarMode.THUMBNAIL_BAR_MODE_NONE) .setSettingsMenuItems(EnumSet.allOf(SettingsMenuItemType::class.java)) .setTabBarHidingMode(TabBarHidingMode.AUTOMATIC) - .scrollMode(readerPrefs.scrollMode()) - .scrollDirection(readerPrefs.scrollDirection()) - .layoutMode(readerPrefs.pageLayoutMode()) - .themeMode(readerPrefs.themeMode()) +// .scrollMode(readerPrefs.scrollMode()) +// .scrollDirection(readerPrefs.scrollDirection()) +// .layoutMode(readerPrefs.pageLayoutMode()) +// .themeMode(readerPrefs.themeMode()) .build() return PdfActivityIntentBuilder.emptyActivity(context) diff --git a/features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderPrefsImpl.kt b/features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderPrefsImpl.kt index 82fb9e837..8b94bde00 100644 --- a/features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderPrefsImpl.kt +++ b/features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderPrefsImpl.kt @@ -25,6 +25,12 @@ package app.ss.pdf import android.content.Context import android.content.SharedPreferences import androidx.core.content.edit +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.SharedPreferencesMigration +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore import androidx.preference.PreferenceManager import com.pspdfkit.configuration.PdfConfiguration import com.pspdfkit.configuration.page.PageLayoutMode @@ -32,58 +38,103 @@ import com.pspdfkit.configuration.page.PageScrollDirection import com.pspdfkit.configuration.page.PageScrollMode import com.pspdfkit.configuration.theming.ThemeMode import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import ss.foundation.coroutines.DispatcherProvider +import ss.foundation.coroutines.Scopable +import ss.foundation.coroutines.ioScopable import ss.libraries.pdf.api.PdfReaderPrefs import javax.inject.Inject import javax.inject.Singleton +internal const val KEY_LAYOUT_MODE = "key:layout_mode" +internal const val KEY_SCROLL_MODE = "key:scroll_mode" +internal const val KEY_SCROLL_DIRECTION = "key:scroll_direction" +internal const val KEY_THEME_MODE = "key:theme_mode" + +private val Context.pdfDataStore: DataStore by preferencesDataStore( + name = "pdf_prefs", + produceMigrations = { + listOf( + SharedPreferencesMigration( + it, + "${it.packageName}_preferences", + setOf( + KEY_LAYOUT_MODE, + KEY_SCROLL_MODE, + KEY_SCROLL_DIRECTION, + KEY_THEME_MODE, + ) + ) + ) + } +) + @Singleton -internal class PdfReaderPrefsImpl @Inject constructor( - @param:ApplicationContext private val context: Context, -) : PdfReaderPrefs { - private val sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) - - override fun scrollMode(): PageScrollMode = - sharedPreferences.getString(KEY_SCROLL_MODE, null)?.let { - PageScrollMode.valueOf(it) +internal class PdfReaderPrefsImpl( + private val dataStore: DataStore, + private val sharedPreferences: SharedPreferences, + dispatcherProvider: DispatcherProvider +) : PdfReaderPrefs, Scopable by ioScopable(dispatcherProvider) { + + @Inject + constructor( + @ApplicationContext context: Context, + dispatcherProvider: DispatcherProvider, + ) : this( + dataStore = context.pdfDataStore, + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context), + dispatcherProvider = dispatcherProvider + ) + + private val scrollModeKey = stringPreferencesKey(KEY_SCROLL_MODE) + private val layoutModeKey = stringPreferencesKey(KEY_LAYOUT_MODE) + private val scrollDirectionKey = stringPreferencesKey(KEY_SCROLL_DIRECTION) + private val themeModeKey = stringPreferencesKey(KEY_THEME_MODE) + + override fun scrollMode(): Flow = dataStore.data.map { preferences -> + preferences[scrollModeKey]?.let { + runCatching { PageScrollMode.valueOf(it) }.getOrNull() } ?: PageScrollMode.CONTINUOUS + } override fun setScrollMode(mode: PageScrollMode) { - sharedPreferences.edit { - putString(KEY_SCROLL_MODE, mode.name) - } + sharedPreferences.edit { putString(KEY_SCROLL_MODE, mode.name) } + scope.launch { dataStore.edit { it[scrollModeKey] = mode.name } } } - override fun pageLayoutMode(): PageLayoutMode = - sharedPreferences.getString(KEY_LAYOUT_MODE, null)?.let { - PageLayoutMode.valueOf(it) + override fun pageLayoutMode(): Flow = dataStore.data.map { preferences -> + preferences[layoutModeKey]?.let { + runCatching { PageLayoutMode.valueOf(it) }.getOrNull() } ?: PageLayoutMode.SINGLE + } override fun setPageLayoutMode(mode: PageLayoutMode) { - sharedPreferences.edit { - putString(KEY_LAYOUT_MODE, mode.name) - } + sharedPreferences.edit { putString(KEY_LAYOUT_MODE, mode.name) } + scope.launch { dataStore.edit { it[layoutModeKey] = mode.name } } } - override fun scrollDirection(): PageScrollDirection = - sharedPreferences.getString(KEY_SCROLL_DIRECTION, null)?.let { - PageScrollDirection.valueOf(it) + override fun scrollDirection(): Flow = dataStore.data.map { preferences -> + preferences[scrollDirectionKey]?.let { + runCatching { PageScrollDirection.valueOf(it) }.getOrNull() } ?: PageScrollDirection.VERTICAL + } override fun setScrollDirection(direction: PageScrollDirection) { - sharedPreferences.edit { - putString(KEY_SCROLL_DIRECTION, direction.name) - } + sharedPreferences.edit { putString(KEY_SCROLL_DIRECTION, direction.name) } + scope.launch { dataStore.edit { it[scrollDirectionKey] = direction.name } } } - override fun themeMode(): ThemeMode = - sharedPreferences.getString(KEY_THEME_MODE, null)?.let { - ThemeMode.valueOf(it) + override fun themeMode(): Flow = dataStore.data.map { preferences -> + preferences[themeModeKey]?.let { + runCatching { ThemeMode.valueOf(it) }.getOrNull() } ?: ThemeMode.DEFAULT + } override fun setThemeMode(mode: ThemeMode) { - sharedPreferences.edit { - putString(KEY_THEME_MODE, mode.name) - } + sharedPreferences.edit { putString(KEY_THEME_MODE, mode.name) } + scope.launch { dataStore.edit { it[themeModeKey] = mode.name } } } override fun saveConfiguration(configuration: PdfConfiguration) { @@ -92,11 +143,4 @@ internal class PdfReaderPrefsImpl @Inject constructor( setScrollDirection(configuration.scrollDirection) setThemeMode(configuration.themeMode) } - - companion object { - private const val KEY_LAYOUT_MODE = "key:layout_mode" - private const val KEY_SCROLL_MODE = "key:scroll_mode" - private const val KEY_SCROLL_DIRECTION = "key:scroll_direction" - private const val KEY_THEME_MODE = "key:theme_mode" - } } diff --git a/features/pdf/src/main/kotlin/app/ss/pdf/ui/SSReadPdfActivity.kt b/features/pdf/src/main/kotlin/app/ss/pdf/ui/SSReadPdfActivity.kt index 363e5031e..1979dde4f 100644 --- a/features/pdf/src/main/kotlin/app/ss/pdf/ui/SSReadPdfActivity.kt +++ b/features/pdf/src/main/kotlin/app/ss/pdf/ui/SSReadPdfActivity.kt @@ -48,8 +48,8 @@ import ss.libraries.media.resources.R as MediaR @AndroidEntryPoint class SSReadPdfActivity : PdfActivity() { - @Inject - lateinit var readerPrefs: PdfReaderPrefs +// @Inject +// lateinit var readerPrefs: PdfReaderPrefs private val viewModel by viewModels() @@ -171,7 +171,7 @@ class SSReadPdfActivity : PdfActivity() { documents.forEachIndexed { index, pdfDocument -> // viewModel.saveAnnotations(pdfDocument, index) } - readerPrefs.saveConfiguration(configuration.configuration) + // readerPrefs.saveConfiguration(configuration.configuration) super.onStop() } diff --git a/libraries/pdf/api/src/main/kotlin/ss/libraries/pdf/api/PdfReaderPrefs.kt b/libraries/pdf/api/src/main/kotlin/ss/libraries/pdf/api/PdfReaderPrefs.kt index dd98a12c0..1c9bfeee4 100644 --- a/libraries/pdf/api/src/main/kotlin/ss/libraries/pdf/api/PdfReaderPrefs.kt +++ b/libraries/pdf/api/src/main/kotlin/ss/libraries/pdf/api/PdfReaderPrefs.kt @@ -27,15 +27,16 @@ import com.pspdfkit.configuration.page.PageLayoutMode import com.pspdfkit.configuration.page.PageScrollDirection import com.pspdfkit.configuration.page.PageScrollMode import com.pspdfkit.configuration.theming.ThemeMode +import kotlinx.coroutines.flow.Flow interface PdfReaderPrefs { - fun scrollMode(): PageScrollMode + fun scrollMode(): Flow fun setScrollMode(mode: PageScrollMode) - fun pageLayoutMode(): PageLayoutMode + fun pageLayoutMode(): Flow fun setPageLayoutMode(mode: PageLayoutMode) - fun scrollDirection(): PageScrollDirection + fun scrollDirection(): Flow fun setScrollDirection(direction: PageScrollDirection) - fun themeMode(): ThemeMode + fun themeMode(): Flow fun setThemeMode(mode: ThemeMode) fun saveConfiguration(configuration: PdfConfiguration) } From 8c258504a911360d5c52c8236df7ec8c75978d49 Mon Sep 17 00:00:00 2001 From: Tinashe Mzondiwa Date: Fri, 10 Apr 2026 22:08:08 -0400 Subject: [PATCH 18/23] Remove unused pdf code --- features/pdf/build.gradle.kts | 14 +- features/pdf/src/main/AndroidManifest.xml | 4 - .../main/kotlin/app/ss/pdf/PdfReaderImpl.kt | 58 ------ .../app/ss/pdf/model/PdfDocumentSpec.kt | 33 ---- .../kotlin/app/ss/pdf/ui/PdfDocumentView.kt | 42 ---- .../kotlin/app/ss/pdf/ui/ReadPdfViewModel.kt | 151 -------------- .../kotlin/app/ss/pdf/ui/SSReadPdfActivity.kt | 184 ------------------ .../main/res/drawable/ic_pdf_annotations.xml | 10 - .../src/main/res/drawable/ic_pdf_bookmark.xml | 32 --- .../src/main/res/drawable/ic_pdf_settings.xml | 32 --- .../kotlin/ss/libraries/pdf/api/PdfReader.kt | 5 - 11 files changed, 1 insertion(+), 564 deletions(-) delete mode 100644 features/pdf/src/main/kotlin/app/ss/pdf/model/PdfDocumentSpec.kt delete mode 100644 features/pdf/src/main/kotlin/app/ss/pdf/ui/PdfDocumentView.kt delete mode 100644 features/pdf/src/main/kotlin/app/ss/pdf/ui/ReadPdfViewModel.kt delete mode 100644 features/pdf/src/main/kotlin/app/ss/pdf/ui/SSReadPdfActivity.kt delete mode 100644 features/pdf/src/main/res/drawable/ic_pdf_annotations.xml delete mode 100644 features/pdf/src/main/res/drawable/ic_pdf_bookmark.xml delete mode 100644 features/pdf/src/main/res/drawable/ic_pdf_settings.xml diff --git a/features/pdf/build.gradle.kts b/features/pdf/build.gradle.kts index 0f5750127..3cf626753 100644 --- a/features/pdf/build.gradle.kts +++ b/features/pdf/build.gradle.kts @@ -59,25 +59,13 @@ dependencies { implementation(libs.androidx.appcompat) implementation(libs.androidx.core) implementation(libs.androidx.datastore.prefs) - implementation(libs.androidx.lifecycle.viewmodel) implementation(libs.androidx.preference) implementation(libs.google.hilt.android) - implementation(libs.google.material) implementation(libs.nutrient) implementation(libs.timber) - implementation(projects.common.core) - implementation(projects.common.design) - implementation(projects.common.designCompose) - implementation(projects.common.misc) - implementation(projects.common.translations) - implementation(projects.features.media) - implementation(projects.libraries.blockKit.ui) - implementation(projects.libraries.circuit.api) + implementation(projects.common.models) implementation(projects.libraries.foundation.coroutines) - implementation(projects.libraries.lessons.api) - implementation(projects.libraries.media.resources) implementation(projects.libraries.pdf.api) - implementation(projects.services.resources.api) ksp(libs.circuit.codegen) ksp(libs.google.hilt.compiler) diff --git a/features/pdf/src/main/AndroidManifest.xml b/features/pdf/src/main/AndroidManifest.xml index 07f2758de..b1f96d57c 100644 --- a/features/pdf/src/main/AndroidManifest.xml +++ b/features/pdf/src/main/AndroidManifest.xml @@ -23,10 +23,6 @@ - - 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 c5a409d88..8ab713f37 100644 --- a/features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderImpl.kt +++ b/features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderImpl.kt @@ -23,33 +23,19 @@ package app.ss.pdf import android.content.Context -import android.content.Intent import android.net.Uri import app.ss.models.LessonPdf import app.ss.models.PDFAux -import app.ss.pdf.ui.ARG_PDF_SCREEN -import app.ss.pdf.ui.SSReadPdfActivity -import com.pspdfkit.annotations.AnnotationType -import com.pspdfkit.configuration.activity.PdfActivityConfiguration -import com.pspdfkit.configuration.activity.TabBarHidingMode -import com.pspdfkit.configuration.activity.ThumbnailBarMode -import com.pspdfkit.configuration.page.PageFitMode -import com.pspdfkit.configuration.settings.SettingsMenuItemType -import com.pspdfkit.configuration.sharing.ShareFeatures import com.pspdfkit.document.download.DownloadJob import com.pspdfkit.document.download.DownloadRequest -import com.pspdfkit.ui.PdfActivityIntentBuilder import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import ss.foundation.coroutines.DispatcherProvider -import ss.libraries.circuit.navigation.PdfScreen import ss.libraries.pdf.api.LocalFile import ss.libraries.pdf.api.PdfReader -import ss.libraries.pdf.api.PdfReaderPrefs import timber.log.Timber import java.io.File -import java.util.EnumSet import javax.inject.Inject import javax.inject.Singleton import kotlin.coroutines.resume @@ -57,53 +43,9 @@ import kotlin.coroutines.resume @Singleton internal class PdfReaderImpl @Inject constructor( @param:ApplicationContext private val context: Context, - private val readerPrefs: PdfReaderPrefs, private val dispatcherProvider: DispatcherProvider ) : PdfReader, DownloadJob.ProgressListenerAdapter() { - private val allowedAnnotations = listOf( - AnnotationType.HIGHLIGHT, - AnnotationType.INK, - AnnotationType.NOTE, - AnnotationType.WATERMARK, - AnnotationType.STRIKEOUT, - AnnotationType.FREETEXT - ) - - override fun launchIntent(screen: PdfScreen): Intent { - val excludedAnnotationTypes = ArrayList(EnumSet.allOf(AnnotationType::class.java)) - allowedAnnotations.forEach { excludedAnnotationTypes.remove(it) } - - @Suppress("DEPRECATION") - val config = PdfActivityConfiguration.Builder(context) - // .hidePageLabels() - // .hideDocumentTitleOverlay() - // .disableDocumentInfoView() - // .hidePageNumberOverlay() - // .hideThumbnailGrid() - // .disableSearch() - // .disablePrinting() - // .disableOutline() - .fitMode(PageFitMode.FIT_TO_WIDTH) - .animateScrollOnEdgeTaps(true) - .excludedAnnotationTypes(excludedAnnotationTypes) - .setEnabledShareFeatures(EnumSet.noneOf(ShareFeatures::class.java)) - .setThumbnailBarMode(ThumbnailBarMode.THUMBNAIL_BAR_MODE_NONE) - .setSettingsMenuItems(EnumSet.allOf(SettingsMenuItemType::class.java)) - .setTabBarHidingMode(TabBarHidingMode.AUTOMATIC) -// .scrollMode(readerPrefs.scrollMode()) -// .scrollDirection(readerPrefs.scrollDirection()) -// .layoutMode(readerPrefs.pageLayoutMode()) -// .themeMode(readerPrefs.themeMode()) - .build() - - return PdfActivityIntentBuilder.emptyActivity(context) - .configuration(config) - .activityClass(SSReadPdfActivity::class.java) - .build() - .apply { putExtra(ARG_PDF_SCREEN, screen) } - } - override suspend fun downloadFiles(pdfs: List): Result> { return try { val files = pdfs.mapNotNull { diff --git a/features/pdf/src/main/kotlin/app/ss/pdf/model/PdfDocumentSpec.kt b/features/pdf/src/main/kotlin/app/ss/pdf/model/PdfDocumentSpec.kt deleted file mode 100644 index 262e45fb3..000000000 --- a/features/pdf/src/main/kotlin/app/ss/pdf/model/PdfDocumentSpec.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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 app.ss.pdf.model - -import androidx.compose.runtime.Immutable -import com.pspdfkit.configuration.activity.PdfActivityConfiguration -import ss.libraries.pdf.api.LocalFile - -@Immutable -data class PdfDocumentSpec( - val pdfActivityConfiguration: PdfActivityConfiguration, - val file: LocalFile, -) diff --git a/features/pdf/src/main/kotlin/app/ss/pdf/ui/PdfDocumentView.kt b/features/pdf/src/main/kotlin/app/ss/pdf/ui/PdfDocumentView.kt deleted file mode 100644 index e710cf87e..000000000 --- a/features/pdf/src/main/kotlin/app/ss/pdf/ui/PdfDocumentView.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 app.ss.pdf.ui - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import app.ss.pdf.model.PdfDocumentSpec -import com.pspdfkit.jetpack.compose.interactors.rememberDocumentState -import com.pspdfkit.jetpack.compose.views.DocumentView - -@Composable -fun PdfDocumentView( - spec: PdfDocumentSpec, - modifier: Modifier = Modifier -) { - val documentState = rememberDocumentState(spec.file.uri, spec.pdfActivityConfiguration) - - DocumentView( - documentState = documentState, - modifier = modifier, - ) -} diff --git a/features/pdf/src/main/kotlin/app/ss/pdf/ui/ReadPdfViewModel.kt b/features/pdf/src/main/kotlin/app/ss/pdf/ui/ReadPdfViewModel.kt deleted file mode 100644 index 734e60655..000000000 --- a/features/pdf/src/main/kotlin/app/ss/pdf/ui/ReadPdfViewModel.kt +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright (c) 2023. 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 app.ss.pdf.ui - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import app.ss.models.media.MediaAvailability -import com.pspdfkit.annotations.Annotation -import com.pspdfkit.document.PdfDocument -import dagger.hilt.android.lifecycle.HiltViewModel -import io.adventech.blockkit.model.input.PDFAuxAnnotations -import io.adventech.blockkit.model.input.UserInput -import io.adventech.blockkit.model.input.UserInputRequest -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import ss.foundation.coroutines.flow.stateIn -import ss.libraries.circuit.navigation.PdfScreen -import ss.libraries.pdf.api.LocalFile -import ss.libraries.pdf.api.PdfReader -import ss.resources.api.ResourcesRepository -import timber.log.Timber -import javax.inject.Inject - -@HiltViewModel -class ReadPdfViewModel @Inject constructor( - private val pdfReader: PdfReader, - private val resourcesRepository: ResourcesRepository, - private val savedStateHandle: SavedStateHandle -) : ViewModel() { - - private val _pdfFiles = MutableStateFlow>(emptyList()) - val pdfsFilesFlow: StateFlow> = _pdfFiles.asStateFlow() - - private val SavedStateHandle.screen: PdfScreen? - get() = get(ARG_PDF_SCREEN) - - val resourceId: String? get() = savedStateHandle.screen?.resourceId - val documentIndex: String? get() = savedStateHandle.screen?.documentIndex - val segmentId: String? get() = savedStateHandle.screen?.segmentId - - private val mediaAvailability = MutableStateFlow(MediaAvailability()) - val mediaAvailabilityFlow = mediaAvailability.asStateFlow() - - @OptIn(ExperimentalCoroutinesApi::class) - val annotationsStateFlow: StateFlow>> = - flowOf(savedStateHandle.screen?.documentId) - .filterNotNull() - .flatMapLatest(resourcesRepository::documentInput) - .map { - it.asSequence() - .mapNotNull { it as? UserInput.Annotation } - .toList() - } - .map { input -> - val pdfs = savedStateHandle.screen?.pdfs.orEmpty() - pdfs.mapIndexed { index, pdf -> - index to input.filter { it.pdfId == pdf.id }.flatMap { it.data } - }.toMap() - } - .catch { Timber.e(it) } - .stateIn(viewModelScope, emptyMap()) - - init { - checkMediaAvailability() - downloadFiles() - } - - private fun checkMediaAvailability() { - val screen = savedStateHandle.screen ?: return - val (_, _, documentIndex, resourceIndex, _) = screen - viewModelScope.launch { - val audioAvailable = resourcesRepository.audio(resourceIndex, documentIndex).getOrNull().orEmpty().isNotEmpty() - val videoAvailable = resourcesRepository.video(resourceIndex, documentIndex).getOrNull().orEmpty().isNotEmpty() - - mediaAvailability.update { MediaAvailability(audioAvailable, videoAvailable) } - } - } - - private fun downloadFiles() = viewModelScope.launch { - val pdfs = savedStateHandle.screen?.pdfs ?: return@launch - val result = pdfReader.downloadFiles(pdfs) - val files = result.getOrDefault(emptyList()) - _pdfFiles.update { files } - } - - suspend fun saveAnnotations(document: PdfDocument, docIndex: Int) { - val pdfs = savedStateHandle.screen?.pdfs ?: return - val documentId = savedStateHandle.screen?.documentId ?: return - val pdfId = pdfs.getOrNull(docIndex)?.id ?: return - - val syncAnnotations = document.annotations().toSync() - - val userInput = UserInputRequest.Annotation( - blockId = pdfId, - pdfId = pdfId, - data = syncAnnotations - ) - - resourcesRepository.saveDocumentInput(documentId, userInput) - } - - private fun List.toSync(): List { - val groupedAnnotations = groupBy { it.pageIndex } - return groupedAnnotations.keys.mapNotNull { pageIndex -> - val list = groupedAnnotations[pageIndex] ?: return@mapNotNull null - val annotations = list.map { it.toInstantJson() }.filter(::invalidInstantJson) - PDFAuxAnnotations(pageIndex, annotations) - } - } - - private fun invalidInstantJson(json: String) = json != "null" -} - -suspend fun PdfDocument.annotations(): List { - val allAnnotations = mutableListOf() - for (i in 0 until pageCount) { - val annotations = annotationProvider.getAnnotations(i) - allAnnotations.addAll(annotations) - } - return allAnnotations -} diff --git a/features/pdf/src/main/kotlin/app/ss/pdf/ui/SSReadPdfActivity.kt b/features/pdf/src/main/kotlin/app/ss/pdf/ui/SSReadPdfActivity.kt deleted file mode 100644 index 1979dde4f..000000000 --- a/features/pdf/src/main/kotlin/app/ss/pdf/ui/SSReadPdfActivity.kt +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright (c) 2023. 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 app.ss.pdf.ui - -import android.os.Bundle -import android.view.Menu -import android.view.MenuItem -import android.view.ViewGroup -import android.widget.ProgressBar -import androidx.activity.viewModels -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import app.ss.media.playback.ui.nowPlaying.showNowPlaying -import app.ss.media.playback.ui.video.showVideoList -import com.pspdfkit.document.DocumentSource -import com.pspdfkit.document.PdfDocument -import com.pspdfkit.ui.DocumentDescriptor -import com.pspdfkit.ui.PdfActivity -import com.pspdfkit.ui.tabs.PdfTabBarCloseMode -import dagger.hilt.android.AndroidEntryPoint -import io.adventech.blockkit.model.input.PDFAuxAnnotations -import ss.foundation.coroutines.flow.collectIn -import ss.libraries.pdf.api.PdfReaderPrefs -import javax.inject.Inject -import app.ss.translations.R as L10n -import ss.libraries.media.resources.R as MediaR - -@AndroidEntryPoint -class SSReadPdfActivity : PdfActivity() { - -// @Inject -// lateinit var readerPrefs: PdfReaderPrefs - - private val viewModel by viewModels() - - private var loadedDocuments: List = emptyList() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - initUi() - - collectData() - } - - override fun onGenerateMenuItemIds(menuItems: MutableList): MutableList { - return menuItems.also { - val media = viewModel.mediaAvailabilityFlow.value - if (media.video) { - it.add(0, ID_VIDEO) - } - if (media.audio) { - it.add(0, ID_AUDIO) - } - } - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - super.onCreateOptionsMenu(menu) - - menu.findItem(ID_AUDIO)?.custom(L10n.string.ss_media_audio, MediaR.drawable.ic_audio_icon) - menu.findItem(ID_VIDEO)?.custom(L10n.string.ss_media_video, MediaR.drawable.ic_video_icon) - - return true - } - - private fun MenuItem.custom( - @StringRes titleRes: Int, - @DrawableRes iconRes: Int - ) { - title = getString(titleRes) - setIcon(iconRes) - setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) - // icon?.tint(ContextCompat.getColor(this@SSReadPdfActivity, R.color.ss_icon_tint)) - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - android.R.id.home -> true.also { finish() } - ID_AUDIO -> true.also { - supportFragmentManager.showNowPlaying(viewModel.resourceId, viewModel.segmentId) - } - ID_VIDEO -> true.also { - viewModel.documentIndex?.let { - supportFragmentManager.showVideoList(it) - } - } - else -> super.onOptionsItemSelected(item) - } - } - - private fun initUi() { - supportActionBar?.setDisplayHomeAsUpEnabled(true) - pspdfKitViews.tabBar?.setCloseMode(PdfTabBarCloseMode.CLOSE_DISABLED) - (pspdfKitViews.emptyView as? ViewGroup)?.apply { - removeAllViews() - addView(ProgressBar(this@SSReadPdfActivity)) - } - } - - private fun collectData() { - viewModel.pdfsFilesFlow.collectIn(this) { files -> - if (documentCoordinator.documents.isNotEmpty()) return@collectIn - - loadedDocuments = files.map { file -> - DocumentDescriptor.fromDocumentSource(DocumentSource(file.uri)).apply { - setTitle(file.title) - } - } - - if (loadedDocuments.isEmpty()) return@collectIn - - loadedDocuments.forEach { documentCoordinator.addDocument(it) } - documentCoordinator.setVisibleDocument(loadedDocuments.first()) - } - - viewModel.annotationsStateFlow.collectIn(this) { annotations -> - annotations.forEach { (index, annotations) -> - val document = documentCoordinator.documents.getOrNull(index)?.document ?: return@forEach - loadAnnotations(document, annotations) - } - } - - viewModel.mediaAvailabilityFlow.collectIn(this) { invalidateOptionsMenu() } - } - - override fun onDocumentLoaded(document: PdfDocument) { - val index = loadedDocuments.indexOfFirst { it.uid == documentCoordinator.visibleDocument?.uid } - if (index >= 0) { - viewModel.annotationsStateFlow.value[index]?.let { annotations -> - loadAnnotations(document, annotations) - } - } - } - - private fun loadAnnotations(document: PdfDocument, annotations: List) { - if (annotations.isEmpty()) return - -// with(document.annotationProvider) { -// document.annotations() -// .forEach { removeAnnotationFromPage(it) } -// -// annotations -// .flatMap { it.annotations } -// .forEach { createAnnotationFromInstantJson(it) } -// } - } - - override fun onStop() { - val documents = loadedDocuments.mapNotNull { it.document } - documents.forEachIndexed { index, pdfDocument -> - // viewModel.saveAnnotations(pdfDocument, index) - } - // readerPrefs.saveConfiguration(configuration.configuration) - super.onStop() - } - - companion object { - private const val ID_AUDIO = 23 - private const val ID_VIDEO = 24 - } -} - -internal const val ARG_PDF_SCREEN = "as_arg_pdf_screen" diff --git a/features/pdf/src/main/res/drawable/ic_pdf_annotations.xml b/features/pdf/src/main/res/drawable/ic_pdf_annotations.xml deleted file mode 100644 index 9fa1be03f..000000000 --- a/features/pdf/src/main/res/drawable/ic_pdf_annotations.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/features/pdf/src/main/res/drawable/ic_pdf_bookmark.xml b/features/pdf/src/main/res/drawable/ic_pdf_bookmark.xml deleted file mode 100644 index 9e95b201b..000000000 --- a/features/pdf/src/main/res/drawable/ic_pdf_bookmark.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - diff --git a/features/pdf/src/main/res/drawable/ic_pdf_settings.xml b/features/pdf/src/main/res/drawable/ic_pdf_settings.xml deleted file mode 100644 index 0efbe55ac..000000000 --- a/features/pdf/src/main/res/drawable/ic_pdf_settings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - 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..3a2013bd1 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 @@ -22,17 +22,12 @@ package ss.libraries.pdf.api -import android.content.Intent import app.ss.models.LessonPdf import app.ss.models.PDFAux -import ss.libraries.circuit.navigation.PdfScreen /** API for handling pdf lessons. */ interface PdfReader { - /** Returns an intent to read the PDF [screen]. */ - fun launchIntent(screen: PdfScreen): Intent - /** Download these [pdfs] to device storage. */ suspend fun downloadFiles(pdfs: List): Result> From 7567e707c72b58b456668a088d6439c5e2a979c4 Mon Sep 17 00:00:00 2001 From: Tinashe Mzondiwa Date: Fri, 10 Apr 2026 22:08:21 -0400 Subject: [PATCH 19/23] Remove legacy video fragment --- .../playback/ui/video/VideoListFragment.kt | 78 ---------- .../playback/ui/video/VideoListScreen.kt | 18 --- .../playback/ui/video/VideoListViewModel.kt | 76 ---------- .../ui/video/VideoListViewModelTest.kt | 138 ------------------ 4 files changed, 310 deletions(-) delete mode 100644 features/media/src/main/kotlin/app/ss/media/playback/ui/video/VideoListFragment.kt delete mode 100644 features/media/src/main/kotlin/app/ss/media/playback/ui/video/VideoListViewModel.kt delete mode 100644 features/media/src/test/kotlin/app/ss/media/playback/ui/video/VideoListViewModelTest.kt diff --git a/features/media/src/main/kotlin/app/ss/media/playback/ui/video/VideoListFragment.kt b/features/media/src/main/kotlin/app/ss/media/playback/ui/video/VideoListFragment.kt deleted file mode 100644 index 7e01300cd..000000000 --- a/features/media/src/main/kotlin/app/ss/media/playback/ui/video/VideoListFragment.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (c) 2021. 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 NONINFRINGEMENT. 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 app.ss.media.playback.ui.video - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed -import androidx.fragment.app.FragmentManager -import app.ss.design.compose.extensions.surface.BottomSheetSurface -import app.ss.media.playback.ui.video.player.VideoPlayerActivity -import com.cryart.design.base.TransparentBottomSheetFragment -import com.google.android.material.bottomsheet.BottomSheetDialog -import dagger.hilt.android.AndroidEntryPoint -import ss.misc.SSConstants - -@AndroidEntryPoint -class VideoListFragment : TransparentBottomSheetFragment() { - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - return ComposeView(requireContext()).apply { - setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed) - setContent { - BottomSheetSurface { - VideoListScreen( - isAtTop = { isAtTop -> - (dialog as? BottomSheetDialog)?.behavior?.isDraggable = isAtTop - }, - onVideoClick = { video -> - val intent = VideoPlayerActivity.launchIntent( - requireActivity(), - video - ) - requireActivity().startActivity(intent) - } - ) - } - } - } - } -} - -fun FragmentManager.showVideoList( - lessonIndex: String -) { - val fragment = VideoListFragment().apply { - arguments = Bundle().apply { - putString(SSConstants.SS_LESSON_INDEX_EXTRA, lessonIndex) - } - } - fragment.show(this, "VideoList") -} diff --git a/features/media/src/main/kotlin/app/ss/media/playback/ui/video/VideoListScreen.kt b/features/media/src/main/kotlin/app/ss/media/playback/ui/video/VideoListScreen.kt index 877281424..0a9635e3a 100644 --- a/features/media/src/main/kotlin/app/ss/media/playback/ui/video/VideoListScreen.kt +++ b/features/media/src/main/kotlin/app/ss/media/playback/ui/video/VideoListScreen.kt @@ -47,7 +47,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -60,8 +59,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import app.ss.design.compose.extensions.isLargeScreen import app.ss.design.compose.extensions.modifier.asPlaceholder import app.ss.design.compose.extensions.modifier.thenIf @@ -81,21 +78,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import app.ss.translations.R as L10n -@Composable -internal fun VideoListScreen( - viewModel: VideoListViewModel = viewModel(), - isAtTop: (Boolean) -> Unit = {}, - onVideoClick: (SSVideo) -> Unit -) { - val videoList by viewModel.videoListFlow.collectAsStateWithLifecycle() - - VideoListScreen( - videoList = videoList, - isAtTop = isAtTop, - onVideoClick = onVideoClick - ) -} - @Composable internal fun VideoListScreen( videoList: VideoListData, diff --git a/features/media/src/main/kotlin/app/ss/media/playback/ui/video/VideoListViewModel.kt b/features/media/src/main/kotlin/app/ss/media/playback/ui/video/VideoListViewModel.kt deleted file mode 100644 index a7d56f199..000000000 --- a/features/media/src/main/kotlin/app/ss/media/playback/ui/video/VideoListViewModel.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2021. 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 NONINFRINGEMENT. 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 app.ss.media.playback.ui.video - -import androidx.annotation.VisibleForTesting -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import app.ss.models.media.SSVideosInfo -import com.cryart.sabbathschool.core.extensions.intent.lessonIndex -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull -import ss.foundation.coroutines.flow.stateIn -import ss.libraries.media.api.MediaRepository -import javax.inject.Inject - -@HiltViewModel -class VideoListViewModel @Inject constructor( - private val repository: MediaRepository, - private val savedStateHandle: SavedStateHandle -) : ViewModel() { - - @OptIn(ExperimentalCoroutinesApi::class) - val videoListFlow: StateFlow = flowOf(savedStateHandle.lessonIndex) - .mapNotNull { it } - .flatMapLatest { index -> - repository.getVideo(index) - }.map { videos -> - videos.toData(savedStateHandle.lessonIndex) - } - .stateIn(viewModelScope, VideoListData.Empty) -} - -@VisibleForTesting -internal fun List.toData( - lessonIndex: String? -): VideoListData = if (size == 1 && first().clips.isNotEmpty()) { - val allVideos = first().clips - val featuredVideo = allVideos.firstOrNull { it.targetIndex == lessonIndex } ?: allVideos.first() - VideoListData.Vertical( - featured = featuredVideo, - clips = allVideos.subtract(setOf(featuredVideo)).toList(), - showDragHandle = true, - ) -} else { - VideoListData.Horizontal( - data = this, - target = lessonIndex, - showDragHandle = true, - ) -} diff --git a/features/media/src/test/kotlin/app/ss/media/playback/ui/video/VideoListViewModelTest.kt b/features/media/src/test/kotlin/app/ss/media/playback/ui/video/VideoListViewModelTest.kt deleted file mode 100644 index 67441d6df..000000000 --- a/features/media/src/test/kotlin/app/ss/media/playback/ui/video/VideoListViewModelTest.kt +++ /dev/null @@ -1,138 +0,0 @@ -package app.ss.media.playback.ui.video - -import androidx.lifecycle.SavedStateHandle -import androidx.test.ext.junit.runners.AndroidJUnit4 -import app.cash.turbine.test -import app.ss.models.media.AudioFile -import app.ss.models.media.SSAudio -import app.ss.models.media.SSVideo -import app.ss.models.media.SSVideosInfo -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.test.runTest -import org.amshove.kluent.shouldBeEqualTo -import org.junit.Test -import org.junit.runner.RunWith -import ss.libraries.media.api.MediaRepository -import ss.misc.SSConstants - -private const val LESSON_INDEX = "2022-01-01" -private val fakeVideo = SSVideo("Artist", "id", "src", "target", "targetIndex", "thumbnail", "title") - -@RunWith(AndroidJUnit4::class) -class VideoListViewModelTest { - - private val videoFlow = MutableStateFlow>(emptyList()) - private val repository = FakeMediaRepository(videoFlow) - - private val underTest = VideoListViewModel( - repository = repository, - savedStateHandle = SavedStateHandle( - mapOf(SSConstants.SS_LESSON_INDEX_EXTRA to LESSON_INDEX), - ), - ) - - @Test - fun `videoListFlow - Vertical state`() = runTest { - val featuredVideo = fakeVideo.copy(targetIndex = LESSON_INDEX) - underTest.videoListFlow.test { - awaitItem() shouldBeEqualTo VideoListData.Horizontal( - data = emptyList(), - target = LESSON_INDEX, - showDragHandle = true, - ) - - videoFlow.emit( - listOf( - SSVideosInfo( - id = "id", - artist = "artist", - clips = listOf(fakeVideo, featuredVideo), - lessonIndex = LESSON_INDEX, - ) - ) - ) - - awaitItem() shouldBeEqualTo VideoListData.Vertical( - featured = featuredVideo, - clips = listOf(fakeVideo), - showDragHandle = true, - ) - - ensureAllEventsConsumed() - } - } - - @Test - fun `videoListFlow - Horizontal state`() = runTest { - val videos = listOf(fakeVideo, fakeVideo.copy(id = "id2")) - val data = listOf( - SSVideosInfo( - id = "id", - artist = "artist", - clips = videos, - lessonIndex = LESSON_INDEX, - ), - SSVideosInfo( - id = "id2", - artist = "artist2", - clips = videos, - lessonIndex = LESSON_INDEX, - ) - ) - underTest.videoListFlow.test { - awaitItem() shouldBeEqualTo VideoListData.Horizontal( - data = emptyList(), - target = LESSON_INDEX, - showDragHandle = true, - ) - - videoFlow.emit( - listOf( - SSVideosInfo( - id = "id", - artist = "artist", - clips = videos, - lessonIndex = LESSON_INDEX, - ), - SSVideosInfo( - id = "id2", - artist = "artist2", - clips = videos, - lessonIndex = LESSON_INDEX, - ) - ) - ) - - awaitItem() shouldBeEqualTo VideoListData.Horizontal( - data = data, - target = LESSON_INDEX, - showDragHandle = true, - ) - - ensureAllEventsConsumed() - } - } -} - -private class FakeMediaRepository( - private val videoFlow: Flow> -) : MediaRepository { - override fun getAudio(lessonIndex: String): Flow> { - return emptyFlow() - } - - override suspend fun findAudioFile(id: String): AudioFile? { - return null - } - - override suspend fun getPlayList(lessonIndex: String): List { - return emptyList() - } - - override fun getVideo(lessonIndex: String): Flow> { - return videoFlow - } - -} From 32693807a9abc52e665db91eef410f170a6099e1 Mon Sep 17 00:00:00 2001 From: Tinashe Mzondiwa Date: Sat, 11 Apr 2026 17:26:10 +0100 Subject: [PATCH 20/23] Clean up unused code --- features/document/build.gradle.kts | 1 + .../document/segment/components/pdf/PdfUi.kt | 3 +- features/media/build.gradle.kts | 2 - .../media/playback/PlaybackConnectionImpl.kt | 2 +- .../ss/media/playback/PlaybackViewModel.kt | 52 --------- .../ui/nowPlaying/AudioPlayerScreenUi.kt | 37 ------- .../ui/nowPlaying/NowPlayingDetail.kt | 5 +- .../ui/nowPlaying/NowPlayingFragment.kt | 72 ------------- .../ui/nowPlaying/NowPlayingViewModel.kt | 102 ------------------ 9 files changed, 5 insertions(+), 271 deletions(-) delete mode 100644 features/media/src/main/kotlin/app/ss/media/playback/PlaybackViewModel.kt delete mode 100644 features/media/src/main/kotlin/app/ss/media/playback/ui/nowPlaying/NowPlayingFragment.kt delete mode 100644 features/media/src/main/kotlin/app/ss/media/playback/ui/nowPlaying/NowPlayingViewModel.kt diff --git a/features/document/build.gradle.kts b/features/document/build.gradle.kts index e0ef30869..21e6d5279 100644 --- a/features/document/build.gradle.kts +++ b/features/document/build.gradle.kts @@ -45,6 +45,7 @@ ksp { } dependencies { + implementation(libs.androidx.activity.compose) implementation(libs.coil.compose) implementation(libs.google.hilt.android) implementation(libs.joda.time) diff --git a/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt b/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt index bf19a354d..c347aa3e9 100644 --- a/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt +++ b/features/document/src/main/kotlin/ss/document/segment/components/pdf/PdfUi.kt @@ -23,6 +23,7 @@ package ss.document.segment.components.pdf import android.content.Context +import androidx.activity.compose.LocalActivity import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize @@ -96,7 +97,7 @@ fun PdfUi( eventSink = eventSink, ) - val activity = LocalContext.current as? FragmentActivity + val activity = LocalActivity.current as? FragmentActivity DocumentView( documentState = documentState, diff --git a/features/media/build.gradle.kts b/features/media/build.gradle.kts index 44e68eeb4..99c6da3c3 100644 --- a/features/media/build.gradle.kts +++ b/features/media/build.gradle.kts @@ -45,11 +45,9 @@ dependencies { implementation(libs.androidx.activity) implementation(libs.androidx.appcompat) implementation(libs.androidx.constraintlayout) - implementation(libs.androidx.core) implementation(libs.androidx.lifecycle.extensions) implementation(libs.coil.compose) implementation(libs.google.hilt.android) - implementation(libs.google.material) implementation(libs.kotlin.coroutines.guava) implementation(libs.timber) implementation(projects.common.core) diff --git a/features/media/src/main/kotlin/app/ss/media/playback/PlaybackConnectionImpl.kt b/features/media/src/main/kotlin/app/ss/media/playback/PlaybackConnectionImpl.kt index 302356d79..0be0ad929 100644 --- a/features/media/src/main/kotlin/app/ss/media/playback/PlaybackConnectionImpl.kt +++ b/features/media/src/main/kotlin/app/ss/media/playback/PlaybackConnectionImpl.kt @@ -56,7 +56,7 @@ import timber.log.Timber internal class PlaybackConnectionImpl( private val context: Context, private val serviceComponent: ComponentName, - coroutineScope: CoroutineScope = ProcessLifecycleOwner.Companion.get().lifecycleScope + coroutineScope: CoroutineScope = ProcessLifecycleOwner.get().lifecycleScope ) : PlaybackConnection, CoroutineScope by coroutineScope { override val isConnected = MutableStateFlow(false) diff --git a/features/media/src/main/kotlin/app/ss/media/playback/PlaybackViewModel.kt b/features/media/src/main/kotlin/app/ss/media/playback/PlaybackViewModel.kt deleted file mode 100644 index a1ba2fb0d..000000000 --- a/features/media/src/main/kotlin/app/ss/media/playback/PlaybackViewModel.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2023. 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 app.ss.media.playback - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import ss.libraries.media.model.extensions.id -import ss.services.media.ui.PlaybackConnection -import javax.inject.Inject - -@HiltViewModel -class PlaybackViewModel @Inject constructor( - val playbackConnection: PlaybackConnection -) : ViewModel() { - - init { - viewModelScope.launch { - playbackConnection.isConnected.collect { connected -> - if (connected) { - val state = playbackConnection.playbackState.first() - val nowPlaying = playbackConnection.nowPlaying.first() - if (!state.isPlaying && nowPlaying.id.isNotEmpty()) { - playbackConnection.stop() - } - } - } - } - } -} diff --git a/features/media/src/main/kotlin/app/ss/media/playback/ui/nowPlaying/AudioPlayerScreenUi.kt b/features/media/src/main/kotlin/app/ss/media/playback/ui/nowPlaying/AudioPlayerScreenUi.kt index a585c504d..18105bbf7 100644 --- a/features/media/src/main/kotlin/app/ss/media/playback/ui/nowPlaying/AudioPlayerScreenUi.kt +++ b/features/media/src/main/kotlin/app/ss/media/playback/ui/nowPlaying/AudioPlayerScreenUi.kt @@ -51,8 +51,6 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import app.ss.design.compose.theme.SsTheme import app.ss.design.compose.widget.icon.IconBox import app.ss.design.compose.widget.icon.IconSlot @@ -83,41 +81,6 @@ fun AudioPlayerScreenUi(state: AudioPlayerState, modifier: Modifier = Modifier) } } -@Composable -internal fun NowPlayingScreen( - viewModel: NowPlayingViewModel = viewModel(), - isDraggable: (Boolean) -> Unit = {} -) { - val playbackConnection = viewModel.playbackConnection - val playbackState by playbackConnection.playbackState - .collectAsStateWithLifecycle() - val nowPlaying by viewModel.nowPlayingAudio - .collectAsStateWithLifecycle() - val playbackQueue by playbackConnection.playbackQueue - .collectAsStateWithLifecycle() - val playbackSpeed by playbackConnection.playbackSpeed - .collectAsStateWithLifecycle() - val playbackProgressState by playbackConnection.playbackProgress - .collectAsStateWithLifecycle() - val nowPlayingAudio = if (nowPlaying.id.isEmpty()) { - playbackQueue.currentAudio ?: nowPlaying - } else { - nowPlaying - } - - NowPlayingScreen( - spec = NowPlayingScreenSpec( - nowPlayingAudio, - playbackQueue, - playbackState, - playbackProgressState, - playbackConnection, - playbackSpeed, - isDraggable - ) - ) -} - @Composable internal fun NowPlayingScreen( spec: NowPlayingScreenSpec, diff --git a/features/media/src/main/kotlin/app/ss/media/playback/ui/nowPlaying/NowPlayingDetail.kt b/features/media/src/main/kotlin/app/ss/media/playback/ui/nowPlaying/NowPlayingDetail.kt index 91a3db5d3..ea7e83bdc 100644 --- a/features/media/src/main/kotlin/app/ss/media/playback/ui/nowPlaying/NowPlayingDetail.kt +++ b/features/media/src/main/kotlin/app/ss/media/playback/ui/nowPlaying/NowPlayingDetail.kt @@ -26,7 +26,6 @@ import android.view.MotionEvent import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibilityScope -import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionLayout import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.core.Spring @@ -61,7 +60,7 @@ import app.ss.models.media.AudioFile import ss.services.media.ui.spec.PlaybackQueueSpec import ss.services.media.ui.spec.toSpec -@OptIn(ExperimentalComposeUiApi::class, ExperimentalSharedTransitionApi::class) +@OptIn(ExperimentalComposeUiApi::class) @Composable internal fun NowPlayingDetail( spec: NowPlayingScreenSpec, @@ -147,7 +146,6 @@ internal fun NowPlayingDetail( } } -@OptIn(ExperimentalSharedTransitionApi::class) @Composable private fun RowContent( nowPlayingAudio: AudioFile, @@ -188,7 +186,6 @@ private fun RowContent( } } -@OptIn(ExperimentalSharedTransitionApi::class) @Composable private fun ColumnContent( nowPlayingAudio: AudioFile, diff --git a/features/media/src/main/kotlin/app/ss/media/playback/ui/nowPlaying/NowPlayingFragment.kt b/features/media/src/main/kotlin/app/ss/media/playback/ui/nowPlaying/NowPlayingFragment.kt deleted file mode 100644 index fc3a222cb..000000000 --- a/features/media/src/main/kotlin/app/ss/media/playback/ui/nowPlaying/NowPlayingFragment.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (c) 2021. 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 NONINFRINGEMENT. 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 app.ss.media.playback.ui.nowPlaying - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed -import androidx.fragment.app.FragmentManager -import app.ss.design.compose.extensions.surface.BottomSheetSurface -import com.cryart.design.base.TransparentBottomSheetFragment -import com.google.android.material.bottomsheet.BottomSheetDialog -import dagger.hilt.android.AndroidEntryPoint -import ss.misc.SSConstants - -@AndroidEntryPoint -class NowPlayingFragment : TransparentBottomSheetFragment() { - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - return ComposeView(requireContext()).apply { - setViewCompositionStrategy(DisposeOnViewTreeLifecycleDestroyed) - setContent { - BottomSheetSurface { - NowPlayingScreen( - isDraggable = { isDraggable -> - (dialog as? BottomSheetDialog)?.behavior?.isDraggable = isDraggable - } - ) - } - } - } - } -} - -fun FragmentManager.showNowPlaying( - lessonIndex: String?, - readIndex: String? = null -) { - val fragment = NowPlayingFragment().apply { - arguments = Bundle().apply { - putString(SSConstants.SS_LESSON_INDEX_EXTRA, lessonIndex) - putString(SSConstants.SS_READ_INDEX_EXTRA, readIndex) - } - } - fragment.show(this, "NowPlaying") -} diff --git a/features/media/src/main/kotlin/app/ss/media/playback/ui/nowPlaying/NowPlayingViewModel.kt b/features/media/src/main/kotlin/app/ss/media/playback/ui/nowPlaying/NowPlayingViewModel.kt deleted file mode 100644 index 12941e19b..000000000 --- a/features/media/src/main/kotlin/app/ss/media/playback/ui/nowPlaying/NowPlayingViewModel.kt +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (c) 2023. 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 app.ss.media.playback.ui.nowPlaying - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import app.ss.models.media.AudioFile -import com.cryart.sabbathschool.core.extensions.intent.lessonIndex -import com.cryart.sabbathschool.core.extensions.intent.readIndex -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import ss.foundation.coroutines.flow.stateIn -import ss.libraries.media.api.MediaRepository -import ss.libraries.media.model.extensions.id -import ss.libraries.media.model.extensions.targetIndex -import ss.libraries.media.model.toAudio -import ss.services.media.ui.PlaybackConnection -import javax.inject.Inject - -@HiltViewModel -internal class NowPlayingViewModel @Inject constructor( - private val repository: MediaRepository, - val playbackConnection: PlaybackConnection, - private val savedStateHandle: SavedStateHandle -) : ViewModel() { - - val nowPlayingAudio: StateFlow = playbackConnection.nowPlaying - .map { metaData -> - val def = metaData.toAudio() - if (metaData.id.isEmpty()) { - def - } else { - repository.findAudioFile(metaData.id) ?: def - } - } - .stateIn(viewModelScope, AudioFile("")) - - init { - viewModelScope.launch { - playbackConnection.isConnected.collect { connected -> - if (connected) { - generateQueue() - } - } - } - } - - private fun generateQueue() = viewModelScope.launch { - val nowPlaying = playbackConnection.nowPlaying.first() - val lessonIndex = savedStateHandle.lessonIndex ?: return@launch - // Correct queue or playlist already set - if (nowPlaying.targetIndex?.contains(lessonIndex) == true) return@launch - - val playlist = repository.getPlayList(lessonIndex) - - if (nowPlaying.id.isEmpty()) { - setAudioQueue(playlist) - } else { - val audio = repository.findAudioFile(nowPlaying.id) - if (audio?.targetIndex?.startsWith(lessonIndex) == false) { - val state = playbackConnection.playbackState.first() - setAudioQueue(playlist, state.isPlaying) - } - } - } - - private fun setAudioQueue(playlist: List, play: Boolean = false) { - if (playlist.isEmpty()) return - - val index = playlist.indexOfFirst { it.targetIndex == savedStateHandle.readIndex } - val position = index.coerceAtLeast(0) - playbackConnection.setQueue(playlist, index) - - if (play) { - playbackConnection.playAudios(playlist, position) - } - } -} From e00ce861c160f26335b1bb3b5e99e1f00deb0abe Mon Sep 17 00:00:00 2001 From: Tinashe Mzondiwa Date: Sun, 12 Apr 2026 15:45:27 +0200 Subject: [PATCH 21/23] Clean-up DocumentPresenter --- .../kotlin/ss/document/DocumentPresenter.kt | 38 ------------------- 1 file changed, 38 deletions(-) diff --git a/features/document/src/main/kotlin/ss/document/DocumentPresenter.kt b/features/document/src/main/kotlin/ss/document/DocumentPresenter.kt index 7803d6ad0..c465050ae 100644 --- a/features/document/src/main/kotlin/ss/document/DocumentPresenter.kt +++ b/features/document/src/main/kotlin/ss/document/DocumentPresenter.kt @@ -23,14 +23,11 @@ package ss.document import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.Snapshot import androidx.core.net.toUri -import app.ss.models.PDFAux import app.ss.models.media.AudioFile import app.ss.models.media.SSVideo import com.slack.circuit.codegen.annotations.CircuitInject @@ -66,7 +63,6 @@ import ss.document.producer.UserInputStateProducer import ss.document.segment.producer.SegmentOverlayStateProducer import ss.libraries.circuit.navigation.DocumentScreen import ss.libraries.circuit.navigation.ExpandedAudioPlayerScreen -import ss.libraries.circuit.navigation.PdfScreen import ss.libraries.circuit.navigation.ResourceScreen import ss.libraries.media.api.MediaNavigation import ss.libraries.media.api.SSMediaPlayer @@ -75,7 +71,6 @@ import ss.libraries.media.model.SSMediaItem import ss.libraries.media.model.extensions.NONE_PLAYING import ss.libraries.media.service.MusicService import ss.libraries.media.service.VideoService -import ss.libraries.pdf.api.PdfReader import ss.misc.DateHelper import ss.resources.api.ResourcesRepository import ss.services.media.ui.PlaybackConnection @@ -110,8 +105,6 @@ class DocumentPresenter @AssistedInject constructor( val resourceDocument = response - // LaunchedEffect(resourceDocument) { checkPdfOnlySegment(resourceDocument) } - val actionsState = resourceDocument?.let { actionsProducer( navigator = navigator, @@ -291,37 +284,6 @@ class DocumentPresenter @AssistedInject constructor( } ?: firstOrNull() } - private fun checkPdfOnlySegment(resourceDocument: ResourceDocument?) { - val document = resourceDocument ?: return - val segments = document.segments ?: return - val blocks = segments.flatMap { it.blocks.orEmpty() } - val pdfs = segments.flatMap { it.pdf.orEmpty() } - - if (blocks.isEmpty() && pdfs.isNotEmpty()) { - val pdfs = segments.flatMap { it.pdf.orEmpty() } - val screen = PdfScreen( - documentId = document.id, - resourceId = document.resourceId, - resourceIndex = document.resourceIndex, - documentIndex = document.index, - segmentId = null, - pdfs = pdfs.map { - PDFAux( - id = it.id, - src = it.src, - title = it.title, - target = it.target, - targetIndex = it.targetIndex, - ) - }, - ) - Snapshot.withMutableSnapshot { - // navigator.pop() - // navigator.goTo(IntentScreen(pdfReader.launchIntent(screen))) - } - } - } - private fun BlockItem.Video.toSSVideo( resource: Resource?, document: ResourceDocument?, From 52573bc61cabc05b80e08f5ff0feefd9682f81cf Mon Sep 17 00:00:00 2001 From: Tinashe Mzondiwa Date: Sun, 12 Apr 2026 15:56:55 +0200 Subject: [PATCH 22/23] Update PdfReaderPrefsImpl.kt --- .../kotlin/app/ss/pdf/PdfReaderPrefsImpl.kt | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderPrefsImpl.kt b/features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderPrefsImpl.kt index 8b94bde00..0b32d286c 100644 --- a/features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderPrefsImpl.kt +++ b/features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderPrefsImpl.kt @@ -39,6 +39,7 @@ import com.pspdfkit.configuration.page.PageScrollMode import com.pspdfkit.configuration.theming.ThemeMode import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import ss.foundation.coroutines.DispatcherProvider @@ -75,7 +76,7 @@ private val Context.pdfDataStore: DataStore by preferencesDataStore internal class PdfReaderPrefsImpl( private val dataStore: DataStore, private val sharedPreferences: SharedPreferences, - dispatcherProvider: DispatcherProvider + private val dispatcherProvider: DispatcherProvider, ) : PdfReaderPrefs, Scopable by ioScopable(dispatcherProvider) { @Inject @@ -95,9 +96,9 @@ internal class PdfReaderPrefsImpl( override fun scrollMode(): Flow = dataStore.data.map { preferences -> preferences[scrollModeKey]?.let { - runCatching { PageScrollMode.valueOf(it) }.getOrNull() + PageScrollMode.valueOf(it) } ?: PageScrollMode.CONTINUOUS - } + }.flowOn(dispatcherProvider.io) override fun setScrollMode(mode: PageScrollMode) { sharedPreferences.edit { putString(KEY_SCROLL_MODE, mode.name) } @@ -106,9 +107,9 @@ internal class PdfReaderPrefsImpl( override fun pageLayoutMode(): Flow = dataStore.data.map { preferences -> preferences[layoutModeKey]?.let { - runCatching { PageLayoutMode.valueOf(it) }.getOrNull() + PageLayoutMode.valueOf(it) } ?: PageLayoutMode.SINGLE - } + }.flowOn(dispatcherProvider.io) override fun setPageLayoutMode(mode: PageLayoutMode) { sharedPreferences.edit { putString(KEY_LAYOUT_MODE, mode.name) } @@ -117,9 +118,9 @@ internal class PdfReaderPrefsImpl( override fun scrollDirection(): Flow = dataStore.data.map { preferences -> preferences[scrollDirectionKey]?.let { - runCatching { PageScrollDirection.valueOf(it) }.getOrNull() + PageScrollDirection.valueOf(it) } ?: PageScrollDirection.VERTICAL - } + }.flowOn(dispatcherProvider.io) override fun setScrollDirection(direction: PageScrollDirection) { sharedPreferences.edit { putString(KEY_SCROLL_DIRECTION, direction.name) } @@ -127,10 +128,8 @@ internal class PdfReaderPrefsImpl( } override fun themeMode(): Flow = dataStore.data.map { preferences -> - preferences[themeModeKey]?.let { - runCatching { ThemeMode.valueOf(it) }.getOrNull() - } ?: ThemeMode.DEFAULT - } + preferences[themeModeKey]?.let { ThemeMode.valueOf(it) } ?: ThemeMode.DEFAULT + }.flowOn(dispatcherProvider.io) override fun setThemeMode(mode: ThemeMode) { sharedPreferences.edit { putString(KEY_THEME_MODE, mode.name) } From aa3b4c34bddba6ef77f0a6f04220c535004a74cd Mon Sep 17 00:00:00 2001 From: Tinashe Mzondiwa Date: Sun, 12 Apr 2026 16:21:50 +0200 Subject: [PATCH 23/23] Move to services module --- app/build.gradle.kts | 2 +- features/pdf/lint-baseline.xml | 27 ---------------- features/pdf/src/main/AndroidManifest.xml | 31 ------------------- .../pdf/impl}/build.gradle.kts | 17 ++-------- services/pdf/impl/lint-baseline.xml | 4 +++ .../pdf/impl/src/main/AndroidManifest.xml | 8 +++++ .../ss/services/pdf/impl}/PdfReaderImpl.kt | 4 +-- .../services/pdf/impl}/PdfReaderPrefsImpl.kt | 4 +-- .../services/pdf/impl}/di/BindingsModule.kt | 8 ++--- settings.gradle.kts | 2 +- 10 files changed, 24 insertions(+), 83 deletions(-) delete mode 100644 features/pdf/lint-baseline.xml delete mode 100644 features/pdf/src/main/AndroidManifest.xml rename {features/pdf => services/pdf/impl}/build.gradle.kts (87%) create mode 100644 services/pdf/impl/lint-baseline.xml create mode 100644 services/pdf/impl/src/main/AndroidManifest.xml rename {features/pdf/src/main/kotlin/app/ss/pdf => services/pdf/impl/src/main/kotlin/ss/services/pdf/impl}/PdfReaderImpl.kt (97%) rename {features/pdf/src/main/kotlin/app/ss/pdf => services/pdf/impl/src/main/kotlin/ss/services/pdf/impl}/PdfReaderPrefsImpl.kt (98%) rename {features/pdf/src/main/kotlin/app/ss/pdf => services/pdf/impl/src/main/kotlin/ss/services/pdf/impl}/di/BindingsModule.kt (89%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 983c34585..f14705a66 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -160,7 +160,6 @@ dependencies { implementation(projects.features.languages) implementation(projects.features.media) implementation(projects.features.navigationSuite) - implementation(projects.features.pdf) implementation(projects.features.resource) implementation(projects.features.settings) implementation(projects.features.share) @@ -172,6 +171,7 @@ dependencies { implementation(projects.services.lessons.impl) implementation(projects.services.media.impl) implementation(projects.services.media.ui) + implementation(projects.services.pdf.impl) implementation(projects.services.prefs.impl) implementation(projects.services.resources.impl) implementation(projects.services.storage.impl) diff --git a/features/pdf/lint-baseline.xml b/features/pdf/lint-baseline.xml deleted file mode 100644 index beaf28b56..000000000 --- a/features/pdf/lint-baseline.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - diff --git a/features/pdf/src/main/AndroidManifest.xml b/features/pdf/src/main/AndroidManifest.xml deleted file mode 100644 index b1f96d57c..000000000 --- a/features/pdf/src/main/AndroidManifest.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/features/pdf/build.gradle.kts b/services/pdf/impl/build.gradle.kts similarity index 87% rename from features/pdf/build.gradle.kts rename to services/pdf/impl/build.gradle.kts index 3cf626753..16ab326ef 100644 --- a/features/pdf/build.gradle.kts +++ b/services/pdf/impl/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023. Adventech + * Copyright (c) 2026. 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 @@ -21,7 +21,6 @@ */ import com.android.build.api.dsl.LibraryExtension -import org.gradle.kotlin.dsl.configure import java.io.FileInputStream import java.util.Properties @@ -39,25 +38,14 @@ val psPdfKitKey = readPropertyValue( ) extensions.configure { - namespace = "app.ss.pdf" + namespace = "ss.services.pdf.impl" defaultConfig { manifestPlaceholders["psPdfKitKey"] = psPdfKitKey } } -foundry { - features { compose() } -} - -ksp { - arg("circuit.codegen.mode", "hilt") -} - dependencies { - implementation(libs.androidx.activity) - implementation(libs.androidx.appcompat) - implementation(libs.androidx.core) implementation(libs.androidx.datastore.prefs) implementation(libs.androidx.preference) implementation(libs.google.hilt.android) @@ -67,7 +55,6 @@ dependencies { implementation(projects.libraries.foundation.coroutines) implementation(projects.libraries.pdf.api) - ksp(libs.circuit.codegen) ksp(libs.google.hilt.compiler) } diff --git a/services/pdf/impl/lint-baseline.xml b/services/pdf/impl/lint-baseline.xml new file mode 100644 index 000000000..8cf328fb8 --- /dev/null +++ b/services/pdf/impl/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/services/pdf/impl/src/main/AndroidManifest.xml b/services/pdf/impl/src/main/AndroidManifest.xml new file mode 100644 index 000000000..2a5b692b3 --- /dev/null +++ b/services/pdf/impl/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderImpl.kt b/services/pdf/impl/src/main/kotlin/ss/services/pdf/impl/PdfReaderImpl.kt similarity index 97% rename from features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderImpl.kt rename to services/pdf/impl/src/main/kotlin/ss/services/pdf/impl/PdfReaderImpl.kt index 8ab713f37..6f0c1aabf 100644 --- a/features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderImpl.kt +++ b/services/pdf/impl/src/main/kotlin/ss/services/pdf/impl/PdfReaderImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025. Adventech + * Copyright (c) 2026. 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 @@ -20,7 +20,7 @@ * THE SOFTWARE. */ -package app.ss.pdf +package ss.services.pdf.impl import android.content.Context import android.net.Uri diff --git a/features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderPrefsImpl.kt b/services/pdf/impl/src/main/kotlin/ss/services/pdf/impl/PdfReaderPrefsImpl.kt similarity index 98% rename from features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderPrefsImpl.kt rename to services/pdf/impl/src/main/kotlin/ss/services/pdf/impl/PdfReaderPrefsImpl.kt index 0b32d286c..c6c42417b 100644 --- a/features/pdf/src/main/kotlin/app/ss/pdf/PdfReaderPrefsImpl.kt +++ b/services/pdf/impl/src/main/kotlin/ss/services/pdf/impl/PdfReaderPrefsImpl.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025. Adventech + * Copyright (c) 2026. 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 @@ -20,7 +20,7 @@ * THE SOFTWARE. */ -package app.ss.pdf +package ss.services.pdf.impl import android.content.Context import android.content.SharedPreferences diff --git a/features/pdf/src/main/kotlin/app/ss/pdf/di/BindingsModule.kt b/services/pdf/impl/src/main/kotlin/ss/services/pdf/impl/di/BindingsModule.kt similarity index 89% rename from features/pdf/src/main/kotlin/app/ss/pdf/di/BindingsModule.kt rename to services/pdf/impl/src/main/kotlin/ss/services/pdf/impl/di/BindingsModule.kt index 3c9a3ad5c..8bb67d0d4 100644 --- a/features/pdf/src/main/kotlin/app/ss/pdf/di/BindingsModule.kt +++ b/services/pdf/impl/src/main/kotlin/ss/services/pdf/impl/di/BindingsModule.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023. Adventech + * Copyright (c) 2026. 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 @@ -20,10 +20,10 @@ * THE SOFTWARE. */ -package app.ss.pdf.di +package ss.services.pdf.impl.di -import app.ss.pdf.PdfReaderImpl -import app.ss.pdf.PdfReaderPrefsImpl +import ss.services.pdf.impl.PdfReaderImpl +import ss.services.pdf.impl.PdfReaderPrefsImpl import dagger.Binds import dagger.Module import dagger.hilt.InstallIn diff --git a/settings.gradle.kts b/settings.gradle.kts index d61eae87d..e809cfe49 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -57,7 +57,6 @@ include( ":features:feed", ":features:media", ":features:navigation-suite", - ":features:pdf", ":features:resource", ":features:settings", ":features:share", @@ -91,6 +90,7 @@ include( ":services:lessons:impl", ":services:media:impl", ":services:media:ui", + ":services:pdf:impl", ":services:prefs:impl", ":services:resources:api", ":services:resources:impl",