diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e79c758b2..7743014f7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -3,6 +3,7 @@ import com.android.build.gradle.internal.tasks.factory.dependsOn plugins { id("com.android.application") id("org.jetbrains.kotlin.android") + id("org.jetbrains.kotlin.plugin.compose") version "2.1.21" } kotlin { @@ -46,6 +47,7 @@ android { buildFeatures { buildConfig = true + compose = true viewBinding = true } @@ -109,6 +111,7 @@ android { dependencies { // AndroidX + implementation("androidx.activity:activity-compose:1.10.1") implementation("androidx.appcompat:appcompat:1.7.0") implementation("androidx.constraintlayout:constraintlayout:2.2.1") implementation("androidx.core:core-ktx:1.16.0") @@ -119,6 +122,15 @@ dependencies { implementation("com.google.android.material:material:1.12.0") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5") + // Compose + val composeBom = platform("androidx.compose:compose-bom:2025.05.01") + implementation(composeBom) + testImplementation(composeBom) + androidTestImplementation(composeBom) + implementation("androidx.compose.foundation:foundation") + implementation("androidx.compose.material3:material3") + implementation("androidx.compose.ui:ui-tooling-preview-android") + // Third-party implementation("com.journeyapps:zxing-android-embedded:4.3.0@aar") implementation("com.github.yalantis:ucrop:2.2.10") diff --git a/app/src/main/java/protect/card_locker/AboutActivity.kt b/app/src/main/java/protect/card_locker/AboutActivity.kt index ed3af7da7..d6ecfada2 100644 --- a/app/src/main/java/protect/card_locker/AboutActivity.kt +++ b/app/src/main/java/protect/card_locker/AboutActivity.kt @@ -1,149 +1,155 @@ 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.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.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 + setContent { + ScreenContent( + showDonate = BuildConfig.showDonate, + showRateOnGooglePlay = BuildConfig.showRateOnGooglePlay + ) } - - bindClickListeners() } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - android.R.id.home -> { - finish() - true + @Composable + fun ScreenContent( + showDonate: Boolean, + showRateOnGooglePlay: Boolean + ) { + CatimaTheme { + Scaffold( + topBar = { CatimaTopAppBar(title.toString(), onBackPressedDispatcher) } + ) { innerPadding -> + Column( + modifier = Modifier.padding(innerPadding).verticalScroll(rememberScrollState()) + ) { + CatimaAboutSection( + stringResource(R.string.version_history), + content.versionHistory, + 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, + 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), + onClickUrl = "https://hosted.weblate.org/engage/catima/" + ) + CatimaAboutSection( + stringResource(R.string.license), + stringResource(R.string.app_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), + onClickUrl = "https://github.com/CatimaLoyalty/Android/" + ) + CatimaAboutSection( + stringResource(R.string.privacy_policy), + stringResource(R.string.and_data_usage), + 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), + "", + onClickUrl = "https://catima.app/donate" + ) + } + if (showRateOnGooglePlay) { + CatimaAboutSection( + stringResource(R.string.rate_this_app), + stringResource(R.string.on_google_play), + onClickUrl = "https://play.google.com/store/apps/details?id=me.hackerchick.catima" + ) + } + CatimaAboutSection( + stringResource(R.string.report_error), + stringResource(R.string.on_github), + onClickUrl = "https://github.com/CatimaLoyalty/Android/issues" + ) + } } - - 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) } - } - - show() - } - } - - private fun openExternalBrowser(view: View) { - val tag = view.tag - if (tag is String && tag.startsWith("https://")) { - OpenWebLinkHandler().openBrowser(this, tag) - } + @Preview + @Composable + fun AboutActivityPreview() { + ScreenContent( + showDonate = true, + showRateOnGooglePlay = true + ) } } diff --git a/app/src/main/java/protect/card_locker/AboutContent.java b/app/src/main/java/protect/card_locker/AboutContent.java index e4e8d87fc..e1d7ead70 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("Color Picker", "https://github.com/jaredrummler/ColorPicker", "Apache 2.0")); usedLibraries.add(new ThirdPartyInfo("Commons CSV", "https://commons.apache.org/proper/commons-csv/", "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/OpenWebLinkHandler.java b/app/src/main/java/protect/card_locker/OpenWebLinkHandler.java index 1586ac4f2..8bfcef4f6 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/compose/AboutActivity.kt b/app/src/main/java/protect/card_locker/compose/AboutActivity.kt new file mode 100644 index 000000000..555b76690 --- /dev/null +++ b/app/src/main/java/protect/card_locker/compose/AboutActivity.kt @@ -0,0 +1,92 @@ +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, onClickUrl: String? = null, onClickDialogText: AnnotatedString? = null) { + val activity = LocalActivity.current + + val openDialog = remember { mutableStateOf(false) } + + Column( + modifier = Modifier + .padding(8.dp) + .clickable { + if (onClickDialogText != null) { + openDialog.value = true + } else if (onClickUrl != null) { + OpenWebLinkHandler().openBrowser(activity, onClickUrl) + } + } + ) { + Row { + Column(modifier = Modifier.weight(1F)) { + Text( + text = title, + style = MaterialTheme.typography.titleLarge + ) + Text(text = message) + } + Text(modifier = Modifier.align(Alignment.CenterVertically), + text = ">", + style = MaterialTheme.typography.titleMedium + ) + } + } + 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 000000000..1aaa07def --- /dev/null +++ b/app/src/main/java/protect/card_locker/compose/Catima.kt @@ -0,0 +1,31 @@ +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.res.stringResource +import protect.card_locker.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CatimaTopAppBar(title: String, onBackPressedDispatcher: OnBackPressedDispatcher?) { + TopAppBar( + title = { + Text(text = title) + }, + navigationIcon = { if (onBackPressedDispatcher != null) { + IconButton(onClick = { onBackPressedDispatcher.onBackPressed() }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.back) + ) + } + } else null } + ) +} \ 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 000000000..c6f23e303 --- /dev/null +++ b/app/src/main/java/protect/card_locker/compose/theme/Theme.kt @@ -0,0 +1,46 @@ +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.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)) + } + + val darkTheme = if (isDynamicColorSupported) { + dynamicDarkColorScheme(context) + } else { + darkColorScheme(primary = colorResource(id = R.color.md_theme_dark_primary)) + } + + 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 989f6d2af..000000000 --- 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 d78a86dc2..7c8e47a13 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -363,4 +363,5 @@ Sorry, something went wrong, please try again... Width Set Barcode Width + Back diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 94d92ee69..a4de1cbbe 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,5 +1,4 @@ -