From 6873bd53c2b7cfc22ebdb3bfec2670ca315e8932 Mon Sep 17 00:00:00 2001 From: Soumen Date: Fri, 7 Feb 2025 22:47:10 +0530 Subject: [PATCH] #4159 migrated Help Fragment to jetpack Compose --- .../kiwixmobile/help/KiwixHelpFragment.kt | 3 + buildSrc/src/main/kotlin/Libs.kt | 40 +++++ buildSrc/src/main/kotlin/Versions.kt | 12 ++ core/build.gradle.kts | 21 +++ .../kiwixmobile/core/help/HelpFragment.kt | 96 ++++++----- .../kiwix/kiwixmobile/core/help/HelpScreen.kt | 152 ++++++++++++++++++ .../kiwixmobile/core/help/HelpScreenItem.kt | 125 ++++++++++++++ .../core/help/HelpScreenItemDataClass.kt | 22 +++ 8 files changed, 427 insertions(+), 44 deletions(-) create mode 100644 core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpScreen.kt create mode 100644 core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpScreenItem.kt create mode 100644 core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpScreenItemDataClass.kt diff --git a/app/src/main/java/org/kiwix/kiwixmobile/help/KiwixHelpFragment.kt b/app/src/main/java/org/kiwix/kiwixmobile/help/KiwixHelpFragment.kt index 62e467caea..7dc0fdcd2b 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/help/KiwixHelpFragment.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/help/KiwixHelpFragment.kt @@ -22,6 +22,9 @@ import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.help.HelpFragment class KiwixHelpFragment : HelpFragment() { + override val navHostFragmentId: Int + get() = org.kiwix.kiwixmobile.R.id.nav_host_fragment + override fun rawTitleDescriptionMap() = if (sharedPreferenceUtil.isPlayStoreBuildWithAndroid11OrAbove()) { listOf( diff --git a/buildSrc/src/main/kotlin/Libs.kt b/buildSrc/src/main/kotlin/Libs.kt index 8de3a54d4f..7e3ee406ae 100644 --- a/buildSrc/src/main/kotlin/Libs.kt +++ b/buildSrc/src/main/kotlin/Libs.kt @@ -366,4 +366,44 @@ object Libs { */ const val fetch: String = "com.github.tonyofrancis.Fetch:fetch2:" + Versions.fetch const val fetchOkhttp: String = "com.github.tonyofrancis.Fetch:fetch2okhttp:" + Versions.fetch + + /** + * https://developer.android.com/reference/kotlin/androidx/compose/material3 + */ + const val androidx_compose_material3: String = + "androidx.compose.material3:material3-android:" + Versions.androidx_compose_material3_version + + /** + * https://developer.android.com/reference/kotlin/androidx/activity/compose + */ + const val androidx_activity_compose: String = + "androidx.activity:activity-compose:" + Versions.androidx_activity_compose_version + + /** + * https://developer.android.com/develop/ui/compose/documentation + */ + const val androidx_compose_ui: String = + "androidx.compose.ui:ui:" + Versions.androidx_compose_ui_version + + const val androidx_compose_bom: String = + "androidx.compose:compose-bom:" + Versions.androidx_compose_bom_version + + const val androidx_compose_tooling_preview: String = + "androidx.compose.ui:ui-tooling-preview" + + const val androidx_compose_runtime_livedata: String = + "androidx.compose.runtime:runtime-livedata" + + const val androidx_compose_runtime_rxjava2: String = + "androidx.compose.runtime:runtime-rxjava2" + + /** + * testing libraries for compose + */ + const val androidx_compose_ui_test_junit4: String = + "androidx.compose.ui:ui-test-junit4" + + const val androidx_compose_ui_tooling: String = + "androidx.compose.ui:ui-tooling" + } diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index df019ada76..ced265fe72 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -115,6 +115,18 @@ object Versions { const val keeper = "0.16.1" const val fetch: String = "3.4.1" + + const val org_jetbrains_kotlin_plugin_compose = "2.1.10" + + const val kotlin_compiler_extension_version = "1.5.15" + + const val androidx_compose_material3_version = "1.3.1" + + const val androidx_activity_compose_version = "1.10.0" + + const val androidx_compose_ui_version = "1.7.7" + + const val androidx_compose_bom_version = "2025.01.01" } /** diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 20216805f3..1846f17850 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -13,6 +13,8 @@ buildscript { } plugins { `android-library` + id("org.jetbrains.kotlin.android") + id("org.jetbrains.kotlin.plugin.compose") version Versions.org_jetbrains_kotlin_plugin_compose } plugins.apply(KiwixConfigurationPlugin::class) apply(plugin = "io.objectbox") @@ -26,6 +28,12 @@ android { isMinifyEnabled = false } } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = Versions.kotlin_compiler_extension_version + } } fun shouldUseLocalVersion() = File(projectDir, "libs").exists() @@ -63,4 +71,17 @@ dependencies { implementation(Libs.kotlinx_coroutines_android) implementation(Libs.kotlinx_coroutines_rx3) implementation(Libs.zxing) + + implementation(Libs.androidx_compose_material3) + implementation(Libs.androidx_activity_compose) + + implementation(Libs.androidx_compose_ui) + implementation(platform(Libs.androidx_compose_bom)) + implementation(Libs.androidx_compose_ui_tooling) + implementation(Libs.androidx_compose_runtime_livedata) + implementation(Libs.androidx_compose_runtime_rxjava2) + + // For Compose UI Testing + androidTestImplementation(Libs.androidx_compose_ui_test_junit4) + debugImplementation(Libs.androidx_compose_ui_tooling) } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpFragment.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpFragment.kt index 3765ea7285..c3781b73ff 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpFragment.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpFragment.kt @@ -17,83 +17,91 @@ */ package org.kiwix.kiwixmobile.core.help +import android.content.Context import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar -import androidx.recyclerview.widget.DividerItemDecoration +import androidx.compose.ui.platform.ComposeView +import androidx.navigation.Navigation import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.base.BaseActivity import org.kiwix.kiwixmobile.core.base.BaseFragment -import org.kiwix.kiwixmobile.core.databinding.FragmentHelpBinding -import org.kiwix.kiwixmobile.core.error.DiagnosticReportActivity -import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.start import org.kiwix.kiwixmobile.core.main.CoreMainActivity import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import javax.inject.Inject @Suppress("UnnecessaryAbstractClass") abstract class HelpFragment : BaseFragment() { + @Inject lateinit var sharedPreferenceUtil: SharedPreferenceUtil - private var fragmentHelpBinding: FragmentHelpBinding? = null - protected open fun rawTitleDescriptionMap(): List> = emptyList() - override val fragmentToolbar: Toolbar? by lazy { - fragmentHelpBinding?.root?.findViewById(R.id.toolbar) - } - override val fragmentTitle: String? by lazy { getString(R.string.menu_help) } - private val titleDescriptionMap by lazy { - rawTitleDescriptionMap().associate { (title, description) -> - val descriptionValue = when (description) { - is String -> description - is Int -> resources.getStringArray(description).joinToString(separator = "\n") - else -> { - throw IllegalArgumentException("Invalid description resource type for title: $title") - } - } + protected abstract val navHostFragmentId: Int - getString(title) to descriptionValue + // Instead of keeping the XML binding, we now directly return a ComposeView. + protected open fun createFragmentView( + inflater: LayoutInflater, + container: ViewGroup? + ): View { + return ComposeView(requireContext()).apply { + setContent { + // Create the helpScreen data using your rawTitleDescriptionMap. + val helpScreenData = transformToHelpScreenData( + requireContext(), + rawTitleDescriptionMap() + ) + // Retrieve the NavController if your composable needs it. + val navController = Navigation.findNavController(requireActivity(), navHostFragmentId) + // Call your HelpScreen composable. + HelpScreen(data = helpScreenData, navController = navController) + } } } + // Each subclass is responsible for providing its own raw data. + protected open fun rawTitleDescriptionMap(): List> = emptyList() + + // The following properties are now optional – if no longer use an XML toolbar or title, + // we can remove or update these accordingly. + override val fragmentToolbar: Toolbar? by lazy { + // Already Applied ad TopAppBAr in scaffold in composable + null + } + override val fragmentTitle: String? by lazy { getString(R.string.menu_help) } + override fun inject(baseActivity: BaseActivity) { (baseActivity as CoreMainActivity).cachedComponent.inject(this) } + // Remove or adjust onViewCreated if you no longer need to manipulate XML-based views. override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - val activity = requireActivity() as AppCompatActivity - fragmentHelpBinding?.activityHelpDiagnosticImageView?.setOnClickListener { - sendDiagnosticReport() - } - fragmentHelpBinding?.activityHelpDiagnosticTextView?.setOnClickListener { - sendDiagnosticReport() - } - fragmentHelpBinding?.activityHelpRecyclerView?.addItemDecoration( - DividerItemDecoration(activity, DividerItemDecoration.VERTICAL) - ) - fragmentHelpBinding?.activityHelpRecyclerView?.adapter = HelpAdapter(titleDescriptionMap) + // Any additional logic that is independent of the XML layout can be kept here. } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - fragmentHelpBinding = - FragmentHelpBinding.inflate(inflater, container, false) - return fragmentHelpBinding?.root - } - - private fun sendDiagnosticReport() { - requireActivity().start() - } + ): View? = createFragmentView(inflater, container) +} - override fun onDestroyView() { - super.onDestroyView() - fragmentHelpBinding = null +// Util function to modify the data accordingly +fun transformToHelpScreenData( + context: Context, + rawTitleDescriptionMap: List> +): List { + return rawTitleDescriptionMap.map { (titleResId, description) -> + val title = context.getString(titleResId) + val descriptionValue = when (description) { + is String -> description + is Int -> context.resources.getStringArray(description).joinToString(separator = "\n") + else -> { + throw IllegalArgumentException("Invalid description resource type for title: $titleResId") + } + } + HelpScreenItemDataClass(title, descriptionValue) } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpScreen.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpScreen.kt new file mode 100644 index 0000000000..2cba40c595 --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpScreen.kt @@ -0,0 +1,152 @@ +/* + * Kiwix Android + * Copyright (c) 2025 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package org.kiwix.kiwixmobile.core.help + +import android.app.Activity +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment + +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +import androidx.navigation.NavController +import org.kiwix.kiwixmobile.core.R +import org.kiwix.kiwixmobile.core.error.DiagnosticReportActivity +import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.start + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HelpScreen( + modifier: Modifier = Modifier, + data: List, + navController: NavController +) { + val context = LocalContext.current + + val isDarkTheme = isSystemInDarkTheme() + + val backgroundColor = + if (isDarkTheme) colorResource(id = R.color.mine_shaft_gray900) else Color.White + val dividerColor = + if (isDarkTheme) colorResource(id = R.color.mine_shaft_gray600) else colorResource( + id = R.color.mine_shaft_gray350 + ) + + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + TopAppBar( + title = { + Text( + modifier = modifier.padding(start = 16.dp), + text = stringResource(id = R.string.menu_help), + color = Color.White // Set title text color to white + ) + }, + navigationIcon = { + IconButton(onClick = navController::popBackStack) { + Icon( + imageVector = Icons.Filled.ArrowBack, + contentDescription = "Back", + tint = Color.White // Set navigation icon color to white + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = Color.Black // Set top app bar background color to black + ) + ) + }, + containerColor = backgroundColor + ) { + + Column( + modifier = Modifier + .padding(it) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + (context as? Activity)?.start() + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start + ) { + Image( + painter = painterResource(R.drawable.ic_feedback_orange_24dp), + contentDescription = "Feedback", + modifier = Modifier + .padding(16.dp) + ) + + Text( + text = stringResource(R.string.send_report), + color = if (isDarkTheme) Color.LightGray else Color.DarkGray, + fontSize = 18.sp + ) + } + + LazyColumn( + modifier = Modifier + .fillMaxWidth() + ) { + itemsIndexed(data, key = { _, item -> item.title }) { index, item -> + HorizontalDivider( + color = dividerColor + ) + HelpScreenItem(data = item) + } + item { + HorizontalDivider( + color = dividerColor + ) + } + } + } + } +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpScreenItem.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpScreenItem.kt new file mode 100644 index 0000000000..e9a90cdb13 --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpScreenItem.kt @@ -0,0 +1,125 @@ +/* + * Kiwix Android + * Copyright (c) 2025 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package org.kiwix.kiwixmobile.core.help + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.material.icons.Icons +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material3.Icon +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.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun HelpScreenItem( + modifier: Modifier = Modifier, + data: HelpScreenItemDataClass, + initiallyOpened: Boolean = false +) { + var isOpen by remember { mutableStateOf(initiallyOpened) } + val isDarkTheme = isSystemInDarkTheme() + val itemColor = if (isDarkTheme) Color.White else Color.Black + val arrowRotation by animateFloatAsState( + targetValue = if (isOpen) 180f else 0f, + animationSpec = tween(300), + label = "arrowRotation" + ) + + val interactionSource = remember(::MutableInteractionSource) + + Column( + modifier = modifier + .fillMaxWidth() + .padding(top = 12.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .clickable(interactionSource = interactionSource, indication = null, onClick = { + isOpen = !isOpen + }) + .padding(horizontal = 16.dp) + ) { + Text( + text = data.title, + fontSize = 18.sp, + color = itemColor, + fontWeight = FontWeight.SemiBold + ) + Icon( + imageVector = Icons.Default.KeyboardArrowDown, + contentDescription = "Open or Close DropDown", + modifier = Modifier + .graphicsLayer { + rotationZ = arrowRotation + } + .size(46.dp), + tint = itemColor + ) + } + + Spacer(modifier = Modifier.height(12.dp)) + + AnimatedVisibility(visible = isOpen) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp) + ) { + Text( + text = data.description, + fontSize = 16.sp, + textAlign = TextAlign.Left, + color = itemColor, + modifier = Modifier.padding(bottom = 8.dp) + ) + } + } + } +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpScreenItemDataClass.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpScreenItemDataClass.kt new file mode 100644 index 0000000000..6e6fb74af3 --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpScreenItemDataClass.kt @@ -0,0 +1,22 @@ +/* + * Kiwix Android + * Copyright (c) 2025 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package org.kiwix.kiwixmobile.core.help + +//same as HelpItem data class in HelpAdapter.kt +data class HelpScreenItemDataClass(val title: String, val description: String)