From ae3f4b3dcdc6ccec1469bd4e48b371a8fd05eef3 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:48:15 +0000 Subject: [PATCH 1/2] Initial plan From bf48b30a11817f12b297a3245e5e514a8669688c Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:55:46 +0000 Subject: [PATCH 2/2] feat: add dynamic app shortcuts and overlay card view Add ShortcutManager to manage top 4 cards as app shortcuts. Add CardShortcutActivity to display single card as overlay. Integrate biometric authentication in overlay activity. Update shortcuts automatically when cards or sort order changes. Co-authored-by: pawcoding <78467484+pawcoding@users.noreply.github.com> --- app/src/main/AndroidManifest.xml | 9 ++ .../pawcode/cardstore/CardShortcutActivity.kt | 113 ++++++++++++++++++ .../data/managers/ShortcutManager.kt | 54 +++++++++ .../cardstore/ui/screens/CardListScreen.kt | 10 ++ 4 files changed, 186 insertions(+) create mode 100644 app/src/main/java/de/pawcode/cardstore/CardShortcutActivity.kt create mode 100644 app/src/main/java/de/pawcode/cardstore/data/managers/ShortcutManager.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 67fa63e..626fc93 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -44,6 +44,15 @@ + + + diff --git a/app/src/main/java/de/pawcode/cardstore/CardShortcutActivity.kt b/app/src/main/java/de/pawcode/cardstore/CardShortcutActivity.kt new file mode 100644 index 0000000..1f0722f --- /dev/null +++ b/app/src/main/java/de/pawcode/cardstore/CardShortcutActivity.kt @@ -0,0 +1,113 @@ +package de.pawcode.cardstore + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.dp +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.lifecycleScope +import de.pawcode.cardstore.data.database.entities.CardEntity +import de.pawcode.cardstore.data.database.repositories.CardRepository +import de.pawcode.cardstore.data.managers.PreferencesManager +import de.pawcode.cardstore.data.managers.ShortcutManager +import de.pawcode.cardstore.data.services.BiometricAuthService +import de.pawcode.cardstore.ui.components.BiometricPlaceholder +import de.pawcode.cardstore.ui.sheets.ViewCardSheet +import de.pawcode.cardstore.ui.theme.CardStoreTheme +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +class CardShortcutActivity : FragmentActivity() { + private var cardRepository: CardRepository? = null + private var preferencesManager: PreferencesManager? = null + private var card by mutableStateOf(null) + private var isAuthenticated by mutableStateOf(false) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + preferencesManager = PreferencesManager(applicationContext) + cardRepository = CardRepository(applicationContext) + + val cardId = intent.getStringExtra(ShortcutManager.EXTRA_CARD_ID) + + if (cardId == null) { + finish() + return + } + + lifecycleScope.launch { + val cardWithLabels = cardRepository?.getCardById(cardId)?.first() + card = cardWithLabels?.card + if (card == null) { + finish() + return@launch + } + + // Increment usage count + card?.let { currentCard -> + val updatedCard = + currentCard.copy(useCount = currentCard.useCount + 1, lastUsed = System.currentTimeMillis()) + cardRepository?.updateCard(updatedCard) + } + } + + checkAuthentication() + + setContent { + CardStoreTheme { + Box( + modifier = + Modifier.fillMaxSize() + .background(Color.Black.copy(alpha = 0.5f)) + .pointerInput(Unit) { detectTapGestures(onTap = { finish() }) }, + contentAlignment = Alignment.Center, + ) { + Box( + modifier = + Modifier.padding(16.dp).pointerInput(Unit) { + detectTapGestures(onTap = { + // Prevent tap from propagating to parent + }) + } + ) { + if (isAuthenticated) { + card?.let { ViewCardSheet(it) } + } else { + BiometricPlaceholder(onRetry = { checkAuthentication() }) + } + } + } + } + } + } + + private fun checkAuthentication() { + lifecycleScope.launch { + val biometricEnabled = preferencesManager?.biometricEnabled?.first() ?: false + if (biometricEnabled && BiometricAuthService.isBiometricAvailable(this@CardShortcutActivity)) { + BiometricAuthService.authenticate( + activity = this@CardShortcutActivity, + title = getString(R.string.biometric_auth_title), + subtitle = getString(R.string.biometric_auth_subtitle), + onSuccess = { isAuthenticated = true }, + onError = { finish() }, + ) + } else { + isAuthenticated = true + } + } + } +} diff --git a/app/src/main/java/de/pawcode/cardstore/data/managers/ShortcutManager.kt b/app/src/main/java/de/pawcode/cardstore/data/managers/ShortcutManager.kt new file mode 100644 index 0000000..0c8e742 --- /dev/null +++ b/app/src/main/java/de/pawcode/cardstore/data/managers/ShortcutManager.kt @@ -0,0 +1,54 @@ +package de.pawcode.cardstore.data.managers + +import android.content.Context +import android.content.Intent +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat +import de.pawcode.cardstore.CardShortcutActivity +import de.pawcode.cardstore.R +import de.pawcode.cardstore.data.database.entities.CardEntity +import de.pawcode.cardstore.data.enums.SortAttribute +import de.pawcode.cardstore.utils.calculateCardScore + +object ShortcutManager { + private const val MAX_SHORTCUTS = 4 + const val EXTRA_CARD_ID = "card_id" + + fun updateShortcuts(context: Context, cards: List, sortAttribute: SortAttribute) { + val topCards = getTopCards(cards, sortAttribute).take(MAX_SHORTCUTS) + + val shortcuts = + topCards.mapIndexed { index, card -> + val intent = + Intent(context, CardShortcutActivity::class.java).apply { + action = Intent.ACTION_VIEW + putExtra(EXTRA_CARD_ID, card.cardId) + } + + ShortcutInfoCompat.Builder(context, "card_${card.cardId}") + .setShortLabel(card.storeName) + .setLongLabel(card.storeName) + .setIcon(IconCompat.createWithResource(context, R.mipmap.ic_launcher)) + .setIntent(intent) + .setRank(index) + .build() + } + + ShortcutManagerCompat.removeAllDynamicShortcuts(context) + ShortcutManagerCompat.addDynamicShortcuts(context, shortcuts) + } + + fun clearShortcuts(context: Context) { + ShortcutManagerCompat.removeAllDynamicShortcuts(context) + } + + private fun getTopCards(cards: List, sortAttribute: SortAttribute): List { + return when (sortAttribute) { + SortAttribute.INTELLIGENT -> cards.sortedByDescending { calculateCardScore(it) } + SortAttribute.ALPHABETICALLY -> cards.sortedBy { it.storeName } + SortAttribute.RECENTLY_USED -> cards.sortedByDescending { it.lastUsed } + SortAttribute.MOST_USED -> cards.sortedByDescending { it.useCount } + } + } +} diff --git a/app/src/main/java/de/pawcode/cardstore/ui/screens/CardListScreen.kt b/app/src/main/java/de/pawcode/cardstore/ui/screens/CardListScreen.kt index 863b35e..d1d3183 100644 --- a/app/src/main/java/de/pawcode/cardstore/ui/screens/CardListScreen.kt +++ b/app/src/main/java/de/pawcode/cardstore/ui/screens/CardListScreen.kt @@ -43,6 +43,7 @@ import de.pawcode.cardstore.data.database.entities.EXAMPLE_LABEL_LIST import de.pawcode.cardstore.data.database.entities.LabelEntity import de.pawcode.cardstore.data.enums.SortAttribute import de.pawcode.cardstore.data.managers.PreferencesManager +import de.pawcode.cardstore.data.managers.ShortcutManager import de.pawcode.cardstore.data.services.DeeplinkService import de.pawcode.cardstore.data.services.ReviewService import de.pawcode.cardstore.data.services.SnackbarService @@ -137,6 +138,7 @@ fun CardListScreenComponent( onSortChange: (SortAttribute) -> Unit, onShowAbout: () -> Unit, ) { + val context = LocalContext.current val cards by cardsFlow.collectAsState(initial = emptyList()) val labels by labelsFlow.collectAsState(initial = emptyList()) val sortBy by sortByFlow.collectAsState(initial = null) @@ -183,6 +185,14 @@ fun CardListScreenComponent( LaunchedEffect(sortBy, selectedLabel) { listState.scrollToItem(0) } + // Update app shortcuts when cards or sort order changes + val currentContext = context + LaunchedEffect(cardsFiltered, sortBy) { + sortBy?.let { currentSortBy -> + ShortcutManager.updateShortcuts(currentContext, cardsFiltered, currentSortBy) + } + } + Scaffold( topBar = { AppBar(