From a8c0f64cdf743a00e66246f4441265af8955f01f Mon Sep 17 00:00:00 2001 From: Nelson Chadali Date: Thu, 17 Oct 2024 10:10:57 +0200 Subject: [PATCH] Rei 07 - Manuals module implementation (#14) * Add Room databese and create all the necessary implementations and methods * add hilt viewModel injector to compose * Fix room and add ui state * Implement PDF view * Fix issues and remove annecessary code * Delete ManualService.kt --------- Co-authored-by: Carlos Macaneta <56470814+CarlosMacaneta@users.noreply.github.com> --- gradle/libs.versions.toml | 9 ++- support-module/build.gradle.kts | 21 ++++++ .../saudigitus/support_module/MainActivity.kt | 2 + .../support_module/data/AppModule.kt | 4 -- .../data/local/ManualsRepository.kt | 17 +++++ .../data/local/database/DBconfig.kt | 18 +++++ .../data/local/database/ManualsDAO.kt | 23 ++++++ .../local/repository/ManualsRepositoryImp.kt | 63 +++++++++++++++++ .../data/models/manuals/Manual.kt | 25 +++++++ .../data/models/manuals/ManualItem.kt | 24 +++++++ .../saudigitus/support_module/di/AppModule.kt | 37 ++++++++++ .../support_module/ui/ManualState.kt | 15 ++++ .../saudigitus/support_module/ui/Routes.kt | 1 + .../ui/components/CustomCard.kt | 2 - .../support_module/ui/components/ListCard.kt | 48 +++++++------ .../ui/manualScreen/AppPdfViewer.kt | 52 ++++++++++++++ .../ui/manualScreen/ManualScreen.kt | 67 ++++++++++++------ .../ui/manualScreen/ManualViewModel.kt | 70 +++++++++++++++++++ .../support_module/utils/AppNavHost.kt | 12 ++++ .../support_module/utils/Constants.kt | 2 + .../saudigitus/support_module/utils/Mapper.kt | 15 ++++ .../src/main/res/values/strings.xml | 1 + 22 files changed, 480 insertions(+), 48 deletions(-) delete mode 100644 support-module/src/main/java/com/saudigitus/support_module/data/AppModule.kt create mode 100644 support-module/src/main/java/com/saudigitus/support_module/data/local/ManualsRepository.kt create mode 100644 support-module/src/main/java/com/saudigitus/support_module/data/local/database/DBconfig.kt create mode 100644 support-module/src/main/java/com/saudigitus/support_module/data/local/database/ManualsDAO.kt create mode 100644 support-module/src/main/java/com/saudigitus/support_module/data/local/repository/ManualsRepositoryImp.kt create mode 100644 support-module/src/main/java/com/saudigitus/support_module/data/models/manuals/Manual.kt create mode 100644 support-module/src/main/java/com/saudigitus/support_module/data/models/manuals/ManualItem.kt create mode 100644 support-module/src/main/java/com/saudigitus/support_module/di/AppModule.kt create mode 100644 support-module/src/main/java/com/saudigitus/support_module/ui/ManualState.kt create mode 100644 support-module/src/main/java/com/saudigitus/support_module/ui/manualScreen/AppPdfViewer.kt create mode 100644 support-module/src/main/java/com/saudigitus/support_module/ui/manualScreen/ManualViewModel.kt create mode 100644 support-module/src/main/java/com/saudigitus/support_module/utils/Mapper.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 83ed364418..9bb2f8757c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -96,7 +96,15 @@ navigationCompose = "2.8.2" lifecycleRuntimeKtx = "2.8.6" composeBom = "2024.04.01" androidxHiltNavigation = "1.2.0" +room_version = "2.6.1" +bouquet-version = "1.1.2" [libraries] + +io-github-grizzi91 = { group = "io.github.grizzi91", name = "bouquet", version.ref = "bouquet-version" } +androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidxHiltNavigation" } +room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room_version" } +room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room_version" } +room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room_version" } gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "gradle" } kotlinPlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } kotlinSerialization = { group = "org.jetbrains.kotlin", name = "kotlin-serialization", version.ref = "kotlin" } @@ -154,7 +162,6 @@ dagger-compiler = { group = "com.google.dagger", name = "dagger-compiler", versi dagger-hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } dagger-hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } dagger-hilt-compiler-new = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" } -androidx-hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "androidxHiltNavigation" } rx-java = { group = "io.reactivex.rxjava2", name = "rxjava", version.ref = "rxjava" } rx-android = { group = "io.reactivex.rxjava2", name = "rxandroid", version.ref = "rxandroid" } rx-binding-compat = { group = "com.jakewharton.rxbinding2", name = "rxbinding-appcompat-v7", version.ref = "rxbindings" } diff --git a/support-module/build.gradle.kts b/support-module/build.gradle.kts index 2eee9db323..ed4a881187 100644 --- a/support-module/build.gradle.kts +++ b/support-module/build.gradle.kts @@ -1,6 +1,10 @@ plugins { id("com.android.library") kotlin("android") + kotlin("kapt") + id("kotlinx-serialization") + id("kotlin-parcelize") + id("dagger.hilt.android.plugin") alias(libs.plugins.kotlin.compose.compiler) } @@ -49,6 +53,19 @@ android { } dependencies { + // HILT + implementation(libs.androidx.compose.lifecycle) + implementation(libs.androidx.hilt.navigation.compose) + implementation(libs.dagger.hilt.android) + + // ROOM + implementation(libs.room.runtime) + annotationProcessor(libs.room.compiler) + implementation(libs.room.ktx) + + // PDF VIEWER + implementation(libs.io.github.grizzi91) + implementation(project(":commons")) implementation(libs.androidx.coreKtx) implementation(libs.androidx.appcompat) @@ -71,4 +88,8 @@ dependencies { coreLibraryDesugaring(libs.desugar) debugImplementation(libs.androidx.compose.uitooling) debugImplementation(libs.test.ui.test.manifest) + + kapt(libs.room.compiler) + kapt(libs.dagger.compiler) + kapt(libs.dagger.hilt.android.compiler) } \ No newline at end of file diff --git a/support-module/src/main/java/com/saudigitus/support_module/MainActivity.kt b/support-module/src/main/java/com/saudigitus/support_module/MainActivity.kt index 53505fbe73..fedc3c240c 100644 --- a/support-module/src/main/java/com/saudigitus/support_module/MainActivity.kt +++ b/support-module/src/main/java/com/saudigitus/support_module/MainActivity.kt @@ -20,8 +20,10 @@ import com.saudigitus.support_module.ui.Screen import com.saudigitus.support_module.ui.manualScreen.ManualScreen import com.saudigitus.support_module.ui.theme.SupportUiTheme import com.saudigitus.support_module.utils.Constants +import dagger.hilt.android.AndroidEntryPoint import org.dhis2.ui.theme.Dhis2Theme +@AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/support-module/src/main/java/com/saudigitus/support_module/data/AppModule.kt b/support-module/src/main/java/com/saudigitus/support_module/data/AppModule.kt deleted file mode 100644 index f5b2c1663d..0000000000 --- a/support-module/src/main/java/com/saudigitus/support_module/data/AppModule.kt +++ /dev/null @@ -1,4 +0,0 @@ -package com.saudigitus.support_module.data - -class AppModule { -} \ No newline at end of file diff --git a/support-module/src/main/java/com/saudigitus/support_module/data/local/ManualsRepository.kt b/support-module/src/main/java/com/saudigitus/support_module/data/local/ManualsRepository.kt new file mode 100644 index 0000000000..d13595ad7f --- /dev/null +++ b/support-module/src/main/java/com/saudigitus/support_module/data/local/ManualsRepository.kt @@ -0,0 +1,17 @@ +package com.saudigitus.support_module.data.local + +import android.content.Context +import com.saudigitus.support_module.data.models.manuals.ManualItem +import kotlinx.coroutines.flow.Flow +import okhttp3.ResponseBody +import java.io.File + +interface ManualsRepository { + fun getManualsDataStore(): Flow> + suspend fun openManual(context: Context, url: String, fileName: String): File? + suspend fun downloadManualToLocal( + context: Context, + url: String, + fileName: String + ) +} \ No newline at end of file diff --git a/support-module/src/main/java/com/saudigitus/support_module/data/local/database/DBconfig.kt b/support-module/src/main/java/com/saudigitus/support_module/data/local/database/DBconfig.kt new file mode 100644 index 0000000000..59c3080531 --- /dev/null +++ b/support-module/src/main/java/com/saudigitus/support_module/data/local/database/DBconfig.kt @@ -0,0 +1,18 @@ +package com.saudigitus.support_module.data.local.database + + +import androidx.room.Database +import androidx.room.* +import com.saudigitus.support_module.data.models.manuals.ManualItem + +@Database( + entities = [ManualItem::class], + version = 1 +) +abstract class AppDatabase: RoomDatabase() { + abstract fun manualsDAO(): ManualsDao + + companion object { + const val DB_NAME = "manuals_db" + } +} \ No newline at end of file diff --git a/support-module/src/main/java/com/saudigitus/support_module/data/local/database/ManualsDAO.kt b/support-module/src/main/java/com/saudigitus/support_module/data/local/database/ManualsDAO.kt new file mode 100644 index 0000000000..7c4461d2b1 --- /dev/null +++ b/support-module/src/main/java/com/saudigitus/support_module/data/local/database/ManualsDAO.kt @@ -0,0 +1,23 @@ +package com.saudigitus.support_module.data.local.database + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.saudigitus.support_module.data.models.manuals.ManualItem + +@Dao +interface ManualsDao { + @Query("SELECT * FROM manualItem") + fun getAll(): List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun create(mediaDetails: ManualItem) + + @Query("SELECT * FROM manualItem WHERE uid LIKE :id") + fun getDetailsById(id: String): ManualItem + + @Delete + fun delete(manual: ManualItem) +} \ No newline at end of file diff --git a/support-module/src/main/java/com/saudigitus/support_module/data/local/repository/ManualsRepositoryImp.kt b/support-module/src/main/java/com/saudigitus/support_module/data/local/repository/ManualsRepositoryImp.kt new file mode 100644 index 0000000000..adcd6ba0d3 --- /dev/null +++ b/support-module/src/main/java/com/saudigitus/support_module/data/local/repository/ManualsRepositoryImp.kt @@ -0,0 +1,63 @@ +package com.saudigitus.support_module.data.local.repository + +import android.app.DownloadManager +import android.content.Context +import android.net.Uri +import android.os.Environment +import com.saudigitus.support_module.data.local.ManualsRepository +import com.saudigitus.support_module.data.local.database.ManualsDao +import com.saudigitus.support_module.data.models.manuals.Manual +import com.saudigitus.support_module.data.models.manuals.ManualItem +import com.saudigitus.support_module.utils.Constants +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.withContext +import org.hisp.dhis.android.core.D2 +import java.io.File + +class ManualsRepositoryImp( + private val manualsDAO: ManualsDao, + private val d2: D2 +): ManualsRepository { + override fun getManualsDataStore(): Flow> { + val dataStoreValue = d2.dataStoreModule().dataStore().byKey() + .eq(Constants.MEDIA_DATA_STORE_KEY).blockingGet() + .getOrNull(0)?.value() + val dataStore = dataStoreValue?.let { Manual.fromJson(it) } + return if (dataStore != null) { + flowOf(dataStore) + } else { + flowOf(emptyList()) + } + } + + override suspend fun downloadManualToLocal(context: Context, url: String, fileName: String): Unit + = withContext(Dispatchers.IO) { + val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager + val uri = Uri.parse(url) + val file = File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), + "$fileName.pdf" + ) + if (!file.exists()) { + val request = DownloadManager.Request(uri).apply { + setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE) + // Store the file in the app's private external files directory + setDestinationInExternalFilesDir(context, Environment.DIRECTORY_DOCUMENTS, "$fileName.pdf") + } + downloadManager.enqueue(request) + } + return@withContext + } + + + override suspend fun openManual(context: Context, url: String, fileName: String): File? + = withContext(Dispatchers.IO) { + // Get the file from the app's private storage + val file = File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "$fileName.pdf") + if (file.exists()) { + return@withContext file + } + return@withContext null + } +} \ No newline at end of file diff --git a/support-module/src/main/java/com/saudigitus/support_module/data/models/manuals/Manual.kt b/support-module/src/main/java/com/saudigitus/support_module/data/models/manuals/Manual.kt new file mode 100644 index 0000000000..c7ea0de9b1 --- /dev/null +++ b/support-module/src/main/java/com/saudigitus/support_module/data/models/manuals/Manual.kt @@ -0,0 +1,25 @@ +package com.saudigitus.support_module.data.models.manuals + +import com.fasterxml.jackson.databind.ObjectMapper +import com.saudigitus.support_module.utils.Mapper.translateJsonToObject + +class Manual { + private fun toJson(): String = translateJsonToObject().writeValueAsString(this) + + companion object { + fun fromJson(json: String?): List? = if (json != null) { + val mapper = ObjectMapper() + + translateJsonToObject() + .readValue( + json, + mapper.typeFactory.constructCollectionType( + List::class.java, + ManualItem::class.java, + ), + ) + } else { + null + } + } +} \ No newline at end of file diff --git a/support-module/src/main/java/com/saudigitus/support_module/data/models/manuals/ManualItem.kt b/support-module/src/main/java/com/saudigitus/support_module/data/models/manuals/ManualItem.kt new file mode 100644 index 0000000000..ea204eab0f --- /dev/null +++ b/support-module/src/main/java/com/saudigitus/support_module/data/models/manuals/ManualItem.kt @@ -0,0 +1,24 @@ +package com.saudigitus.support_module.data.models.manuals + + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty + +@JsonIgnoreProperties(ignoreUnknown = true) +@Entity +data class ManualItem( + @JsonProperty("uid") + @PrimaryKey val uid: String, + + @JsonProperty("title") + @ColumnInfo(name = "title") val title: String, + + @JsonProperty("subtitle") + @ColumnInfo(name = "subtitle") val subtitle: String?, + + @JsonProperty("path") + @ColumnInfo(name = "path") val path: String? +) diff --git a/support-module/src/main/java/com/saudigitus/support_module/di/AppModule.kt b/support-module/src/main/java/com/saudigitus/support_module/di/AppModule.kt new file mode 100644 index 0000000000..3613dcdc1a --- /dev/null +++ b/support-module/src/main/java/com/saudigitus/support_module/di/AppModule.kt @@ -0,0 +1,37 @@ +package com.saudigitus.support_module.di + +import android.app.Application +import androidx.room.Room +import com.saudigitus.support_module.data.local.ManualsRepository +import com.saudigitus.support_module.data.local.database.AppDatabase +import com.saudigitus.support_module.data.local.repository.ManualsRepositoryImp +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import org.hisp.dhis.android.core.D2 +import org.hisp.dhis.android.core.D2Manager +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object AppModule { + @Provides + @Singleton + fun manualsDatabase(app: Application): AppDatabase = + Room.databaseBuilder( + app, + AppDatabase::class.java, + AppDatabase.DB_NAME + ).build() + + /** + * Inject ManualsRepository + */ + + @Provides + @Singleton fun providesManualRepository(appDatabase: AppDatabase, d2: D2): ManualsRepository { + return ManualsRepositoryImp(appDatabase.manualsDAO(), d2 = d2) + } + +} \ No newline at end of file diff --git a/support-module/src/main/java/com/saudigitus/support_module/ui/ManualState.kt b/support-module/src/main/java/com/saudigitus/support_module/ui/ManualState.kt new file mode 100644 index 0000000000..517d36e555 --- /dev/null +++ b/support-module/src/main/java/com/saudigitus/support_module/ui/ManualState.kt @@ -0,0 +1,15 @@ +package com.saudigitus.support_module.ui + +import com.rizzi.bouquet.ResourceType +import com.rizzi.bouquet.VerticalPdfReaderState +import com.saudigitus.support_module.data.models.manuals.ManualItem +import java.io.File + +data class ManualsUiState( + val isLoading: Boolean = false, + val hasFileLoaded: Boolean = false, + val isDownloading: Boolean = false, + val manualItems : List = emptyList(), + val pdf: File? = null, + val pdfVerticalReaderState: VerticalPdfReaderState? = null +) \ No newline at end of file diff --git a/support-module/src/main/java/com/saudigitus/support_module/ui/Routes.kt b/support-module/src/main/java/com/saudigitus/support_module/ui/Routes.kt index b8bfbd1307..98c92b8fac 100644 --- a/support-module/src/main/java/com/saudigitus/support_module/ui/Routes.kt +++ b/support-module/src/main/java/com/saudigitus/support_module/ui/Routes.kt @@ -4,6 +4,7 @@ sealed class Screen(val route: String) { object Menu : Screen("menu") object Manuals : Screen("manuals") object Support : Screen("support") + object ViewPdf : Screen("pdf_view/{path}") } diff --git a/support-module/src/main/java/com/saudigitus/support_module/ui/components/CustomCard.kt b/support-module/src/main/java/com/saudigitus/support_module/ui/components/CustomCard.kt index c35f04dee3..a12da7281c 100644 --- a/support-module/src/main/java/com/saudigitus/support_module/ui/components/CustomCard.kt +++ b/support-module/src/main/java/com/saudigitus/support_module/ui/components/CustomCard.kt @@ -32,8 +32,6 @@ fun CustomCard(imageResId: Int, title: String, onClick: () -> Unit) { .clickable { onClick() }, colors = CardDefaults.cardColors(containerColor = Color.White), elevation = CardDefaults.cardElevation(50.dp), - - ) { Column( modifier = Modifier.fillMaxSize(), diff --git a/support-module/src/main/java/com/saudigitus/support_module/ui/components/ListCard.kt b/support-module/src/main/java/com/saudigitus/support_module/ui/components/ListCard.kt index a69324fb49..8787936a56 100644 --- a/support-module/src/main/java/com/saudigitus/support_module/ui/components/ListCard.kt +++ b/support-module/src/main/java/com/saudigitus/support_module/ui/components/ListCard.kt @@ -1,24 +1,21 @@ package com.saudigitus.support_module.ui.components import androidx.compose.foundation.Image -import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDownward -import androidx.compose.material.icons.filled.KeyboardArrowRight -import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -28,23 +25,29 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.saudigitus.support_module.R -import com.saudigitus.support_module.ui.MenuScreen -import com.saudigitus.support_module.ui.components.CustomCard +import com.saudigitus.support_module.ui.ManualsUiState @Composable -fun ListCard(imageResId: Int, title: String, subtitle: String, icon: androidx.compose.ui.graphics.vector.ImageVector) { +fun ListCard( + imageResId: Int, + title: String, + subtitle: String, + icon: androidx.compose.ui.graphics.vector.ImageVector, + onClick: () -> Unit, + state: ManualsUiState +) { Card( modifier = Modifier .fillMaxWidth() .size(width = 0.dp, height = 80.dp) .shadow(2.dp, RoundedCornerShape(16.dp)) - .clip(RoundedCornerShape(16.dp)), + .clip(RoundedCornerShape(16.dp)) + .clickable(onClick = onClick), colors = CardDefaults.cardColors(containerColor = Color.White), elevation = CardDefaults.cardElevation(50.dp), ){ @@ -64,28 +67,31 @@ fun ListCard(imageResId: Int, title: String, subtitle: String, icon: androidx.co Column ( modifier = Modifier. width(220.dp), - //.background(Color.Yellow), horizontalAlignment = Alignment.Start, ) { Text( text = title, fontSize = 14.sp, - //fontWeight = FontWeight.Bold, overflow = TextOverflow.Ellipsis, - color = Color.Black + color = Color.Black, + maxLines = 1 ) Text( text = subtitle, fontSize = 12.sp, - color = Color.Gray + color = Color.Gray, + overflow = TextOverflow.Ellipsis, + maxLines = 1 ) } - Icon( - imageVector = icon, - contentDescription = title, - // modifier = Modifier.size(48.dp), - tint = Color.Black - ) + if(!state.isDownloading) + Icon( + imageVector = icon, + contentDescription = title, + tint = Color.Black + ) + if(state.isDownloading) + CircularProgressIndicator() } } } @@ -93,5 +99,5 @@ fun ListCard(imageResId: Int, title: String, subtitle: String, icon: androidx.co @Preview(showBackground = true) @Composable fun MyScreenPreview() { - ListCard(imageResId = R.drawable.manual_icon, title = "Manual title here alfa omega beta", subtitle = "Manual subtitle here alfa", icon = Icons.Default.ArrowDownward) + ListCard(imageResId = R.drawable.manual_icon, title = "Manual title here alfa omega beta", subtitle = "Manual subtitle here alfa", onClick = {}, icon = Icons.Default.ArrowDownward, state = ManualsUiState()) } \ No newline at end of file diff --git a/support-module/src/main/java/com/saudigitus/support_module/ui/manualScreen/AppPdfViewer.kt b/support-module/src/main/java/com/saudigitus/support_module/ui/manualScreen/AppPdfViewer.kt new file mode 100644 index 0000000000..9d9dd43204 --- /dev/null +++ b/support-module/src/main/java/com/saudigitus/support_module/ui/manualScreen/AppPdfViewer.kt @@ -0,0 +1,52 @@ +package com.saudigitus.support_module.ui.manualScreen + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.runtime.Composable +import androidx.navigation.NavHostController +import com.saudigitus.support_module.ui.components.BasicApp +import java.io.File +import androidx.compose.material3.Text +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.rizzi.bouquet.VerticalPDFReader +import com.saudigitus.support_module.R +import timber.log.Timber + +@Composable +fun PdfViewer( + navController: NavHostController, + onBack: () -> Unit = {}, // Placeholder for back action + viewModel: ManualViewModel = hiltViewModel(), + file: File?, +) { + val pdfVerticalReaderState by viewModel.pdfVerticalReaderState.collectAsStateWithLifecycle() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + LaunchedEffect(file) { + if (file != null) { + viewModel.openPdf(file) + } + } + BasicApp( + title = stringResource(id = R.string.user_manual_title), + onBack = onBack, + content = { + if(uiState.hasFileLoaded){ + VerticalPDFReader( + state = pdfVerticalReaderState, + modifier = Modifier + .fillMaxSize() + .background(color = Color.White) + ) + } + }) +} diff --git a/support-module/src/main/java/com/saudigitus/support_module/ui/manualScreen/ManualScreen.kt b/support-module/src/main/java/com/saudigitus/support_module/ui/manualScreen/ManualScreen.kt index b5bcf065c0..f6a78275ca 100644 --- a/support-module/src/main/java/com/saudigitus/support_module/ui/manualScreen/ManualScreen.kt +++ b/support-module/src/main/java/com/saudigitus/support_module/ui/manualScreen/ManualScreen.kt @@ -5,12 +5,17 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDownward +import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -20,18 +25,28 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController +import com.rizzi.bouquet.ResourceType import com.saudigitus.support_module.R import com.saudigitus.support_module.ui.components.BasicApp import com.saudigitus.support_module.ui.components.ListCard import com.saudigitus.support_module.ui.MenuScreen +import com.saudigitus.support_module.ui.Screen import timber.log.Timber +import java.net.URLEncoder +import java.nio.charset.StandardCharsets @Composable fun ManualScreen( navController: NavHostController, onBack: () -> Unit ) { + val viewModel = hiltViewModel() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val context = LocalContext.current + BasicApp( title = stringResource(id = R.string.manuals), onBack = onBack, @@ -39,7 +54,7 @@ fun ManualScreen( Column( modifier = Modifier .fillMaxSize() - .background(color = Color(0xFFFFFFFF)) + .background(color = Color.White) .padding(16.dp), verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.Start @@ -50,26 +65,38 @@ fun ManualScreen( color = Color.Gray ) Spacer(Modifier.height(20.dp)) - ListCard( - imageResId = R.drawable.manual_icon, title = "Manual title here alfa omega zeta", - subtitle = "Manual subtitle here alfas", - icon = Icons.Default.ArrowDownward, - ) - Spacer(Modifier.height(10.dp)) - ListCard( - imageResId = R.drawable.manual_icon, title = "Manual title here", - subtitle = "Manual subtitle here alfa", - icon = Icons.Default.ArrowDownward, - ) - Spacer(Modifier.height(10.dp)) - ListCard( - imageResId = R.drawable.manual_icon, title = "Manual title here", - subtitle = "Manual subtitle here alfa", - icon = Icons.Default.ArrowDownward, - ) + if(uiState.isDownloading){ + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + items(uiState.manualItems) { manual -> + ListCard( + imageResId = R.drawable.manual_icon, title = manual.title, + subtitle = manual.subtitle.toString(), + icon = Icons.Default.ArrowDownward, + onClick = { + val file = viewModel.open(context = context,fileName = manual.uid) + if(file.isFile){ + val encodedPath = URLEncoder.encode( + file.absolutePath, + StandardCharsets.UTF_8.toString() + ) + navController.navigate( + Screen.ViewPdf.route.replace( + "{path}", + encodedPath + ) + ) + } + }, + state = uiState + ) + } + } } - - }) } diff --git a/support-module/src/main/java/com/saudigitus/support_module/ui/manualScreen/ManualViewModel.kt b/support-module/src/main/java/com/saudigitus/support_module/ui/manualScreen/ManualViewModel.kt new file mode 100644 index 0000000000..293daeecf0 --- /dev/null +++ b/support-module/src/main/java/com/saudigitus/support_module/ui/manualScreen/ManualViewModel.kt @@ -0,0 +1,70 @@ +package com.saudigitus.support_module.ui.manualScreen + +import android.content.Context +import android.net.Uri +import android.os.Environment +import android.widget.Toast +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.rizzi.bouquet.ResourceType +import com.rizzi.bouquet.VerticalPdfReaderState +import com.saudigitus.support_module.data.local.ManualsRepository +import com.saudigitus.support_module.ui.ManualsUiState +import com.saudigitus.support_module.utils.Constants.NO_MANUAL_MESSAGE +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import java.io.File +import javax.inject.Inject + +@HiltViewModel +class ManualViewModel @Inject internal constructor( + private val manualsRepository: ManualsRepository, + @ApplicationContext private val context: Context +) : ViewModel() { + private val _uiState = MutableStateFlow(ManualsUiState()) + val uiState = _uiState.asStateFlow() + + init { + getManualsFromDataStore() + } + + private val _pdfVerticalReaderState = MutableStateFlow( VerticalPdfReaderState( + resource = ResourceType.Local(Uri.parse("")), + isZoomEnable = true + )) + val pdfVerticalReaderState = _pdfVerticalReaderState.asStateFlow() + + private fun getManualsFromDataStore(){ + viewModelScope.launch { + _uiState.update {it.copy(isDownloading = true)} + manualsRepository.getManualsDataStore().collect{ manuals -> + manuals.forEach { + manualsRepository.downloadManualToLocal(context = context, url= it.path ?: "", fileName = it.uid) + } + _uiState.update { + it.copy(manualItems = manuals, isDownloading = false) + } + } + } + } + + fun openPdf(file: File){ + _pdfVerticalReaderState.value = VerticalPdfReaderState( + resource = ResourceType.Local(Uri.fromFile(file)), + isZoomEnable = true + ) + _uiState.update {it.copy(hasFileLoaded = true)} + } + + fun open(context: Context, fileName: String): File { + val file = File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "$fileName.pdf") + if (!file.exists()){ + Toast.makeText(context, NO_MANUAL_MESSAGE, Toast.LENGTH_SHORT).show() + } + return file + } +} \ No newline at end of file diff --git a/support-module/src/main/java/com/saudigitus/support_module/utils/AppNavHost.kt b/support-module/src/main/java/com/saudigitus/support_module/utils/AppNavHost.kt index 6847f60bd9..42d7090ea3 100644 --- a/support-module/src/main/java/com/saudigitus/support_module/utils/AppNavHost.kt +++ b/support-module/src/main/java/com/saudigitus/support_module/utils/AppNavHost.kt @@ -7,7 +7,11 @@ import androidx.navigation.compose.composable import com.saudigitus.support_module.ui.MenuScreen import com.saudigitus.support_module.ui.Screen import com.saudigitus.support_module.ui.manualScreen.ManualScreen +import com.saudigitus.support_module.ui.manualScreen.PdfViewer import timber.log.Timber +import java.io.File +import java.net.URLDecoder +import java.nio.charset.StandardCharsets @Composable fun AppNavHost(navController: NavHostController, route: String, activity: Activity) { @@ -28,6 +32,14 @@ fun AppNavHost(navController: NavHostController, route: String, activity: Activi activity.finish() }) } + composable(Screen.ViewPdf.route) { backStackEntry -> + val encodedPath = backStackEntry.arguments?.getString("path") + val filePath = encodedPath?.let { URLDecoder.decode(it, StandardCharsets.UTF_8.toString()) } + val file = if (filePath != null) File(filePath) else null + PdfViewer(navController = navController, file = file, onBack = { + navController.popBackStack() + }) + } } } diff --git a/support-module/src/main/java/com/saudigitus/support_module/utils/Constants.kt b/support-module/src/main/java/com/saudigitus/support_module/utils/Constants.kt index f009fcecc0..0a2e8b8509 100644 --- a/support-module/src/main/java/com/saudigitus/support_module/utils/Constants.kt +++ b/support-module/src/main/java/com/saudigitus/support_module/utils/Constants.kt @@ -2,4 +2,6 @@ package com.saudigitus.support_module.utils object Constants { const val SCREENS_KEY = "screen" + const val MEDIA_DATA_STORE_KEY = "user_manuals" + const val NO_MANUAL_MESSAGE = "Manual not downloaded yet!" } \ No newline at end of file diff --git a/support-module/src/main/java/com/saudigitus/support_module/utils/Mapper.kt b/support-module/src/main/java/com/saudigitus/support_module/utils/Mapper.kt new file mode 100644 index 0000000000..1de6d723f7 --- /dev/null +++ b/support-module/src/main/java/com/saudigitus/support_module/utils/Mapper.kt @@ -0,0 +1,15 @@ +package com.saudigitus.support_module.utils + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.PropertyNamingStrategy +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper + +object Mapper { + fun translateJsonToObject(): ObjectMapper { + return jacksonObjectMapper().apply { + propertyNamingStrategy = PropertyNamingStrategy.LOWER_CAMEL_CASE + setSerializationInclusion(JsonInclude.Include.NON_NULL) + } + } +} \ No newline at end of file diff --git a/support-module/src/main/res/values/strings.xml b/support-module/src/main/res/values/strings.xml index 9f5d50f2de..d887fd19b2 100644 --- a/support-module/src/main/res/values/strings.xml +++ b/support-module/src/main/res/values/strings.xml @@ -9,4 +9,5 @@ Relatar erros de sincronização Relatar outros erros + Manual do Utilizador \ No newline at end of file