diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index f948787bab..a019ca8be6 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -4,6 +4,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.com.android.application)
alias(libs.plugins.org.jetbrains.kotlin.android)
+ alias(libs.plugins.org.jetbrains.kotlin.plugin.compose)
}
kotlin {
@@ -48,6 +49,7 @@ android {
buildFeatures {
buildConfig = true
+ compose = true
viewBinding = true
}
@@ -93,9 +95,10 @@ android {
lint {
lintConfig = file("lint.xml")
}
+
kotlin {
compilerOptions {
- jvmTarget = JvmTarget.JVM_21
+ jvmTarget = JvmTarget.JVM_17
}
}
compileOptions {
@@ -103,8 +106,10 @@ android {
// Flag to enable support for the new language APIs
isCoreLibraryDesugaringEnabled = true
- sourceCompatibility = JavaVersion.VERSION_21
- targetCompatibility = JavaVersion.VERSION_21
+
+
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
}
}
@@ -121,6 +126,19 @@ dependencies {
implementation(libs.com.google.android.material.material)
coreLibraryDesugaring(libs.com.android.tools.desugar.jdk.libs)
+ // Compose
+ implementation(libs.androidx.activity.activity.compose)
+ val composeBom = platform(libs.androidx.compose.compose.bom)
+ implementation(composeBom)
+ implementation(libs.androidx.compose.foundation.foundation)
+ implementation(libs.androidx.compose.material3.material3)
+ implementation(libs.androidx.compose.material.material.icons.extended)
+ implementation(libs.androidx.compose.ui.ui.tooling.preview.android)
+ debugImplementation(libs.androidx.compose.ui.ui.test.manifest)
+
+ androidTestImplementation(composeBom)
+ androidTestImplementation(libs.androidx.compose.ui.ui.test.junit4)
+
// Third-party
implementation(libs.com.journeyapps.zxing.android.embedded)
implementation(libs.com.github.yalantis.ucrop)
@@ -140,6 +158,8 @@ dependencies {
androidTestImplementation(libs.bundles.androidx.test)
androidTestImplementation(libs.junit.junit)
androidTestImplementation(libs.androidx.test.ext.junit)
+ androidTestImplementation(libs.androidx.test.rules)
+ androidTestImplementation(libs.androidx.test.runner)
androidTestImplementation(libs.androidx.test.uiautomator.uiautomator)
androidTestImplementation(libs.androidx.test.espresso.espresso.core)
}
diff --git a/app/src/androidTest/java/protect/card_locker/AboutActivityTest.kt b/app/src/androidTest/java/protect/card_locker/AboutActivityTest.kt
new file mode 100644
index 0000000000..7555fa5a98
--- /dev/null
+++ b/app/src/androidTest/java/protect/card_locker/AboutActivityTest.kt
@@ -0,0 +1,89 @@
+package protect.card_locker
+
+import android.app.Instrumentation
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performScrollTo
+import androidx.compose.ui.test.runComposeUiTest
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import protect.card_locker.compose.theme.CatimaTheme
+
+@OptIn(ExperimentalTestApi::class)
+@RunWith(AndroidJUnit4::class)
+class AboutActivityTest {
+ @get:Rule
+ private val rule: ComposeContentTestRule = createComposeRule()
+
+ private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+
+ private val content: AboutContent = AboutContent(instrumentation.targetContext)
+
+ @Test
+ fun testInitialState(): Unit = runComposeUiTest {
+ setContent {
+ AboutScreenContent(content = content)
+ }
+
+ onNodeWithTag("topbar_catima").assertIsDisplayed()
+
+ onNodeWithTag("card_version_history").assertIsDisplayed()
+ onNodeWithText(content.versionHistory).assertIsDisplayed()
+
+ onNodeWithTag("card_credits").assertIsDisplayed()
+ onNodeWithText(content.copyrightShort).assertIsDisplayed()
+
+ onNodeWithTag("card_translate").assertIsDisplayed()
+ onNodeWithTag("card_license").assertIsDisplayed()
+
+ // We might be off the screen so start scrolling
+ onNodeWithTag("card_source_github").performScrollTo().assertIsDisplayed()
+ onNodeWithTag("card_privacy_policy").performScrollTo().assertIsDisplayed()
+ onNodeWithTag("card_donate").performScrollTo().assertIsDisplayed()
+ // Dont scroll to this, since its not displayed
+ onNodeWithTag("card_rate_google").assertIsNotDisplayed()
+ onNodeWithTag("card_report_error").performScrollTo().assertIsDisplayed()
+ }
+
+ @Test
+ fun testDonateAndGoogleCardVisible(): Unit = runComposeUiTest {
+ setContent {
+ CatimaTheme {
+ AboutScreenContent(
+ content = content,
+ showDonate = true,
+ showRateOnGooglePlay = true,
+ )
+ }
+ }
+
+ onNodeWithTag("card_donate").performScrollTo().assertIsDisplayed()
+ onNodeWithTag("card_rate_google").performScrollTo().assertIsDisplayed()
+ }
+
+ @Test
+ fun testDonateAndGoogleCardHidden(): Unit = runComposeUiTest {
+ setContent {
+ CatimaTheme {
+ AboutScreenContent(
+ content = content,
+ showDonate = false,
+ showRateOnGooglePlay = false,
+ )
+ }
+ }
+
+ onNodeWithTag("card_privacy_policy").performScrollTo().assertIsDisplayed()
+ onNodeWithTag("card_donate").assertIsNotDisplayed()
+ onNodeWithTag("card_rate_google").assertIsNotDisplayed()
+ onNodeWithTag("card_report_error").performScrollTo().assertIsDisplayed()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/protect/card_locker/AboutActivity.kt b/app/src/main/java/protect/card_locker/AboutActivity.kt
index ed3af7da76..add6c1c07e 100644
--- a/app/src/main/java/protect/card_locker/AboutActivity.kt
+++ b/app/src/main/java/protect/card_locker/AboutActivity.kt
@@ -1,149 +1,167 @@
package protect.card_locker
import android.os.Bundle
-import android.text.Spanned
-import android.view.MenuItem
-import android.view.View
-import android.widget.ScrollView
-import android.widget.TextView
-
-import androidx.annotation.StringRes
-import androidx.core.view.isVisible
-
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-
-import protect.card_locker.databinding.AboutActivityBinding
-
-class AboutActivity : CatimaAppCompatActivity() {
- private companion object {
- private const val TAG = "Catima"
- }
-
- private lateinit var binding: AboutActivityBinding
+import androidx.activity.ComponentActivity
+import androidx.activity.OnBackPressedDispatcher
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.TextLinkStyles
+import androidx.compose.ui.text.fromHtml
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.tooling.preview.Preview
+import protect.card_locker.compose.CatimaAboutSection
+import protect.card_locker.compose.CatimaTopAppBar
+import protect.card_locker.compose.theme.CatimaTheme
+
+class AboutActivity : ComponentActivity() {
private lateinit var content: AboutContent
+ @OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- binding = AboutActivityBinding.inflate(layoutInflater)
content = AboutContent(this)
title = content.pageTitle
- setContentView(binding.root)
- setSupportActionBar(binding.toolbar)
- enableToolbarBackButton()
-
- binding.apply {
- creditsSub.text = content.copyrightShort
- versionHistorySub.text = content.versionHistory
-
- versionHistory.tag = "https://catima.app/changelog/"
- translate.tag = "https://hosted.weblate.org/engage/catima/"
- license.tag = "https://github.com/CatimaLoyalty/Android/blob/main/LICENSE"
- repo.tag = "https://github.com/CatimaLoyalty/Android/"
- privacy.tag = "https://catima.app/privacy-policy/"
- reportError.tag = "https://github.com/CatimaLoyalty/Android/issues"
- rate.tag = "https://play.google.com/store/apps/details?id=me.hackerchick.catima"
- donate.tag = "https://catima.app/donate"
-
- // Hide Google Play rate button if not on Google Play
- rate.isVisible = BuildConfig.showRateOnGooglePlay
- // Hide donate button on Google Play (Google Play doesn't allow donation links)
- donate.isVisible = BuildConfig.showDonate
- }
- bindClickListeners()
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- return when (item.itemId) {
- android.R.id.home -> {
- finish()
- true
+ setContent {
+ CatimaTheme {
+ AboutScreenContent(
+ content = content,
+ showDonate = BuildConfig.showDonate,
+ showRateOnGooglePlay = BuildConfig.showRateOnGooglePlay,
+ onBackPressedDispatcher = onBackPressedDispatcher
+ )
}
-
- else -> super.onOptionsItemSelected(item)
- }
- }
-
- override fun onDestroy() {
- super.onDestroy()
- content.destroy()
- clearClickListeners()
- }
-
- private fun bindClickListeners() {
- binding.apply {
- versionHistory.setOnClickListener { showHistory(it) }
- translate.setOnClickListener { openExternalBrowser(it) }
- license.setOnClickListener { showLicense(it) }
- repo.setOnClickListener { openExternalBrowser(it) }
- privacy.setOnClickListener { showPrivacy(it) }
- reportError.setOnClickListener { openExternalBrowser(it) }
- rate.setOnClickListener { openExternalBrowser(it) }
- donate.setOnClickListener { openExternalBrowser(it) }
- credits.setOnClickListener { showCredits() }
- }
- }
-
- private fun clearClickListeners() {
- binding.apply {
- versionHistory.setOnClickListener(null)
- translate.setOnClickListener(null)
- license.setOnClickListener(null)
- repo.setOnClickListener(null)
- privacy.setOnClickListener(null)
- reportError.setOnClickListener(null)
- rate.setOnClickListener(null)
- donate.setOnClickListener(null)
- credits.setOnClickListener(null)
}
}
+}
- private fun showCredits() {
- showHTML(R.string.credits, content.contributorInfo, null)
- }
-
- private fun showHistory(view: View) {
- showHTML(R.string.version_history, content.historyInfo, view)
- }
-
- private fun showLicense(view: View) {
- showHTML(R.string.license, content.licenseInfo, view)
- }
-
- private fun showPrivacy(view: View) {
- showHTML(R.string.privacy_policy, content.privacyInfo, view)
- }
-
- private fun showHTML(@StringRes title: Int, text: Spanned, view: View?) {
- val dialogContentPadding = resources.getDimensionPixelSize(R.dimen.alert_dialog_content_padding)
- val textView = TextView(this).apply {
- setText(text)
- Utils.makeTextViewLinksClickable(this, text)
- }
-
- val scrollView = ScrollView(this).apply {
- addView(textView)
- setPadding(dialogContentPadding, dialogContentPadding / 2, dialogContentPadding, 0)
- }
-
- MaterialAlertDialogBuilder(this).apply {
- setTitle(title)
- setView(scrollView)
- setPositiveButton(R.string.ok, null)
-
- // Add View online button if an URL is linked to this view
- view?.tag?.let {
- setNeutralButton(R.string.view_online) { _, _ -> openExternalBrowser(view) }
+@Composable
+fun AboutScreenContent(
+ content: AboutContent,
+ showDonate: Boolean = true,
+ showRateOnGooglePlay: Boolean = false,
+ onBackPressedDispatcher: OnBackPressedDispatcher? = null,
+) {
+ Scaffold(
+ topBar = { CatimaTopAppBar(content.pageTitle.toString(), onBackPressedDispatcher) }
+ ) { innerPadding ->
+ Column(
+ modifier = Modifier
+ .padding(innerPadding)
+ .verticalScroll(rememberScrollState())
+ ) {
+ CatimaAboutSection(
+ stringResource(R.string.version_history),
+ content.versionHistory,
+ modifier = Modifier.testTag("card_version_history"),
+ onClickUrl = "https://catima.app/changelog/",
+ onClickDialogText = AnnotatedString.fromHtml(
+ htmlString = content.historyHtml,
+ linkStyles = TextLinkStyles(
+ style = SpanStyle(
+ textDecoration = TextDecoration.Underline,
+ color = MaterialTheme.colorScheme.primary
+ )
+ )
+ )
+ )
+ CatimaAboutSection(
+ stringResource(R.string.credits),
+ content.copyrightShort,
+ modifier = Modifier.testTag("card_credits"),
+ onClickDialogText = AnnotatedString.fromHtml(
+ htmlString = content.contributorInfoHtml,
+ linkStyles = TextLinkStyles(
+ style = SpanStyle(
+ textDecoration = TextDecoration.Underline,
+ color = MaterialTheme.colorScheme.primary
+ )
+ )
+ )
+ )
+ CatimaAboutSection(
+ stringResource(R.string.help_translate_this_app),
+ stringResource(R.string.translate_platform),
+ modifier = Modifier.testTag("card_translate"),
+ onClickUrl = "https://hosted.weblate.org/engage/catima/"
+ )
+ CatimaAboutSection(
+ stringResource(R.string.license),
+ stringResource(R.string.app_license),
+ modifier = Modifier.testTag("card_license"),
+ onClickUrl = "https://github.com/CatimaLoyalty/Android/blob/main/LICENSE",
+ onClickDialogText = AnnotatedString.fromHtml(
+ htmlString = content.licenseHtml,
+ linkStyles = TextLinkStyles(
+ style = SpanStyle(
+ textDecoration = TextDecoration.Underline,
+ color = MaterialTheme.colorScheme.primary
+ )
+ )
+ )
+ )
+ CatimaAboutSection(
+ stringResource(R.string.source_repository),
+ stringResource(R.string.on_github),
+ modifier = Modifier.testTag("card_source_github"),
+ onClickUrl = "https://github.com/CatimaLoyalty/Android/"
+ )
+ CatimaAboutSection(
+ stringResource(R.string.privacy_policy),
+ stringResource(R.string.and_data_usage),
+ modifier = Modifier.testTag("card_privacy_policy"),
+ onClickUrl = "https://catima.app/privacy-policy/",
+ onClickDialogText = AnnotatedString.fromHtml(
+ htmlString = content.privacyHtml,
+ linkStyles = TextLinkStyles(
+ style = SpanStyle(
+ textDecoration = TextDecoration.Underline,
+ color = MaterialTheme.colorScheme.primary
+ )
+ )
+ )
+ )
+ if (showDonate) {
+ CatimaAboutSection(
+ stringResource(R.string.donate),
+ "",
+ modifier = Modifier.testTag("card_donate"),
+ onClickUrl = "https://catima.app/donate"
+ )
}
-
- show()
+ if (showRateOnGooglePlay) {
+ CatimaAboutSection(
+ stringResource(R.string.rate_this_app),
+ stringResource(R.string.on_google_play),
+ modifier = Modifier.testTag("card_rate_google"),
+ onClickUrl = "https://play.google.com/store/apps/details?id=me.hackerchick.catima"
+ )
+ }
+ CatimaAboutSection(
+ stringResource(R.string.report_error),
+ stringResource(R.string.on_github),
+ modifier = Modifier.testTag("card_report_error"),
+ onClickUrl = "https://github.com/CatimaLoyalty/Android/issues"
+ )
}
}
+}
- private fun openExternalBrowser(view: View) {
- val tag = view.tag
- if (tag is String && tag.startsWith("https://")) {
- OpenWebLinkHandler().openBrowser(this, tag)
- }
- }
+@Preview
+@Composable
+private fun AboutActivityPreview() {
+ AboutScreenContent(AboutContent(LocalContext.current))
}
diff --git a/app/src/main/java/protect/card_locker/AboutContent.java b/app/src/main/java/protect/card_locker/AboutContent.java
index aa717ad702..50daf1589d 100644
--- a/app/src/main/java/protect/card_locker/AboutContent.java
+++ b/app/src/main/java/protect/card_locker/AboutContent.java
@@ -3,11 +3,8 @@
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
-import android.text.Spanned;
import android.util.Log;
-import androidx.core.text.HtmlCompat;
-
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
@@ -55,7 +52,7 @@ public String getCopyrightShort() {
return context.getString(R.string.app_copyright_short);
}
- public String getContributors() {
+ public String getContributorsHtml() {
String contributors;
try {
contributors = "
" + Utils.readTextFile(context, R.raw.contributors);
@@ -65,7 +62,7 @@ public String getContributors() {
return contributors.replace("\n", "
");
}
- public String getHistory() {
+ public String getHistoryHtml() {
String versionHistory;
try {
versionHistory = Utils.readTextFile(context, R.raw.changelog)
@@ -77,7 +74,7 @@ public String getHistory() {
.replace("\n", "
");
}
- public String getLicense() {
+ public String getLicenseHtml() {
try {
return Utils.readTextFile(context, R.raw.license);
} catch (IOException ignored) {
@@ -85,7 +82,7 @@ public String getLicense() {
}
}
- public String getPrivacy() {
+ public String getPrivacyHtml() {
String privacyPolicy;
try {
privacyPolicy = Utils.readTextFile(context, R.raw.privacy)
@@ -97,7 +94,7 @@ public String getPrivacy() {
.replace("\n", "
");
}
- public String getThirdPartyLibraries() {
+ public String getThirdPartyLibrariesHtml() {
final List usedLibraries = new ArrayList<>();
usedLibraries.add(new ThirdPartyInfo("ACRA", "https://github.com/ACRA/acra", "Apache 2.0"));
usedLibraries.add(new ThirdPartyInfo("Color Picker", "https://github.com/jaredrummler/ColorPicker", "Apache 2.0"));
@@ -116,7 +113,7 @@ public String getThirdPartyLibraries() {
return result.toString();
}
- public String getUsedThirdPartyAssets() {
+ public String getUsedThirdPartyAssetsHtml() {
final List usedAssets = new ArrayList<>();
usedAssets.add(new ThirdPartyInfo("Android icons", "https://fonts.google.com/icons?selected=Material+Icons", "Apache 2.0"));
@@ -129,31 +126,19 @@ public String getUsedThirdPartyAssets() {
return result.toString();
}
- public Spanned getContributorInfo() {
+ public String getContributorInfoHtml() {
StringBuilder contributorInfo = new StringBuilder();
contributorInfo.append(getCopyright());
contributorInfo.append("
");
contributorInfo.append(context.getString(R.string.app_copyright_old));
contributorInfo.append("
");
- contributorInfo.append(String.format(context.getString(R.string.app_contributors), getContributors()));
+ contributorInfo.append(String.format(context.getString(R.string.app_contributors), getContributorsHtml()));
contributorInfo.append("
");
- contributorInfo.append(String.format(context.getString(R.string.app_libraries), getThirdPartyLibraries()));
+ contributorInfo.append(String.format(context.getString(R.string.app_libraries), getThirdPartyLibrariesHtml()));
contributorInfo.append("
");
- contributorInfo.append(String.format(context.getString(R.string.app_resources), getUsedThirdPartyAssets()));
-
- return HtmlCompat.fromHtml(contributorInfo.toString(), HtmlCompat.FROM_HTML_MODE_COMPACT);
- }
-
- public Spanned getHistoryInfo() {
- return HtmlCompat.fromHtml(getHistory(), HtmlCompat.FROM_HTML_MODE_COMPACT);
- }
-
- public Spanned getLicenseInfo() {
- return HtmlCompat.fromHtml(getLicense(), HtmlCompat.FROM_HTML_MODE_LEGACY);
- }
+ contributorInfo.append(String.format(context.getString(R.string.app_resources), getUsedThirdPartyAssetsHtml()));
- public Spanned getPrivacyInfo() {
- return HtmlCompat.fromHtml(getPrivacy(), HtmlCompat.FROM_HTML_MODE_COMPACT);
+ return contributorInfo.toString();
}
public String getVersionHistory() {
diff --git a/app/src/main/java/protect/card_locker/MainActivity.kt b/app/src/main/java/protect/card_locker/MainActivity.kt
index 23471b9649..e4083ef787 100644
--- a/app/src/main/java/protect/card_locker/MainActivity.kt
+++ b/app/src/main/java/protect/card_locker/MainActivity.kt
@@ -58,8 +58,8 @@ class MainActivity : CatimaAppCompatActivity(), CardAdapterListener {
private var selectedTab: Int = 0
private lateinit var groupsTabLayout: TabLayout
private lateinit var mUpdateLoyaltyCardListRunnable: Runnable
- private lateinit var mBarcodeScannerLauncher: ActivityResultLauncher
- private lateinit var mSettingsLauncher: ActivityResultLauncher
+ private lateinit var mBarcodeScannerLauncher: ActivityResultLauncher
+ private lateinit var mSettingsLauncher: ActivityResultLauncher
private val mCurrentActionModeCallback: ActionMode.Callback = object : ActionMode.Callback {
override fun onCreateActionMode(inputMode: ActionMode, inputMenu: Menu?): Boolean {
diff --git a/app/src/main/java/protect/card_locker/OpenWebLinkHandler.java b/app/src/main/java/protect/card_locker/OpenWebLinkHandler.java
index 1586ac4f28..8bfcef4f65 100644
--- a/app/src/main/java/protect/card_locker/OpenWebLinkHandler.java
+++ b/app/src/main/java/protect/card_locker/OpenWebLinkHandler.java
@@ -1,18 +1,17 @@
package protect.card_locker;
+import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import android.widget.Toast;
-import androidx.appcompat.app.AppCompatActivity;
-
public class OpenWebLinkHandler {
private static final String TAG = "Catima";
- public void openBrowser(AppCompatActivity activity, String url) {
+ public void openBrowser(Activity activity, String url) {
if (url == null) {
return;
}
diff --git a/app/src/main/java/protect/card_locker/ScanActivity.kt b/app/src/main/java/protect/card_locker/ScanActivity.kt
index f1dbf6b971..52beb68eaf 100644
--- a/app/src/main/java/protect/card_locker/ScanActivity.kt
+++ b/app/src/main/java/protect/card_locker/ScanActivity.kt
@@ -538,7 +538,7 @@ class ScanActivity : CatimaAppCompatActivity() {
override fun onRequestPermissionsResult(
requestCode: Int,
- permissions: Array,
+ permissions: Array,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
@@ -548,7 +548,7 @@ class ScanActivity : CatimaAppCompatActivity() {
override fun onMockedRequestPermissionsResult(
requestCode: Int,
- permissions: Array,
+ permissions: Array,
grantResults: IntArray
) {
val granted =
diff --git a/app/src/main/java/protect/card_locker/compose/AboutActivity.kt b/app/src/main/java/protect/card_locker/compose/AboutActivity.kt
new file mode 100644
index 0000000000..5411063ccc
--- /dev/null
+++ b/app/src/main/java/protect/card_locker/compose/AboutActivity.kt
@@ -0,0 +1,97 @@
+package protect.card_locker.compose
+
+import androidx.activity.compose.LocalActivity
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.unit.dp
+import protect.card_locker.OpenWebLinkHandler
+import protect.card_locker.R
+
+@Composable
+fun CatimaAboutSection(
+ title: String,
+ message: String,
+ modifier: Modifier = Modifier,
+ onClickUrl: String? = null,
+ onClickDialogText: AnnotatedString? = null,
+) {
+ val activity = LocalActivity.current
+
+ val openDialog = remember { mutableStateOf(false) }
+
+ Row(
+ modifier = modifier
+ .padding(horizontal = 16.dp, vertical = 8.dp)
+ .clickable {
+ if (onClickDialogText != null) {
+ openDialog.value = true
+ } else if (onClickUrl != null) {
+ OpenWebLinkHandler().openBrowser(activity, onClickUrl)
+ }
+ }
+ ) {
+ Column(modifier = Modifier.weight(1F)) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleMedium
+ )
+ Text(text = message)
+ }
+ Text(modifier = Modifier.align(Alignment.CenterVertically),
+ text = ">",
+ style = MaterialTheme.typography.bodyMedium
+ )
+ }
+ if (openDialog.value && onClickDialogText != null) {
+ AlertDialog(
+ icon = {},
+ title = {
+ Text(text = title)
+ },
+ text = {
+ Text(
+ text = onClickDialogText,
+ modifier = Modifier.verticalScroll(rememberScrollState())
+ )
+ },
+ onDismissRequest = {
+ openDialog.value = false
+ },
+ confirmButton = {
+ TextButton(
+ onClick = {
+ openDialog.value = false
+ }
+ ) {
+ Text(stringResource(R.string.ok))
+ }
+ },
+ dismissButton = {
+ if (onClickUrl != null) {
+ TextButton(
+ onClick = {
+ OpenWebLinkHandler().openBrowser(activity, onClickUrl)
+ }
+ ) {
+ Text(stringResource(R.string.view_online))
+ }
+ }
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/protect/card_locker/compose/Catima.kt b/app/src/main/java/protect/card_locker/compose/Catima.kt
new file mode 100644
index 0000000000..2fd71cc03f
--- /dev/null
+++ b/app/src/main/java/protect/card_locker/compose/Catima.kt
@@ -0,0 +1,34 @@
+package protect.card_locker.compose
+
+import androidx.activity.OnBackPressedDispatcher
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.stringResource
+import protect.card_locker.R
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun CatimaTopAppBar(title: String, onBackPressedDispatcher: OnBackPressedDispatcher?) {
+ TopAppBar(
+ modifier = Modifier.testTag("topbar_catima"),
+ title = { Text(text = title) },
+ navigationIcon = {
+ if (onBackPressedDispatcher != null) {
+ IconButton(onClick = { onBackPressedDispatcher.onBackPressed() }) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = stringResource(R.string.back)
+ )
+ }
+ }
+ }
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/protect/card_locker/compose/theme/Theme.kt b/app/src/main/java/protect/card_locker/compose/theme/Theme.kt
new file mode 100644
index 0000000000..c6d2ceb5f6
--- /dev/null
+++ b/app/src/main/java/protect/card_locker/compose/theme/Theme.kt
@@ -0,0 +1,51 @@
+package protect.card_locker.compose.theme
+
+import android.os.Build
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.colorResource
+import protect.card_locker.R
+import protect.card_locker.preferences.Settings
+
+@Composable
+fun CatimaTheme(content: @Composable () -> Unit) {
+ val context = LocalContext.current
+ val settings = Settings(context)
+
+ val isDynamicColorSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
+
+ val lightTheme = if (isDynamicColorSupported) {
+ dynamicLightColorScheme(context)
+ } else {
+ lightColorScheme(primary = colorResource(id = R.color.md_theme_light_primary))
+ }
+
+ var darkTheme = if (isDynamicColorSupported) {
+ dynamicDarkColorScheme(context)
+ } else {
+ darkColorScheme(primary = colorResource(id = R.color.md_theme_dark_primary))
+ }
+
+ if (settings.oledDark) {
+ darkTheme = darkTheme.copy(background = Color.Black)
+ }
+
+ val colorScheme = when (settings.theme) {
+ AppCompatDelegate.MODE_NIGHT_NO -> lightTheme
+ AppCompatDelegate.MODE_NIGHT_YES -> darkTheme
+ else -> if (isSystemInDarkTheme()) darkTheme else lightTheme
+ }
+
+ MaterialTheme(
+ colorScheme = colorScheme,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/about_activity.xml b/app/src/main/res/layout/about_activity.xml
deleted file mode 100644
index 989f6d2afa..0000000000
--- a/app/src/main/res/layout/about_activity.xml
+++ /dev/null
@@ -1,421 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index fc69ae460b..6ddbc4109e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -363,4 +363,5 @@
No value found
Barcode encoding
Automatic
+ Back
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 94d92ee69c..a4de1cbbe6 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,5 +1,4 @@
-