From 6a79c2709b42906be1145dc5132866d109f6791f Mon Sep 17 00:00:00 2001 From: Kota-Jagadeesh Date: Tue, 24 Mar 2026 11:31:40 +0530 Subject: [PATCH 01/10] feat: migrate AboutActivity UI to Jetpack Compose --- .../java/fr/free/nrw/commons/AboutActivity.kt | 392 +++++++++++------- 1 file changed, 242 insertions(+), 150 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/AboutActivity.kt b/app/src/main/java/fr/free/nrw/commons/AboutActivity.kt index 865ad3ddb13..ac4c26dab60 100644 --- a/app/src/main/java/fr/free/nrw/commons/AboutActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/AboutActivity.kt @@ -6,202 +6,294 @@ import android.content.Intent import android.content.Intent.ACTION_VIEW import android.net.Uri import android.os.Bundle -import android.view.Menu -import android.view.MenuItem -import android.view.View -import android.widget.ArrayAdapter -import android.widget.LinearLayout -import android.widget.Spinner -import fr.free.nrw.commons.CommonsApplication.Companion.instance -import fr.free.nrw.commons.databinding.ActivityAboutBinding +import androidx.activity.compose.setContent +import androidx.compose.animation.core.* +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material.icons.filled.Share +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.net.toUri import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.ui.widget.HtmlTextView import fr.free.nrw.commons.utils.ConfigUtils.getVersionNameWithSha import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog -import java.util.Collections -import androidx.core.net.toUri -import fr.free.nrw.commons.utils.applyEdgeToEdgeTopInsets import fr.free.nrw.commons.utils.handleWebUrl -import fr.free.nrw.commons.utils.setUnderlinedText +import java.util.* + +// grouped the actions into a single class for the betteer scalability +data class AboutActions( + val onBackClick: () -> Unit, + val onShareClick: () -> Unit, + val onLaunchFacebook: () -> Unit, + val onLaunchGithub: () -> Unit, + val onLaunchWebsite: () -> Unit, + val onRateUs: () -> Unit, + val onUserGuide: () -> Unit, + val onPrivacyPolicy: () -> Unit, + val onTranslate: () -> Unit, + val onCredits: () -> Unit, + val onFaq: () -> Unit +) -/** - * Represents about screen of this app - */ class AboutActivity : BaseActivity() { - /* - This View Binding class is auto-generated for each xml file. The format is usually the name - of the file with PascalCasing (The underscore characters will be ignored). - More information is available at https://developer.android.com/topic/libraries/view-binding - */ - private var binding: ActivityAboutBinding? = null - - /** - * This method helps in the creation About screen - * - * @param savedInstanceState Data bundle - */ - @SuppressLint("StringFormatInvalid") //TODO: - public override fun onCreate(savedInstanceState: Bundle?) { + + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - /* - Instead of just setting the view with the xml file. We need to use View Binding class. - */ - binding = ActivityAboutBinding.inflate(layoutInflater) - val view: View = binding!!.root - applyEdgeToEdgeTopInsets(binding!!.toolbarLayout) - setContentView(view) - - setSupportActionBar(binding!!.toolbarBinding.toolbar) - supportActionBar!!.setDisplayHomeAsUpEnabled(true) - val aboutText = getString(R.string.about_license) - /* - We can then access all the views by just using the id names like this. - camelCasing is used with underscore characters being ignored. - */ - binding!!.aboutLicense.setHtmlText(aboutText) - - @SuppressLint("StringFormatMatches") // TODO: - val improveText = - String.format(getString(R.string.about_improve), Urls.NEW_ISSUE_URL) - binding!!.aboutImprove.setHtmlText(improveText) - binding!!.aboutVersion.text = applicationContext.getVersionNameWithSha() - - binding!!.aboutFaq.setUnderlinedText(R.string.about_faq) - binding!!.aboutRateUs.setUnderlinedText(R.string.about_rate_us) - binding!!.aboutUserGuide.setUnderlinedText(R.string.user_guide) - binding!!.aboutPrivacyPolicy.setUnderlinedText(R.string.about_privacy_policy) - binding!!.aboutTranslate.setUnderlinedText(R.string.about_translate) - binding!!.aboutCredits.setUnderlinedText(R.string.about_credits) - - /* - To set listeners, we can create a separate method and use lambda syntax. - */ - binding!!.facebookLaunchIcon.setOnClickListener(::launchFacebook) - binding!!.githubLaunchIcon.setOnClickListener(::launchGithub) - binding!!.websiteLaunchIcon.setOnClickListener(::launchWebsite) - binding!!.aboutRateUs.setOnClickListener(::launchRatings) - binding!!.aboutCredits.setOnClickListener(::launchCredits) - binding!!.aboutPrivacyPolicy.setOnClickListener(::launchPrivacyPolicy) - binding!!.aboutUserGuide.setOnClickListener(::launchUserGuide) - binding!!.aboutFaq.setOnClickListener(::launchFrequentlyAskedQuesions) - binding!!.aboutTranslate.setOnClickListener(::launchTranslate) + setContent { + CommonsTheme { + val actions = AboutActions( + onBackClick = { finish() }, + onShareClick = { shareApp() }, + onLaunchFacebook = { launchFacebook() }, + onLaunchGithub = { launchGithub() }, + onLaunchWebsite = { launchWebsite() }, + onRateUs = { launchRatings() }, + onUserGuide = { launchUserGuide() }, + onPrivacyPolicy = { launchPrivacyPolicy() }, + onTranslate = { launchTranslate() }, + onCredits = { launchCredits() }, + onFaq = { launchFrequentlyAskedQuestions() } + ) + + AboutScreen( + version = applicationContext.getVersionNameWithSha(), + actions = actions + ) + } + } } - override fun onSupportNavigateUp(): Boolean { - onBackPressed() - return true + // logic methods remain within the Activity to keep theUI pure + @SuppressLint("StringFormatInvalid") + private fun shareApp() { + val shareText = String.format(getString(R.string.share_text), Urls.PLAY_STORE_URL_PREFIX + packageName) + val sendIntent = Intent(Intent.ACTION_SEND).apply { + putExtra(Intent.EXTRA_TEXT, shareText) + type = "text/plain" + } + startActivity(Intent.createChooser(sendIntent, getString(R.string.share_via))) } - fun launchFacebook(view: View?) { - val intent: Intent + private fun launchFacebook() { try { - intent = Intent(ACTION_VIEW, Urls.FACEBOOK_APP_URL.toUri()) - intent.setPackage(Urls.FACEBOOK_PACKAGE_NAME) - startActivity(intent) + startActivity(Intent(ACTION_VIEW, Urls.FACEBOOK_APP_URL.toUri()).setPackage(Urls.FACEBOOK_PACKAGE_NAME)) } catch (e: Exception) { handleWebUrl(this, Urls.FACEBOOK_WEB_URL.toUri()) } } - fun launchGithub(view: View?) { - val intent: Intent + private fun launchGithub() { try { - intent = Intent(ACTION_VIEW, Urls.GITHUB_REPO_URL.toUri()) - intent.setPackage(Urls.GITHUB_PACKAGE_NAME) - startActivity(intent) + startActivity(Intent(ACTION_VIEW, Urls.GITHUB_REPO_URL.toUri()).setPackage(Urls.GITHUB_PACKAGE_NAME)) } catch (e: Exception) { handleWebUrl(this, Urls.GITHUB_REPO_URL.toUri()) } } - fun launchWebsite(view: View?) { - handleWebUrl(this, Urls.WEBSITE_URL.toUri()) - } + private fun launchWebsite() = handleWebUrl(this, Urls.WEBSITE_URL.toUri()) + private fun launchCredits() = handleWebUrl(this, Urls.CREDITS_URL.toUri()) + private fun launchUserGuide() = handleWebUrl(this, Urls.USER_GUIDE_URL.toUri()) + private fun launchPrivacyPolicy() = handleWebUrl(this, BuildConfig.PRIVACY_POLICY_URL.toUri()) + private fun launchFrequentlyAskedQuestions() = handleWebUrl(this, Urls.FAQ_URL.toUri()) - fun launchRatings(view: View?) { + private fun launchRatings() { try { - startActivity( - Intent( - ACTION_VIEW, - (Urls.PLAY_STORE_PREFIX + packageName).toUri() - ) - ) + startActivity(Intent(ACTION_VIEW, (Urls.PLAY_STORE_PREFIX + packageName).toUri())) } catch (_: ActivityNotFoundException) { handleWebUrl(this, (Urls.PLAY_STORE_URL_PREFIX + packageName).toUri()) } } - fun launchCredits(view: View?) { - handleWebUrl(this, Urls.CREDITS_URL.toUri()) + private fun launchTranslate() { + val instance = CommonsApplication.instance + val sortedNames = instance.languageLookUpTable!!.getCanonicalNames().toMutableList() + Collections.sort(sortedNames) + val spinner = android.widget.Spinner(this).apply { + adapter = android.widget.ArrayAdapter(this@AboutActivity, android.R.layout.simple_spinner_dropdown_item, sortedNames) + } + showAlertDialog(this, getString(R.string.about_translate_title), getString(R.string.about_translate_message), + getString(R.string.about_translate_proceed), getString(R.string.about_translate_cancel), + { + val langCode = instance.languageLookUpTable!!.getCodes()[spinner.selectedItemPosition] + handleWebUrl(this, (Urls.TRANSLATE_WIKI_URL + langCode).toUri()) + }, {}, spinner + ) } +} +//custom theme wrapper to centralize the color logic +@Composable +fun CommonsTheme(content: @Composable () -> Unit) { + val isDarkTheme = isSystemInDarkTheme() + val background = if (isDarkTheme) colorResource(R.color.main_background_dark) else colorResource(R.color.main_background_light) - fun launchUserGuide(view: View?) { - handleWebUrl(this, Urls.USER_GUIDE_URL.toUri()) + val colorScheme = if (isDarkTheme) { + darkColorScheme(primary = colorResource(R.color.primaryColor), background = background, onBackground = Color.White) + } else { + lightColorScheme(primary = colorResource(R.color.primaryColor), background = background, onBackground = Color.Black) } - fun launchPrivacyPolicy(view: View?) { - handleWebUrl(this, BuildConfig.PRIVACY_POLICY_URL.toUri()) + MaterialTheme(colorScheme = colorScheme) { + Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { + content() + } } +} - fun launchFrequentlyAskedQuesions(view: View?) { - handleWebUrl(this, Urls.FAQ_URL.toUri()) +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AboutScreen( + version: String, + actions: AboutActions +) { + val logoAlpha = remember { Animatable(0f) } + // smoooth fade in for the app logo on opening the screen + LaunchedEffect(Unit) { + logoAlpha.animateTo(1f, animationSpec = tween(1000)) } - override fun onCreateOptionsMenu(menu: Menu): Boolean { - val inflater = menuInflater - inflater.inflate(R.menu.menu_about, menu) - return super.onCreateOptionsMenu(menu) - } + Scaffold( + topBar = { + CenterAlignedTopAppBar( + title = { + Text(text = stringResource(R.string.about), fontWeight = FontWeight.Bold, maxLines = 1, + softWrap = false, style = MaterialTheme.typography.titleLarge) + }, + navigationIcon = { + IconButton(onClick = actions.onBackClick) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, null, tint = Color.White) + } + }, + actions = { + IconButton(onClick = actions.onShareClick) { + Icon(Icons.Default.Share, null, tint = Color.White) + } + }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors(containerColor = MaterialTheme.colorScheme.primary, titleContentColor = Color.White) + ) + }, + containerColor = Color.Transparent + ) { padding -> + Column( + modifier = Modifier.fillMaxSize().padding(padding).verticalScroll(rememberScrollState()).padding(horizontal = 20.dp, vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box(modifier = Modifier.alpha(logoAlpha.value)) { + Image(painter = painterResource(R.drawable.ic_launcher), contentDescription = stringResource(R.string.commons_logo), + modifier = Modifier.size(110.dp).clip(CircleShape)) + } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.share_app_icon -> { - val shareText = String.format( - getString(R.string.share_text), - Urls.PLAY_STORE_URL_PREFIX + this.packageName - ) - val sendIntent = Intent() - sendIntent.setAction(Intent.ACTION_SEND) - sendIntent.putExtra(Intent.EXTRA_TEXT, shareText) - sendIntent.setType("text/plain") - startActivity(Intent.createChooser(sendIntent, getString(R.string.share_via))) - return true + Text(text = stringResource(R.string.app_name), style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onBackground) + + // tucked the version info inside a small pill shaped chip to look better + AssistChip( + onClick = { }, + label = { Text(version, style = MaterialTheme.typography.labelSmall) }, + colors = AssistChipDefaults.assistChipColors(labelColor = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.6f)), + modifier = Modifier.padding(top = 4.dp, bottom = 16.dp) + ) + + HtmlText(stringResource(R.string.about_license)) + HtmlText(String.format(stringResource(R.string.about_improve), Urls.NEW_ISSUE_URL), Modifier.padding(top = 12.dp)) + + // grouped the social media icons in a neat row + Row(modifier = Modifier.padding(vertical = 24.dp), horizontalArrangement = Arrangement.spacedBy(28.dp)) { + EnhancedSocialIcon(R.drawable.ic_action_website, actions.onLaunchWebsite) + EnhancedSocialIcon(R.drawable.ic_action_facebook, actions.onLaunchFacebook) + EnhancedSocialIcon(R.drawable.ic_action_github, actions.onLaunchGithub) } - else -> return super.onOptionsItemSelected(item) + // grouping the all link items into a single card layout + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = if (isSystemInDarkTheme()) Color(0xFF1E1E1E) else Color.White), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column { + EnhancedLinkRow(stringResource(R.string.about_rate_us), actions.onRateUs) + DividerRow() + EnhancedLinkRow(stringResource(R.string.user_guide), actions.onUserGuide) + DividerRow() + EnhancedLinkRow(stringResource(R.string.about_privacy_policy), actions.onPrivacyPolicy) + DividerRow() + EnhancedLinkRow(stringResource(R.string.about_translate), actions.onTranslate) + DividerRow() + EnhancedLinkRow(stringResource(R.string.about_credits), actions.onCredits) + DividerRow() + EnhancedLinkRow(stringResource(R.string.about_faq), actions.onFaq) + } + } + Spacer(modifier = Modifier.height(32.dp)) } } +} - fun launchTranslate(view: View?) { - val sortedLocalizedNamesRef = instance.languageLookUpTable!!.getCanonicalNames() - Collections.sort(sortedLocalizedNamesRef) - val languageAdapter = ArrayAdapter( - this@AboutActivity, - android.R.layout.simple_spinner_dropdown_item, sortedLocalizedNamesRef - ) - val spinner = Spinner(this@AboutActivity) - spinner.layoutParams = - LinearLayout.LayoutParams( - LinearLayout.LayoutParams.WRAP_CONTENT, - LinearLayout.LayoutParams.WRAP_CONTENT - ) - spinner.adapter = languageAdapter - spinner.gravity = 17 - spinner.setPadding(50, 0, 0, 0) +@Composable +fun EnhancedSocialIcon(drawableId: Int, onClick: () -> Unit) { + val tint = MaterialTheme.colorScheme.primary + Surface(modifier = Modifier.size(52.dp).clickable { onClick() }, shape = CircleShape, color = tint.copy(alpha = 0.1f) + ) { + Box(contentAlignment = Alignment.Center) { + Image(painter = painterResource(drawableId), contentDescription = null, colorFilter = ColorFilter.tint(tint), modifier = Modifier.size(28.dp)) + } + } +} + +@Composable +fun EnhancedLinkRow(text: String, onClick: () -> Unit) { + Row(modifier = Modifier.fillMaxWidth().clickable { onClick() }.padding(16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) { + Text(text = text, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurface) + Icon(imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = null, tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f), modifier = Modifier.size(20.dp)) + } +} - val positiveButtonRunnable = Runnable { - val langCode = instance.languageLookUpTable!!.getCodes()[spinner.selectedItemPosition] - handleWebUrl(this@AboutActivity, (Urls.TRANSLATE_WIKI_URL + langCode).toUri()) +@Composable +fun DividerRow() { + HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)) +} + +@Composable +fun HtmlText(html: String, modifier: Modifier = Modifier) { + val textColor = if (isSystemInDarkTheme()) android.graphics.Color.WHITE else android.graphics.Color.BLACK + // bridge thhe existing xml-based HtmlTextView into the compose ui + AndroidView( + modifier = modifier, + factory = { context -> HtmlTextView(context).apply { gravity = android.view.Gravity.CENTER } }, + update = { view -> + view.setTextColor(textColor) + view.setHtmlText(html) } - showAlertDialog( - this, - getString(R.string.about_translate_title), - getString(R.string.about_translate_message), - getString(R.string.about_translate_proceed), - getString(R.string.about_translate_cancel), - positiveButtonRunnable, - {}, - spinner + ) +} + +@Preview(showBackground = true) +@Composable +fun AboutScreenPreview() { + CommonsTheme { + AboutScreen( + version = "v6.4.0-debug-master", + actions = AboutActions({}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}) ) } -} +} \ No newline at end of file From 74c0c89bc50d18a96f16ebcbe8c97768dbfcc80a Mon Sep 17 00:00:00 2001 From: Kota-Jagadeesh Date: Tue, 24 Mar 2026 11:58:02 +0530 Subject: [PATCH 02/10] fix: change visibility of launch methods to internal for unit testing --- .../java/fr/free/nrw/commons/AboutActivity.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/AboutActivity.kt b/app/src/main/java/fr/free/nrw/commons/AboutActivity.kt index ac4c26dab60..46ef3c0a497 100644 --- a/app/src/main/java/fr/free/nrw/commons/AboutActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/AboutActivity.kt @@ -85,7 +85,7 @@ class AboutActivity : BaseActivity() { // logic methods remain within the Activity to keep theUI pure @SuppressLint("StringFormatInvalid") - private fun shareApp() { + internal fun shareApp() { val shareText = String.format(getString(R.string.share_text), Urls.PLAY_STORE_URL_PREFIX + packageName) val sendIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_TEXT, shareText) @@ -94,7 +94,7 @@ class AboutActivity : BaseActivity() { startActivity(Intent.createChooser(sendIntent, getString(R.string.share_via))) } - private fun launchFacebook() { + internal fun launchFacebook() { try { startActivity(Intent(ACTION_VIEW, Urls.FACEBOOK_APP_URL.toUri()).setPackage(Urls.FACEBOOK_PACKAGE_NAME)) } catch (e: Exception) { @@ -102,7 +102,7 @@ class AboutActivity : BaseActivity() { } } - private fun launchGithub() { + internal fun launchGithub() { try { startActivity(Intent(ACTION_VIEW, Urls.GITHUB_REPO_URL.toUri()).setPackage(Urls.GITHUB_PACKAGE_NAME)) } catch (e: Exception) { @@ -110,13 +110,13 @@ class AboutActivity : BaseActivity() { } } - private fun launchWebsite() = handleWebUrl(this, Urls.WEBSITE_URL.toUri()) - private fun launchCredits() = handleWebUrl(this, Urls.CREDITS_URL.toUri()) - private fun launchUserGuide() = handleWebUrl(this, Urls.USER_GUIDE_URL.toUri()) - private fun launchPrivacyPolicy() = handleWebUrl(this, BuildConfig.PRIVACY_POLICY_URL.toUri()) - private fun launchFrequentlyAskedQuestions() = handleWebUrl(this, Urls.FAQ_URL.toUri()) + internal fun launchWebsite() = handleWebUrl(this, Urls.WEBSITE_URL.toUri()) + internal fun launchCredits() = handleWebUrl(this, Urls.CREDITS_URL.toUri()) + internal fun launchUserGuide() = handleWebUrl(this, Urls.USER_GUIDE_URL.toUri()) + internal fun launchPrivacyPolicy() = handleWebUrl(this, BuildConfig.PRIVACY_POLICY_URL.toUri()) + internal fun launchFrequentlyAskedQuestions() = handleWebUrl(this, Urls.FAQ_URL.toUri()) - private fun launchRatings() { + internal fun launchRatings() { try { startActivity(Intent(ACTION_VIEW, (Urls.PLAY_STORE_PREFIX + packageName).toUri())) } catch (_: ActivityNotFoundException) { @@ -124,7 +124,7 @@ class AboutActivity : BaseActivity() { } } - private fun launchTranslate() { + internal fun launchTranslate() { val instance = CommonsApplication.instance val sortedNames = instance.languageLookUpTable!!.getCanonicalNames().toMutableList() Collections.sort(sortedNames) From 038d8c9fa44f5b64b775a2c82928ec0cca25ce80 Mon Sep 17 00:00:00 2001 From: Kota-Jagadeesh Date: Tue, 24 Mar 2026 12:10:35 +0530 Subject: [PATCH 03/10] fix: update unit tests to support AboutActivity compose migration and fix visibility issues --- .../free/nrw/commons/AboutActivityUnitTests.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/AboutActivityUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/AboutActivityUnitTests.kt index 56549fa1c73..5b6a136a7ad 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/AboutActivityUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/AboutActivityUnitTests.kt @@ -43,7 +43,7 @@ class AboutActivityUnitTests { @Test @Throws(Exception::class) fun testLaunchFacebook() { - activity.launchFacebook(null) + activity.launchFacebook() val shadowActivity: ShadowActivity = Shadows.shadowOf(activity) val startedIntent = shadowActivity.nextStartedActivity Assert.assertEquals(startedIntent.action, "android.intent.action.VIEW") @@ -54,43 +54,43 @@ class AboutActivityUnitTests { @Test @Throws(Exception::class) fun testLaunchGithub() { - activity.launchGithub(null) + activity.launchGithub() } @Test @Throws(Exception::class) fun testLaunchWebsite() { - activity.launchWebsite(null) + activity.launchWebsite() } @Test @Throws(Exception::class) fun testLaunchRatings() { - activity.launchRatings(null) + activity.launchRatings() } @Test @Throws(Exception::class) fun testLaunchCredits() { - activity.launchCredits(null) + activity.launchCredits() } @Test @Throws(Exception::class) fun testLaunchPrivacyPolicy() { - activity.launchPrivacyPolicy(null) + activity.launchPrivacyPolicy() } @Test @Throws(Exception::class) fun testLaunchUserGuide() { - activity.launchUserGuide(null) + activity.launchUserGuide() } @Test @Throws(Exception::class) fun testLaunchFrequentlyAskedQuestions() { - activity.launchFrequentlyAskedQuesions(null) + activity.launchFrequentlyAskedQuestions() } @Test From 14e7c0f373f37007e73cbd3b6e6b44e9af60579c Mon Sep 17 00:00:00 2001 From: Kota-Jagadeesh Date: Wed, 25 Mar 2026 20:48:12 +0530 Subject: [PATCH 04/10] feat: create reusable CommonsTheme with Material 3 design tokens --- .../fr/free/nrw/commons/theme/CommonsTheme.kt | 46 +++++ app/src/main/res/layout/activity_about.xml | 178 ------------------ 2 files changed, 46 insertions(+), 178 deletions(-) create mode 100644 app/src/main/java/fr/free/nrw/commons/theme/CommonsTheme.kt delete mode 100644 app/src/main/res/layout/activity_about.xml diff --git a/app/src/main/java/fr/free/nrw/commons/theme/CommonsTheme.kt b/app/src/main/java/fr/free/nrw/commons/theme/CommonsTheme.kt new file mode 100644 index 00000000000..70770440004 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/theme/CommonsTheme.kt @@ -0,0 +1,46 @@ +package fr.free.nrw.commons.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import fr.free.nrw.commons.R + +@Composable +fun CommonsTheme(content: @Composable () -> Unit) { + val isDarkTheme = isSystemInDarkTheme() + val background = if (isDarkTheme) + colorResource(R.color.main_background_dark) + else + colorResource(R.color.main_background_light) + + val colorScheme = if (isDarkTheme) { + darkColorScheme( + primary = colorResource(R.color.primaryColor), + background = background, + onBackground = Color.White, + surfaceVariant = Color(0xFF1E1E1E), + onSurface = Color.White + ) + } else { + lightColorScheme( + primary = colorResource(R.color.primaryColor), + background = background, + onBackground = Color.Black, + surfaceVariant = Color.White, + onSurface = Color.Black + ) + } + + MaterialTheme(colorScheme = colorScheme) { + Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { + content() + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml deleted file mode 100644 index b0f34d5a8dd..00000000000 --- a/app/src/main/res/layout/activity_about.xml +++ /dev/null @@ -1,178 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From d0bb8e9a33cd8dd53f7cda78ab01e4b5e9d7a130 Mon Sep 17 00:00:00 2001 From: Kota-Jagadeesh Date: Wed, 25 Mar 2026 20:50:16 +0530 Subject: [PATCH 05/10] refactor: AboutActivity to extract design tokens to CommonsTheme --- .../java/fr/free/nrw/commons/AboutActivity.kt | 21 ++----------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/AboutActivity.kt b/app/src/main/java/fr/free/nrw/commons/AboutActivity.kt index 46ef3c0a497..8f1d5688310 100644 --- a/app/src/main/java/fr/free/nrw/commons/AboutActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/AboutActivity.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.net.toUri import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.theme.CommonsTheme import fr.free.nrw.commons.ui.widget.HtmlTextView import fr.free.nrw.commons.utils.ConfigUtils.getVersionNameWithSha import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog @@ -140,24 +141,6 @@ class AboutActivity : BaseActivity() { ) } } -//custom theme wrapper to centralize the color logic -@Composable -fun CommonsTheme(content: @Composable () -> Unit) { - val isDarkTheme = isSystemInDarkTheme() - val background = if (isDarkTheme) colorResource(R.color.main_background_dark) else colorResource(R.color.main_background_light) - - val colorScheme = if (isDarkTheme) { - darkColorScheme(primary = colorResource(R.color.primaryColor), background = background, onBackground = Color.White) - } else { - lightColorScheme(primary = colorResource(R.color.primaryColor), background = background, onBackground = Color.Black) - } - - MaterialTheme(colorScheme = colorScheme) { - Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { - content() - } - } -} @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -227,7 +210,7 @@ fun AboutScreen( Card( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors(containerColor = if (isSystemInDarkTheme()) Color(0xFF1E1E1E) else Color.White), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) ) { Column { From b5a0741cf24dd31f4852bfde183c0c42ae4d39be Mon Sep 17 00:00:00 2001 From: Kota-Jagadeesh Date: Sat, 28 Mar 2026 00:11:10 +0530 Subject: [PATCH 06/10] Feat: create reusable CommonsText component with predefined typography presets --- .../java/fr/free/nrw/commons/theme/Type.kt | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 app/src/main/java/fr/free/nrw/commons/theme/Type.kt diff --git a/app/src/main/java/fr/free/nrw/commons/theme/Type.kt b/app/src/main/java/fr/free/nrw/commons/theme/Type.kt new file mode 100644 index 00000000000..640edd90921 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/theme/Type.kt @@ -0,0 +1,57 @@ +package fr.free.nrw.commons.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.sp + +enum class CommonsTextPreset { Headline, Title, Body, Caption } + +@Composable +fun CommonsText( + text: String, + preset: CommonsTextPreset, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, + textAlign: TextAlign? = null, + maxLines: Int = Int.MAX_VALUE, + overflow: TextOverflow = TextOverflow.Clip, + fontWeight: FontWeight? = null +) { + CommonsText(AnnotatedString(text), preset, modifier, color, textAlign, maxLines, overflow, fontWeight) +} + +@Composable +fun CommonsText( + text: AnnotatedString, + preset: CommonsTextPreset, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, + textAlign: TextAlign? = null, + maxLines: Int = Int.MAX_VALUE, + overflow: TextOverflow = TextOverflow.Clip, + fontWeight: FontWeight? = null +) { + val style = when (preset) { + CommonsTextPreset.Headline -> TextStyle(fontSize = 24.sp, fontWeight = fontWeight ?: FontWeight.Bold) + CommonsTextPreset.Title -> TextStyle(fontSize = 20.sp, fontWeight = fontWeight ?: FontWeight.Medium) + CommonsTextPreset.Body -> TextStyle(fontSize = 16.sp, fontWeight = fontWeight ?: FontWeight.Normal) + CommonsTextPreset.Caption -> TextStyle(fontSize = 14.sp, fontWeight = fontWeight ?: FontWeight.Normal) + } + Text( + text = text, + modifier = modifier, + style = style, + color = if (color == Color.Unspecified) MaterialTheme.colorScheme.onBackground else color, + textAlign = textAlign, + maxLines = maxLines, + overflow = overflow + ) +} \ No newline at end of file From 35cef74932a918592dbc8fab41470a7db4d8d782 Mon Sep 17 00:00:00 2001 From: Kota-Jagadeesh Date: Sat, 28 Mar 2026 00:11:51 +0530 Subject: [PATCH 07/10] establish grid spacing and component dimension tokens for layout consistency --- .../java/fr/free/nrw/commons/theme/Spacing.kt | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 app/src/main/java/fr/free/nrw/commons/theme/Spacing.kt diff --git a/app/src/main/java/fr/free/nrw/commons/theme/Spacing.kt b/app/src/main/java/fr/free/nrw/commons/theme/Spacing.kt new file mode 100644 index 00000000000..71e6d57de93 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/theme/Spacing.kt @@ -0,0 +1,24 @@ +package fr.free.nrw.commons.theme + +import androidx.compose.ui.unit.dp + +object Spacing { + val none = 0.dp + val extraSmall = 4.dp + val small = 8.dp + val medium = 16.dp + val large = 20.dp + val extraLarge = 24.dp + val huge = 32.dp + val gigantic = 48.dp +} + +object Dimensions { + val logoSize = 110.dp + val socialIconBox = 52.dp + val socialIcon = 28.dp + val linkIcon = 20.dp + val cardCorner = 16.dp + val cardElevation = 2.dp + val dividerThickness = 0.5.dp +} \ No newline at end of file From 34680c2a3fefb3c685650853c8107e9d0b33bfba Mon Sep 17 00:00:00 2001 From: Kota-Jagadeesh Date: Sat, 28 Mar 2026 00:12:22 +0530 Subject: [PATCH 08/10] define core brand color tokens for light and dark modes --- .../main/java/fr/free/nrw/commons/theme/Color.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 app/src/main/java/fr/free/nrw/commons/theme/Color.kt diff --git a/app/src/main/java/fr/free/nrw/commons/theme/Color.kt b/app/src/main/java/fr/free/nrw/commons/theme/Color.kt new file mode 100644 index 00000000000..a1713499f28 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/theme/Color.kt @@ -0,0 +1,13 @@ +package fr.free.nrw.commons.theme + +import androidx.compose.ui.graphics.Color + +val PrimaryBlue = Color(0xFF0C609C) +val PrimaryBlueNight = Color(0xFF1E8CAB) +val BackgroundLight = Color(0xFFFAFAFA) +val BackgroundDark = Color(0xFF303030) +val CardBackgroundDark = Color(0xFF1E1E1E) +val CardBackgroundLight = Color(0xFFFFFFFF) +val TextWhite = Color(0xFFFFFFFF) +val TextBlack = Color(0xFF000000) +val Transparent = Color.Transparent \ No newline at end of file From ad4feb57cdb792e2752065b339d64a538977aa56 Mon Sep 17 00:00:00 2001 From: Kota-Jagadeesh Date: Sat, 28 Mar 2026 00:13:17 +0530 Subject: [PATCH 09/10] chore: implement global CommonsTheme usin compose color schemes and material 3 surface --- .../fr/free/nrw/commons/theme/CommonsTheme.kt | 33 +++++++------------ 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/theme/CommonsTheme.kt b/app/src/main/java/fr/free/nrw/commons/theme/CommonsTheme.kt index 70770440004..da0d8ee1e3a 100644 --- a/app/src/main/java/fr/free/nrw/commons/theme/CommonsTheme.kt +++ b/app/src/main/java/fr/free/nrw/commons/theme/CommonsTheme.kt @@ -2,39 +2,28 @@ package fr.free.nrw.commons.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.colorResource -import fr.free.nrw.commons.R @Composable fun CommonsTheme(content: @Composable () -> Unit) { val isDarkTheme = isSystemInDarkTheme() - val background = if (isDarkTheme) - colorResource(R.color.main_background_dark) - else - colorResource(R.color.main_background_light) - val colorScheme = if (isDarkTheme) { darkColorScheme( - primary = colorResource(R.color.primaryColor), - background = background, - onBackground = Color.White, - surfaceVariant = Color(0xFF1E1E1E), - onSurface = Color.White + primary = PrimaryBlueNight, + background = BackgroundDark, + onBackground = TextWhite, + surfaceVariant = CardBackgroundDark, + onSurface = TextWhite ) } else { lightColorScheme( - primary = colorResource(R.color.primaryColor), - background = background, - onBackground = Color.Black, - surfaceVariant = Color.White, - onSurface = Color.Black + primary = PrimaryBlue, + background = BackgroundLight, + onBackground = TextBlack, + surfaceVariant = CardBackgroundLight, + onSurface = TextBlack ) } From e5207d200fbf1d5c62880e09d06ecfc6a37c424c Mon Sep 17 00:00:00 2001 From: Kota-Jagadeesh Date: Sat, 28 Mar 2026 00:13:50 +0530 Subject: [PATCH 10/10] Refactor: about screen to LazyColumn and migrate UI to CommonsText using design tokens --- .../java/fr/free/nrw/commons/AboutActivity.kt | 152 +++++++++--------- 1 file changed, 76 insertions(+), 76 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/AboutActivity.kt b/app/src/main/java/fr/free/nrw/commons/AboutActivity.kt index 8f1d5688310..1aab047d9c5 100644 --- a/app/src/main/java/fr/free/nrw/commons/AboutActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/AboutActivity.kt @@ -10,6 +10,7 @@ import androidx.activity.compose.setContent import androidx.compose.animation.core.* import androidx.compose.foundation.* import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -32,8 +33,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.net.toUri -import fr.free.nrw.commons.theme.BaseActivity -import fr.free.nrw.commons.theme.CommonsTheme +import fr.free.nrw.commons.theme.* import fr.free.nrw.commons.ui.widget.HtmlTextView import fr.free.nrw.commons.utils.ConfigUtils.getVersionNameWithSha import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog @@ -55,6 +55,7 @@ data class AboutActions( val onFaq: () -> Unit ) +data class AboutLinkItem(val textRes: Int, val onClick: () -> Unit) class AboutActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -127,8 +128,7 @@ class AboutActivity : BaseActivity() { internal fun launchTranslate() { val instance = CommonsApplication.instance - val sortedNames = instance.languageLookUpTable!!.getCanonicalNames().toMutableList() - Collections.sort(sortedNames) + val sortedNames = instance.languageLookUpTable!!.getCanonicalNames().toMutableList().apply { Collections.sort(this) } val spinner = android.widget.Spinner(this).apply { adapter = android.widget.ArrayAdapter(this@AboutActivity, android.R.layout.simple_spinner_dropdown_item, sortedNames) } @@ -144,90 +144,87 @@ class AboutActivity : BaseActivity() { @OptIn(ExperimentalMaterial3Api::class) @Composable -fun AboutScreen( - version: String, - actions: AboutActions -) { - val logoAlpha = remember { Animatable(0f) } - // smoooth fade in for the app logo on opening the screen - LaunchedEffect(Unit) { - logoAlpha.animateTo(1f, animationSpec = tween(1000)) +fun AboutScreen(version: String, actions: AboutActions) { + val logoAlpha = remember { Animatable(Spacing.none.value) } + LaunchedEffect(Unit) { logoAlpha.animateTo(1f, animationSpec = tween(1000)) } + + val aboutLinks = remember { + listOf( + AboutLinkItem(R.string.about_rate_us, actions.onRateUs), + AboutLinkItem(R.string.user_guide, actions.onUserGuide), + AboutLinkItem(R.string.about_privacy_policy, actions.onPrivacyPolicy), + AboutLinkItem(R.string.about_translate, actions.onTranslate), + AboutLinkItem(R.string.about_credits, actions.onCredits), + AboutLinkItem(R.string.about_faq, actions.onFaq) + ) } Scaffold( topBar = { CenterAlignedTopAppBar( title = { - Text(text = stringResource(R.string.about), fontWeight = FontWeight.Bold, maxLines = 1, - softWrap = false, style = MaterialTheme.typography.titleLarge) + CommonsText(text = stringResource(R.string.about), preset = CommonsTextPreset.Title, color = TextWhite, fontWeight = FontWeight.Bold) }, navigationIcon = { IconButton(onClick = actions.onBackClick) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, null, tint = Color.White) + Icon(Icons.AutoMirrored.Filled.ArrowBack, null, tint = TextWhite) } }, actions = { IconButton(onClick = actions.onShareClick) { - Icon(Icons.Default.Share, null, tint = Color.White) + Icon(Icons.Default.Share, null, tint = TextWhite) } }, - colors = TopAppBarDefaults.centerAlignedTopAppBarColors(containerColor = MaterialTheme.colorScheme.primary, titleContentColor = Color.White) + colors = TopAppBarDefaults.centerAlignedTopAppBarColors(containerColor = MaterialTheme.colorScheme.primary) ) }, - containerColor = Color.Transparent + containerColor = Transparent ) { padding -> - Column( - modifier = Modifier.fillMaxSize().padding(padding).verticalScroll(rememberScrollState()).padding(horizontal = 20.dp, vertical = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally + LazyColumn( + modifier = Modifier.fillMaxSize().padding(padding), + horizontalAlignment = Alignment.CenterHorizontally, + contentPadding = PaddingValues(horizontal = Spacing.large, vertical = Spacing.medium) ) { - Box(modifier = Modifier.alpha(logoAlpha.value)) { - Image(painter = painterResource(R.drawable.ic_launcher), contentDescription = stringResource(R.string.commons_logo), - modifier = Modifier.size(110.dp).clip(CircleShape)) - } - - Text(text = stringResource(R.string.app_name), style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onBackground) - - // tucked the version info inside a small pill shaped chip to look better - AssistChip( - onClick = { }, - label = { Text(version, style = MaterialTheme.typography.labelSmall) }, - colors = AssistChipDefaults.assistChipColors(labelColor = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.6f)), - modifier = Modifier.padding(top = 4.dp, bottom = 16.dp) - ) - - HtmlText(stringResource(R.string.about_license)) - HtmlText(String.format(stringResource(R.string.about_improve), Urls.NEW_ISSUE_URL), Modifier.padding(top = 12.dp)) - - // grouped the social media icons in a neat row - Row(modifier = Modifier.padding(vertical = 24.dp), horizontalArrangement = Arrangement.spacedBy(28.dp)) { - EnhancedSocialIcon(R.drawable.ic_action_website, actions.onLaunchWebsite) - EnhancedSocialIcon(R.drawable.ic_action_facebook, actions.onLaunchFacebook) - EnhancedSocialIcon(R.drawable.ic_action_github, actions.onLaunchGithub) + item { + Box(modifier = Modifier.alpha(logoAlpha.value)) { + Image(painter = painterResource(R.drawable.ic_launcher), + contentDescription = stringResource(R.string.commons_logo), + modifier = Modifier.size(Dimensions.logoSize).clip(CircleShape)) + } + CommonsText(text = stringResource(R.string.app_name), + preset = CommonsTextPreset.Headline, fontWeight = FontWeight.Bold) + AssistChip( + onClick = { }, + label = { CommonsText(text = version, preset = CommonsTextPreset.Caption, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)) }, + modifier = Modifier.padding(top = Spacing.extraSmall, bottom = Spacing.medium) + ) + HtmlText(stringResource(R.string.about_license)) + HtmlText(String.format(stringResource(R.string.about_improve), Urls.NEW_ISSUE_URL), + Modifier.padding(top = Spacing.medium)) + Row(modifier = Modifier.padding(vertical = Spacing.extraLarge), + horizontalArrangement = Arrangement.spacedBy(Spacing.huge)) { + EnhancedSocialIcon(R.drawable.ic_action_website, actions.onLaunchWebsite) + EnhancedSocialIcon(R.drawable.ic_action_facebook, actions.onLaunchFacebook) + EnhancedSocialIcon(R.drawable.ic_action_github, actions.onLaunchGithub) + } } - - // grouping the all link items into a single card layout - Card( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) - ) { - Column { - EnhancedLinkRow(stringResource(R.string.about_rate_us), actions.onRateUs) - DividerRow() - EnhancedLinkRow(stringResource(R.string.user_guide), actions.onUserGuide) - DividerRow() - EnhancedLinkRow(stringResource(R.string.about_privacy_policy), actions.onPrivacyPolicy) - DividerRow() - EnhancedLinkRow(stringResource(R.string.about_translate), actions.onTranslate) - DividerRow() - EnhancedLinkRow(stringResource(R.string.about_credits), actions.onCredits) - DividerRow() - EnhancedLinkRow(stringResource(R.string.about_faq), actions.onFaq) + item { + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(Dimensions.cardCorner), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), + elevation = CardDefaults.cardElevation(defaultElevation = Dimensions.cardElevation) + ) { + Column { + aboutLinks.forEachIndexed { index, item -> + EnhancedLinkRow(stringResource(item.textRes), item.onClick) + if (index < aboutLinks.size - 1) DividerRow() + } + } } } - Spacer(modifier = Modifier.height(32.dp)) + item { Spacer(modifier = Modifier.height(Spacing.huge)) } } } } @@ -235,36 +232,39 @@ fun AboutScreen( @Composable fun EnhancedSocialIcon(drawableId: Int, onClick: () -> Unit) { val tint = MaterialTheme.colorScheme.primary - Surface(modifier = Modifier.size(52.dp).clickable { onClick() }, shape = CircleShape, color = tint.copy(alpha = 0.1f) - ) { + Surface(modifier = Modifier.size(Dimensions.socialIconBox).clickable { onClick() }, + shape = CircleShape, color = tint.copy(alpha = 0.1f)) { Box(contentAlignment = Alignment.Center) { - Image(painter = painterResource(drawableId), contentDescription = null, colorFilter = ColorFilter.tint(tint), modifier = Modifier.size(28.dp)) + Image(painter = painterResource(drawableId), contentDescription = null, + colorFilter = ColorFilter.tint(tint), modifier = Modifier.size(Dimensions.socialIcon)) } } } @Composable fun EnhancedLinkRow(text: String, onClick: () -> Unit) { - Row(modifier = Modifier.fillMaxWidth().clickable { onClick() }.padding(16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) { - Text(text = text, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurface) - Icon(imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = null, tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f), modifier = Modifier.size(20.dp)) + Row(modifier = Modifier.fillMaxWidth().clickable { onClick() }.padding(Spacing.medium), + verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween) { + CommonsText(text = text, preset = CommonsTextPreset.Body) + Icon(imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f), modifier = Modifier.size(Dimensions.linkIcon)) } } @Composable fun DividerRow() { - HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)) + HorizontalDivider(modifier = Modifier.padding(horizontal = Spacing.medium), + thickness = Dimensions.dividerThickness, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)) } @Composable fun HtmlText(html: String, modifier: Modifier = Modifier) { - val textColor = if (isSystemInDarkTheme()) android.graphics.Color.WHITE else android.graphics.Color.BLACK - // bridge thhe existing xml-based HtmlTextView into the compose ui + val textColor = MaterialTheme.colorScheme.onBackground AndroidView( modifier = modifier, factory = { context -> HtmlTextView(context).apply { gravity = android.view.Gravity.CENTER } }, update = { view -> - view.setTextColor(textColor) + view.setTextColor(textColor.hashCode()) view.setHtmlText(html) } )