From c426d904de06fe624aa2dc6c22bbc62b8d547516 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 19:42:22 +0100 Subject: [PATCH 01/11] feat(quick-settings): add tile to launch app (#74) * Initial plan * feat(quick-settings): add tile to launch app Co-authored-by: pawcoding <78467484+pawcoding@users.noreply.github.com> * refactor(*): implement local review suggestions --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: pawcoding <78467484+pawcoding@users.noreply.github.com> Co-authored-by: pawcode Development --- app/src/main/AndroidManifest.xml | 14 ++++++++- .../pawcode/cardstore/AppLaunchTileService.kt | 30 +++++++++++++++++++ app/src/main/res/values-de/strings.xml | 1 + app/src/main/res/values/strings.xml | 3 +- 4 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/de/pawcode/cardstore/AppLaunchTileService.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 67fa63e..94b75ed 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,6 +13,7 @@ @@ -44,9 +45,20 @@ + + + + + + - \ No newline at end of file + diff --git a/app/src/main/java/de/pawcode/cardstore/AppLaunchTileService.kt b/app/src/main/java/de/pawcode/cardstore/AppLaunchTileService.kt new file mode 100644 index 0000000..858d8fe --- /dev/null +++ b/app/src/main/java/de/pawcode/cardstore/AppLaunchTileService.kt @@ -0,0 +1,30 @@ +package de.pawcode.cardstore + +import android.app.PendingIntent +import android.content.Intent +import android.os.Build +import android.service.quicksettings.TileService + +class AppLaunchTileService : TileService() { + override fun onClick() { + super.onClick() + + val launchIntent = + Intent(this, MainActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + startActivityAndCollapse( + PendingIntent.getActivity( + this, + 0, + launchIntent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, + ), + ) + } else { + @Suppress("DEPRECATION") startActivityAndCollapse(launchIntent) + } + } +} diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 6c9ed1b..f6c4e2f 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -70,6 +70,7 @@ Im Play Store anzeigen App bewerten und rezensieren Ein Problem entdeckt? + CardStore öffnen Barcode scannen Fehler beim Scannen Einstellungen diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 75379e1..0768194 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -66,6 +66,7 @@ View in Play Store Rate and review the app Report an issue + Open CardStore Scan barcode Error while trying to scan barcode App settings @@ -85,4 +86,4 @@ Version Visit website - \ No newline at end of file + From 9b90d56740af9cfd4ecd881e7bdd1c248b67c789 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 20 Feb 2026 18:43:22 +0000 Subject: [PATCH 02/11] chore(release): 1.5.0-staging.1 [skip ci] # [1.5.0-staging.1](https://github.com/pawcoding/card-store/compare/v1.4.3...v1.5.0-staging.1) (2026-02-20) ### Features * **quick-settings:** add tile to launch app ([#74](https://github.com/pawcoding/card-store/issues/74)) ([c426d90](https://github.com/pawcoding/card-store/commit/c426d904de06fe624aa2dc6c22bbc62b8d547516)) --- version.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.properties b/version.properties index 54c6179..8a466b0 100644 --- a/version.properties +++ b/version.properties @@ -1,2 +1,2 @@ -VERSION_CODE=61 -VERSION_NAME=1.4.3 \ No newline at end of file +VERSION_CODE=62 +VERSION_NAME=1.5.0-staging.1 \ No newline at end of file From 4b4afb24bdb23271c0ff98feee0cf7e46437388c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 21 Feb 2026 09:05:55 +0100 Subject: [PATCH 03/11] build(deps): update all non-major dependencies (#78) [skip ci] Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- app/build.gradle.kts | 2 +- gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ffa35a6..253d4ea 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -5,7 +5,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.compose) alias(libs.plugins.jetbrains.kotlin.serialization) - id("com.google.devtools.ksp") version "2.3.5" + id("com.google.devtools.ksp") version "2.3.6" id("com.ncorti.ktfmt.gradle") version "0.25.0" } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 49aac28..688184c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] activityCompose = "1.12.4" -agp = "9.0.0" +agp = "9.0.1" barcodeScanning = "17.3.0" biometric = "1.1.0" colorpickerCompose = "1.1.3" From 8cb18c363cd5518c9f648a8a1150065a7ef65804 Mon Sep 17 00:00:00 2001 From: pawcode Development Date: Sat, 21 Feb 2026 19:17:19 +0100 Subject: [PATCH 04/11] feat(shortcut): add dynamic shortcuts Long-pressing on the app icon now shows your 3 most important cards as shortcuts. These can be placed on your homescreen. When opening a shortcut an overlay shows your card without the whole app being started. --- app/src/main/AndroidManifest.xml | 12 ++ .../pawcode/cardstore/CardOverlayActivity.kt | 150 ++++++++++++++++++ .../java/de/pawcode/cardstore/MainActivity.kt | 3 + .../cardstore/data/utils/ShortcutUpdater.kt | 96 +++++++++++ .../cardstore/ui/viewmodels/CardViewModel.kt | 4 +- app/src/main/res/values-de/strings.xml | 3 +- app/src/main/res/values/strings.xml | 1 + app/src/main/res/values/themes.xml | 6 + 8 files changed, 273 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/de/pawcode/cardstore/CardOverlayActivity.kt create mode 100644 app/src/main/java/de/pawcode/cardstore/data/utils/ShortcutUpdater.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 94b75ed..e78ad9f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -45,6 +45,18 @@ + + + + + + + (null) + private var isAuthenticated by mutableStateOf(false) + private var isLoading by mutableStateOf(true) + + @OptIn(ExperimentalMaterial3Api::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val cardId = intent.getStringExtra(EXTRA_CARD_ID) + if (cardId == null) { + finish() + return + } + + lifecycleScope.launch { + val cardRepository = CardRepository(applicationContext) + val cardWithLabels = cardRepository.getCardById(cardId).first() + if (cardWithLabels == null) { + finish() + return@launch + } + card = cardWithLabels.card + isLoading = false + checkAuthentication() + } + + setContent { + CardStoreTheme { + if (!isLoading) { + val currentCard = card + if (currentCard != null) { + if (isAuthenticated) { + CardOverlayContent( + card = currentCard, + onDismiss = { finish() }, + ) + } else { + BiometricPlaceholder(onRetry = { checkAuthentication() }) + } + } + } + } + } + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + val newCardId = intent.getStringExtra(EXTRA_CARD_ID) + if (newCardId == null) { + finish() + return + } + isLoading = true + isAuthenticated = false + card = null + lifecycleScope.launch { + val cardRepository = CardRepository(applicationContext) + val cardWithLabels = cardRepository.getCardById(newCardId).first() + if (cardWithLabels == null) { + finish() + return@launch + } + card = cardWithLabels.card + isLoading = false + checkAuthentication() + } + } + + private fun checkAuthentication() { + lifecycleScope.launch { + val preferencesManager = PreferencesManager(applicationContext) + val biometricEnabled = preferencesManager.biometricEnabled.first() + if (biometricEnabled && BiometricAuthService.isBiometricAvailable(this@CardOverlayActivity)) { + BiometricAuthService.authenticate( + activity = this@CardOverlayActivity, + title = getString(R.string.biometric_auth_title), + subtitle = getString(R.string.biometric_auth_subtitle), + onSuccess = { isAuthenticated = true }, + onError = { finish() }, + ) + } else { + isAuthenticated = true + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun CardOverlayContent(card: CardEntity, onDismiss: () -> Unit) { + val sheetState = rememberModalBottomSheetState() + val interactionSource = remember { MutableInteractionSource() } + + Box( + modifier = + Modifier.fillMaxSize() + .background(Color.Transparent) + .clickable(interactionSource = interactionSource, indication = null) { onDismiss() }, + ) { + ModalBottomSheet( + modifier = Modifier.fillMaxHeight().windowInsetsPadding(WindowInsets.statusBars), + sheetState = sheetState, + onDismissRequest = onDismiss, + ) { + ViewCardSheet(card) + } + } +} diff --git a/app/src/main/java/de/pawcode/cardstore/MainActivity.kt b/app/src/main/java/de/pawcode/cardstore/MainActivity.kt index 3c3543a..eddde07 100644 --- a/app/src/main/java/de/pawcode/cardstore/MainActivity.kt +++ b/app/src/main/java/de/pawcode/cardstore/MainActivity.kt @@ -16,6 +16,7 @@ import com.google.android.play.core.review.ReviewManager import com.google.android.play.core.review.ReviewManagerFactory import com.google.mlkit.vision.codescanner.GmsBarcodeScanning import de.pawcode.cardstore.data.managers.PreferencesManager +import de.pawcode.cardstore.data.utils.updateShortcuts import de.pawcode.cardstore.data.services.BiometricAuthService import de.pawcode.cardstore.data.services.DeeplinkService import de.pawcode.cardstore.data.services.ReviewService @@ -65,6 +66,8 @@ class MainActivity : FragmentActivity() { checkAuthentication() + lifecycleScope.launch { updateShortcuts(applicationContext) } + setContent { CardStoreTheme { if (isAuthenticated) { diff --git a/app/src/main/java/de/pawcode/cardstore/data/utils/ShortcutUpdater.kt b/app/src/main/java/de/pawcode/cardstore/data/utils/ShortcutUpdater.kt new file mode 100644 index 0000000..dcca35b --- /dev/null +++ b/app/src/main/java/de/pawcode/cardstore/data/utils/ShortcutUpdater.kt @@ -0,0 +1,96 @@ +package de.pawcode.cardstore.data.utils + +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Typeface +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat +import de.pawcode.cardstore.CardOverlayActivity +import de.pawcode.cardstore.data.database.entities.CardEntity +import de.pawcode.cardstore.data.database.repositories.CardRepository +import de.pawcode.cardstore.data.enums.SortAttribute +import de.pawcode.cardstore.data.managers.PreferencesManager +import de.pawcode.cardstore.utils.calculateCardScore +import kotlinx.coroutines.flow.first + +private const val MAX_SHORTCUTS = 3 +private const val SHORTCUT_ACTION = "de.pawcode.cardstore.ACTION_VIEW_CARD" + +suspend fun updateShortcuts(context: Context) { + val cardRepository = CardRepository(context) + val preferencesManager = PreferencesManager(context) + + val allCards = cardRepository.allCards.first().map { it.card } + val sortAttribute = preferencesManager.sortAttribute.first() + + val sortedCards: List = + when (sortAttribute) { + SortAttribute.INTELLIGENT -> allCards.sortedByDescending { calculateCardScore(it) } + SortAttribute.ALPHABETICALLY -> allCards.sortedBy { it.storeName } + SortAttribute.RECENTLY_USED -> allCards.sortedByDescending { it.lastUsed } + SortAttribute.MOST_USED -> allCards.sortedByDescending { it.useCount } + } + + val topCards = sortedCards.take(MAX_SHORTCUTS) + + val newShortcutIds = topCards.map { "card_shortcut_${it.cardId}" }.toSet() + val existingShortcuts = ShortcutManagerCompat.getDynamicShortcuts(context) + val staleIds = existingShortcuts.filter { it.id !in newShortcutIds }.map { it.id } + if (staleIds.isNotEmpty()) ShortcutManagerCompat.removeDynamicShortcuts(context, staleIds) + + topCards.forEach { card -> + val shortcutId = "card_shortcut_${card.cardId}" + val icon = createShortcutIcon(card) + + val intent = + Intent(context, CardOverlayActivity::class.java).apply { + action = SHORTCUT_ACTION + putExtra(CardOverlayActivity.EXTRA_CARD_ID, card.cardId) + } + + val shortcut = + ShortcutInfoCompat.Builder(context, shortcutId) + .setShortLabel(card.storeName.take(25)) + .setLongLabel(card.storeName) + .setIcon(icon) + .setIntent(intent) + .build() + + ShortcutManagerCompat.pushDynamicShortcut(context, shortcut) + } +} + +private fun createShortcutIcon(card: CardEntity): IconCompat { + val size = 108 + val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + + val circlePaint = + Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = card.color + style = Paint.Style.FILL + } + canvas.drawCircle(54f, 54f, 36f, circlePaint) + + val r = android.graphics.Color.red(card.color) / 255f + val g = android.graphics.Color.green(card.color) / 255f + val b = android.graphics.Color.blue(card.color) / 255f + val isLight = (0.299 * r + 0.587 * g + 0.114 * b) > 0.5 + val textPaint = + Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = if (isLight) android.graphics.Color.BLACK else android.graphics.Color.WHITE + textSize = size * 0.45f + typeface = Typeface.DEFAULT_BOLD + textAlign = Paint.Align.CENTER + } + + val firstLetter = card.storeName.firstOrNull()?.uppercaseChar()?.toString() ?: "?" + val textY = 54f - (textPaint.descent() + textPaint.ascent()) / 2f + canvas.drawText(firstLetter, 54f, textY, textPaint) + + return IconCompat.createWithAdaptiveBitmap(bitmap).also { bitmap.recycle() } +} diff --git a/app/src/main/java/de/pawcode/cardstore/ui/viewmodels/CardViewModel.kt b/app/src/main/java/de/pawcode/cardstore/ui/viewmodels/CardViewModel.kt index baf812a..b662e67 100644 --- a/app/src/main/java/de/pawcode/cardstore/ui/viewmodels/CardViewModel.kt +++ b/app/src/main/java/de/pawcode/cardstore/ui/viewmodels/CardViewModel.kt @@ -8,6 +8,7 @@ import de.pawcode.cardstore.data.database.entities.CardEntity import de.pawcode.cardstore.data.database.entities.LabelEntity import de.pawcode.cardstore.data.database.repositories.CardRepository import de.pawcode.cardstore.data.database.repositories.LabelRepository +import de.pawcode.cardstore.data.utils.updateShortcuts import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch @@ -49,7 +50,8 @@ class CardViewModel(application: Application) : AndroidViewModel(application) { val updatedCard = card.copy(useCount = card.useCount + 1, lastUsed = System.currentTimeMillis()) - updateCard(updatedCard) + cardRepository.updateCard(updatedCard) + updateShortcuts(getApplication()) } fun deleteCard(card: CardEntity) = viewModelScope.launch { cardRepository.deleteCard(card) } diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index f6c4e2f..acfa81f 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -74,6 +74,7 @@ Barcode scannen Fehler beim Scannen Einstellungen + Karte öffnen Lass jemand anderen diesen QR-Code scannen, um diese Karte zu importieren. Teile deine Karte Alphabetisch @@ -87,4 +88,4 @@ Version Webseite besuchen - \ No newline at end of file + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0768194..13973d3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -70,6 +70,7 @@ Scan barcode Error while trying to scan barcode App settings + Open card Let someone else scan this QR code to import this card. Share your card Alphabetically diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index a1c6500..4cca2a9 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -3,6 +3,12 @@ +