Compare commits

..

20 Commits

Author SHA1 Message Date
Sylvia van Os
d54323fde0 Merge pull request #2954 from CatimaLoyalty/create-pull-request/patch-1767975957
Update Fastlane changelogs
2026-01-09 17:28:06 +01:00
TheLastProject
c02be50242 Update Fastlane changelogs 2026-01-09 16:25:57 +00:00
Sylvia van Os
a883ce0f43 Update CHANGELOG 2026-01-09 17:25:46 +01:00
Matthias Paulmier
ace353d71d Fix several bugs related to shortcut handling (#2919)
* Improve ShortcuHelper.updateShortcuts to take all actions into account

* Remove now useless calls to removeShortcut

* Add doc to explain the usage of maxShortcut

* Fix typo in doc of maxShortcuts
2026-01-09 17:24:29 +01:00
Sylvia van Os
18dbb24375 Merge pull request #2953 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2026-01-09 08:18:47 +01:00
m45ked
78daf54716 Translated using Weblate (Polish)
Currently translated at 95.4% (147 of 154 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/pl/
2026-01-09 05:56:05 +01:00
Sylvia van Os
ee6541ba54 Merge pull request #2952 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2026-01-08 16:42:28 +01:00
B o d o
e4e9fe05e1 Translated using Weblate (German)
Currently translated at 100.0% (155 of 155 strings)

Translation: Catima/Android (Fastlane)
Translate-URL: https://hosted.weblate.org/projects/catima/fastlane/de/
2026-01-08 11:36:36 +00:00
Sylvia van Os
9d4035c94e Merge pull request #2951 from CatimaLoyalty/create-pull-request/patch-1767812819
Update Fastlane changelogs
2026-01-07 20:09:53 +01:00
TheLastProject
293d38bd09 Update Fastlane changelogs 2026-01-07 19:06:59 +00:00
Sylvia van Os
28be05600b Update CHANGELOG 2026-01-07 20:06:46 +01:00
Sylvia van Os
03649820ce Merge pull request #2950 from CatimaLoyalty/fix/2918
Set Compose TopAppBar to pure black in OLED theme
2026-01-07 20:05:51 +01:00
Sylvia van Os
93515d2f88 Set Compose TopAppBar to pure black in OLED theme 2026-01-07 17:17:49 +01:00
Sylvia van Os
d7dc70c0df Merge pull request #2949 from weblate/weblate-catima-catima
Translations update from Hosted Weblate
2026-01-06 17:50:23 +01:00
Francisco Serrador
ce0738782a Translated using Weblate (Spanish)
Currently translated at 100.0% (316 of 316 strings)

Translation: Catima/Android
Translate-URL: https://hosted.weblate.org/projects/catima/catima/es/
2026-01-06 13:02:02 +00:00
Sylvia van Os
df738b9c1d Merge pull request #2948 from CatimaLoyalty/create-pull-request/patch-1767646247
Update Fastlane changelogs
2026-01-05 21:52:18 +01:00
TheLastProject
8bf0033d22 Update Fastlane changelogs 2026-01-05 20:50:47 +00:00
Sylvia van Os
01e08e4928 Update CHANGELOG 2026-01-05 21:50:33 +01:00
Sylvia van Os
90aee54a3c Merge pull request #2947 from CatimaLoyalty/fix/2945
Fix list widget opening on previous card sometimes
2026-01-05 21:49:50 +01:00
Sylvia van Os
b7c42b5c8c Fix list widget opening on previous card sometimes 2026-01-05 20:20:41 +01:00
13 changed files with 209 additions and 88 deletions

View File

@@ -1,5 +1,11 @@
# Changelog
## Unreleased - 162
- Fix list widget sometimes opening wrong card
- Fix several bugs with shortcut handling
- Fix About activity not using pure black title bar in OLED mode
## v2.41.4 - 161 (2026-01-04)
- Disable automatic barcode encoding detection for now (breaks too many cards)

View File

@@ -69,7 +69,9 @@ class ListWidget : AppWidgetProvider() {
if (hasCards) {
// If we have cards, create the list
views = RemoteViews(context.packageName, R.layout.list_widget)
val templateIntent = Intent(context, LoyaltyCardViewActivity::class.java)
val templateIntent = Intent(context, LoyaltyCardViewActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent = PendingIntent.getActivity(
context,
0,

View File

@@ -1532,7 +1532,7 @@ public class LoyaltyCardEditActivity extends CatimaAppCompatActivity implements
DBHelper.setLoyaltyCardGroups(mDatabase, viewModel.getLoyaltyCardId(), selectedGroups);
ShortcutHelper.updateShortcuts(this, DBHelper.getLoyaltyCard(this, mDatabase, viewModel.getLoyaltyCardId()));
ShortcutHelper.updateShortcuts(this);
if (viewModel.getDuplicateFromLoyaltyCardId()) {
Intent intent = new Intent(getApplicationContext(), MainActivity.class);

View File

@@ -794,7 +794,7 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
invalidateOptionsMenu();
ShortcutHelper.updateShortcuts(this, loyaltyCard);
ShortcutHelper.updateShortcuts(this);
}
private void setStateBasedOnImageTypes() {
@@ -896,7 +896,6 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
DBHelper.updateLoyaltyCardArchiveStatus(database, loyaltyCardId, 1);
Toast.makeText(LoyaltyCardViewActivity.this, R.string.archived, Toast.LENGTH_LONG).show();
ShortcutHelper.removeShortcut(LoyaltyCardViewActivity.this, loyaltyCardId);
new ListWidget().updateAll(LoyaltyCardViewActivity.this);
// Re-init loyaltyCard with new data from DB
@@ -922,7 +921,6 @@ public class LoyaltyCardViewActivity extends CatimaAppCompatActivity implements
DBHelper.deleteLoyaltyCard(database, LoyaltyCardViewActivity.this, loyaltyCardId);
ShortcutHelper.removeShortcut(LoyaltyCardViewActivity.this, loyaltyCardId);
new ListWidget().updateAll(LoyaltyCardViewActivity.this);
finish();

View File

@@ -156,8 +156,6 @@ class MainActivity : CatimaAppCompatActivity(), CardAdapterListener {
Log.d(TAG, "Deleting card: " + loyaltyCard.id)
DBHelper.deleteLoyaltyCard(mDatabase, this@MainActivity, loyaltyCard.id)
ShortcutHelper.removeShortcut(this@MainActivity, loyaltyCard.id)
}
val tab = groupsTabLayout.getTabAt(selectedTab)
mGroup = tab?.tag
@@ -177,7 +175,6 @@ class MainActivity : CatimaAppCompatActivity(), CardAdapterListener {
for (loyaltyCard in mAdapter.getSelectedItems()) {
Log.d(TAG, "Archiving card: " + loyaltyCard.id)
DBHelper.updateLoyaltyCardArchiveStatus(mDatabase, loyaltyCard.id, 1)
ShortcutHelper.removeShortcut(this@MainActivity, loyaltyCard.id)
updateLoyaltyCardList(false)
inputMode.finish()
invalidateOptionsMenu()
@@ -466,6 +463,7 @@ class MainActivity : CatimaAppCompatActivity(), CardAdapterListener {
}
ListWidget().updateAll(mAdapter.mContext)
ShortcutHelper.updateShortcuts(mAdapter.mContext)
}
private fun processParseResultList(

View File

@@ -2,31 +2,35 @@ package protect.card_locker;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.os.Bundle;
import androidx.annotation.VisibleForTesting;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.content.pm.ShortcutManagerCompat;
import androidx.core.graphics.ColorUtils;
import androidx.core.graphics.drawable.IconCompat;
import org.jetbrains.annotations.NotNull;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
class ShortcutHelper {
// Android documentation says that no more than 5 shortcuts
// are supported. However, that may be too many, as not all
// launcher will show all 5. Instead, the number is limited
// to 3 here, so that the most recent shortcut has a good
// chance of being shown.
private static final int MAX_SHORTCUTS = 3;
/**
* This variable controls the maximum number of shortcuts available.
* It is made public only to make testing easier and should not be
* manually modified. We use -1 here as a default value to check if
* the value has been set either manually by the test scenario or
* automatically in the `updateShortcuts` function.
* Its actual value will be set based on the maximum amount of shortcuts
* declared by the launcher via `getMaxShortcutCountPerActivity`.
*/
@VisibleForTesting
public static int maxShortcuts = -1;
// https://developer.android.com/reference/android/graphics/drawable/AdaptiveIconDrawable.html
private static final int ADAPTIVE_BITMAP_SCALE = 1;
@@ -35,86 +39,42 @@ class ShortcutHelper {
private static final int ADAPTIVE_BITMAP_IMAGE_SIZE = ADAPTIVE_BITMAP_VISIBLE_SIZE + 5 * ADAPTIVE_BITMAP_SCALE;
/**
* Add a card to the app shortcuts, and maintain a list of the most
* recently used cards. If there is already a shortcut for the card,
* the card is marked as the most recently used card. If adding this
* card exceeds the max number of shortcuts, then the least recently
* used card shortcut is discarded.
* Update the dynamic shortcut list with the most recently viewed cards
* based on the lastUsed field. Archived cards are excluded from the shortcuts
* list. The list keeps at most maxShortcuts number of elements.
*/
static void updateShortcuts(Context context, LoyaltyCard card) {
if (card.archiveStatus == 1) {
// Don't add archived card to menu
return;
static void updateShortcuts(Context context) {
if (maxShortcuts == -1) {
maxShortcuts = ShortcutManagerCompat.getMaxShortcutCountPerActivity(context);
}
LinkedList<ShortcutInfoCompat> list = new LinkedList<>(ShortcutManagerCompat.getDynamicShortcuts(context));
SQLiteDatabase database = new DBHelper(context).getReadableDatabase();
String shortcutId = Integer.toString(card.id);
// Sort the shortcuts by rank, so working with the relative order will be easier.
// This sorts so that the lowest rank is first.
Collections.sort(list, Comparator.comparingInt(ShortcutInfoCompat::getRank));
Integer foundIndex = null;
for (int index = 0; index < list.size(); index++) {
if (list.get(index).getId().equals(shortcutId)) {
// Found the item already
foundIndex = index;
break;
}
}
if (foundIndex != null) {
// If the item is already found, then the list needs to be
// reordered, so that the selected item now has the lowest
// rank, thus letting it survive longer.
ShortcutInfoCompat found = list.remove(foundIndex.intValue());
list.addFirst(found);
} else {
// The item is new to the list. We add it and trim the list later.
ShortcutInfoCompat shortcut = createShortcutBuilder(context, card).build();
list.addFirst(shortcut);
}
LinkedList<ShortcutInfoCompat> finalList = new LinkedList<>();
SQLiteDatabase database = new DBHelper(context).getReadableDatabase();
Cursor loyaltyCardCursor = DBHelper.getLoyaltyCardCursor(
database,
"",
null,
DBHelper.LoyaltyCardOrder.LastUsed,
DBHelper.LoyaltyCardOrderDirection.Ascending,
DBHelper.LoyaltyCardArchiveFilter.Unarchived
);
int rank = 0;
// The ranks are now updated; the order in the list is the rank.
for (int index = 0; index < list.size(); index++) {
ShortcutInfoCompat prevShortcut = list.get(index);
while (rank < maxShortcuts && loyaltyCardCursor.moveToNext()) {
int id = loyaltyCardCursor.getInt(loyaltyCardCursor.getColumnIndexOrThrow(DBHelper.LoyaltyCardDbIds.ID));
LoyaltyCard loyaltyCard = DBHelper.getLoyaltyCard(context, database, id);
LoyaltyCard loyaltyCard = DBHelper.getLoyaltyCard(context, database, Integer.parseInt(prevShortcut.getId()));
ShortcutInfoCompat updatedShortcut = createShortcutBuilder(context, loyaltyCard)
.setRank(rank)
.build();
// skip outdated cards that no longer exist
if (loyaltyCard != null) {
ShortcutInfoCompat updatedShortcut = createShortcutBuilder(context, loyaltyCard)
.setRank(rank)
.build();
finalList.addLast(updatedShortcut);
rank++;
// trim the list
if (rank >= MAX_SHORTCUTS) {
break;
}
}
finalList.addLast(updatedShortcut);
rank++;
}
ShortcutManagerCompat.setDynamicShortcuts(context, finalList);
}
/**
* Remove the given card id from the app shortcuts, if such a
* shortcut exists.
*/
static void removeShortcut(Context context, int cardId) {
ShortcutManagerCompat.removeDynamicShortcuts(context, Collections.singletonList(Integer.toString(cardId)));
}
static @NotNull
Bitmap createAdaptiveBitmap(@NotNull Bitmap in, int paddingColor) {
Bitmap ret = Bitmap.createBitmap(ADAPTIVE_BITMAP_SIZE, ADAPTIVE_BITMAP_SIZE, Bitmap.Config.ARGB_8888);

View File

@@ -1,6 +1,8 @@
package protect.card_locker.compose
import androidx.activity.OnBackPressedDispatcher
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -8,18 +10,38 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import protect.card_locker.R
import protect.card_locker.preferences.Settings
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CatimaTopAppBar(title: String, onBackPressedDispatcher: OnBackPressedDispatcher?) {
// Use pure black in OLED theme
val context = LocalContext.current
val settings = Settings(context)
val isDarkMode = when (settings.theme) {
AppCompatDelegate.MODE_NIGHT_NO -> false
AppCompatDelegate.MODE_NIGHT_YES -> true
else -> isSystemInDarkTheme()
}
val appBarColors = if (isDarkMode && settings.oledDark) {
TopAppBarDefaults.topAppBarColors().copy(containerColor = Color.Black)
} else {
TopAppBarDefaults.topAppBarColors()
}
TopAppBar(
modifier = Modifier.testTag("topbar_catima"),
title = { Text(text = title) },
colors = appBarColors,
navigationIcon = {
if (onBackPressedDispatcher != null) {
IconButton(onClick = { onBackPressedDispatcher.onBackPressed() }) {

View File

@@ -10,7 +10,7 @@
<string name="edit">Editar</string>
<string name="delete">Eliminar</string>
<string name="confirm">Confirmar</string>
<string name="ok">De acuerdo</string>
<string name="ok">Aceptar</string>
<string name="sendLabel">Enviar…</string>
<string name="editCardTitle">Editar tarjeta</string>
<string name="addCardTitle">Añadir tarjeta</string>
@@ -134,7 +134,7 @@
<item quantity="many"><xliff:g>%d</xliff:g> seleccionadas</item>
<item quantity="other"><xliff:g>%d</xliff:g> seleccionadas</item>
</plurals>
<string name="deleteTitle">Eliminar la tarjeta</string>
<string name="deleteTitle">Eliminar tarjeta</string>
<string name="deleteConfirmation">¿Quiere eliminar permanentemente esta tarjeta\?</string>
<plurals name="deleteCardsConfirmation">
<item quantity="one">¿Borrar esta tarjeta <xliff:g>%d</xliff:g> permanentemente\?</item>
@@ -305,4 +305,7 @@
<string name="copy_value">Copia valor</string>
<string name="copied_to_clipboard">Copiado al portapapeles</string>
<string name="nothing_to_copy">Ningún valor encontrado</string>
<string name="barcodeEncoding">Codificación de barra de código</string>
<string name="automatic">Automático</string>
<string name="back">Atrás</string>
</resources>

View File

@@ -0,0 +1,124 @@
package protect.card_locker;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import android.app.Activity;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Color;
import androidx.core.content.pm.ShortcutInfoCompat;
import androidx.core.content.pm.ShortcutManagerCompat;
import com.google.zxing.BarcodeFormat;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.android.controller.ActivityController;
import java.math.BigDecimal;
import java.util.Comparator;
@RunWith(RobolectricTestRunner.class)
public class ShortcutHelperTest {
private Activity mActivity;
private SQLiteDatabase mDatabase;
private int id1;
private int id2;
private int id3;
private int id4;
@Before
public void setUp() {
mActivity = Robolectric.setupActivity(MainActivity.class);
mDatabase = TestHelpers.getEmptyDb(mActivity).getWritableDatabase();
long now = System.currentTimeMillis();
id1 = (int) DBHelper.insertLoyaltyCard(mDatabase, "store1", "note1", null, null, new BigDecimal("0"), null, "cardId1", null, CatimaBarcode.fromBarcode(BarcodeFormat.UPC_A), null, Color.BLACK, 0, now,0);
id2 = (int) DBHelper.insertLoyaltyCard(mDatabase, "store2", "note2", null, null, new BigDecimal("0"), null, "cardId2", null, CatimaBarcode.fromBarcode(BarcodeFormat.UPC_A), null, Color.BLACK, 0, now + 10,0);
id3 = (int) DBHelper.insertLoyaltyCard(mDatabase, "store3", "note3", null, null, new BigDecimal("0"), null, "cardId3", null, CatimaBarcode.fromBarcode(BarcodeFormat.UPC_A), null, Color.BLACK, 0, now + 20,0);
id4 = (int) DBHelper.insertLoyaltyCard(mDatabase, "store4", "note4", null, null, new BigDecimal("0"), null, "cardId4", null, CatimaBarcode.fromBarcode(BarcodeFormat.UPC_A), null, Color.BLACK, 0, now + 30,0);
ShortcutHelper.maxShortcuts = 3;
}
private Integer[] getShortcutIds(Context context) {
return ShortcutManagerCompat.getDynamicShortcuts(context)
.stream()
.sorted(Comparator.comparingInt(ShortcutInfoCompat::getRank))
.map(shortcut -> Integer.parseInt(shortcut.getId()))
.toArray(Integer[]::new);
}
@Test
public void onArchiveUnarchive() {
ActivityController activityController = Robolectric.buildActivity(MainActivity.class).create();
Activity mainActivity = (Activity) activityController.get();
activityController.pause();
activityController.resume();
assertEquals(3, ShortcutManagerCompat.getDynamicShortcuts(mainActivity).stream().count());
Integer[] ids = getShortcutIds(mainActivity);
assertArrayEquals(new Integer[] {id4, id3, id2}, ids);
DBHelper.updateLoyaltyCardArchiveStatus(mDatabase, id4, 1);
activityController.pause();
activityController.resume();
Integer[] idsAfterArchive = getShortcutIds(mainActivity);
assertArrayEquals(new Integer[] {id3, id2, id1}, idsAfterArchive);
DBHelper.updateLoyaltyCardArchiveStatus(mDatabase, id4, 0);
activityController.pause();
activityController.resume();
Integer[] idsAfterUnarchive = getShortcutIds(mainActivity);
assertArrayEquals(new Integer[] {id4, id3, id2}, idsAfterUnarchive);
}
@Test
public void onAddRemoveFavorite() {
ActivityController activityController = Robolectric.buildActivity(MainActivity.class).create();
Activity mainActivity = (Activity) activityController.get();
activityController.pause();
activityController.resume();
assertEquals(3, ShortcutManagerCompat.getDynamicShortcuts(mainActivity).stream().count());
Integer[] ids = getShortcutIds(mainActivity);
assertArrayEquals(new Integer[] {id4, id3, id2}, ids);
DBHelper.updateLoyaltyCardStarStatus(mDatabase, id1, 1);
activityController.pause();
activityController.resume();
Integer[] idsAfterFav = getShortcutIds(mainActivity);
assertArrayEquals(new Integer[] {id1, id4, id3}, idsAfterFav);
DBHelper.updateLoyaltyCardStarStatus(mDatabase, id1, 0);
activityController.pause();
activityController.resume();
Integer[] idsAfterUnfav = getShortcutIds(mainActivity);
assertArrayEquals(new Integer[] {id4, id3, id2}, idsAfterUnfav);
}
}

View File

@@ -3,4 +3,4 @@
- Einstellung für die Spaltenanzahl funktioniert nun auch in der Kartenansicht für Gruppen
- Unterstützung für Designfarben entfernt
- Maximale Fotogröße reduziert, um Speicherplatz zu sparen (gilt nur für neu hinzugefügte Fotos)
- AboutActivity von Android XML zu Jetpack Compose migriert
- About Activity von Android XML zu Jetpack Compose migriert

View File

@@ -0,0 +1,3 @@
- Fix list widget sometimes opening wrong card
- Fix several bugs with shortcut handling
- Fix About activity not using pure black title bar in OLED mode

View File

@@ -0,0 +1,2 @@
- Nowy projekt logo Catima
- Aktualizacja tłumaczeń

View File

@@ -0,0 +1,3 @@
- Dodanie wsparcia dla plików .pkpasses
- Usunięcie importera Stocard (Stocard już nie istnieje)
- Tymczasowe wyłączenie widżetów dla Androida poniżej 12L (obejście problemu awarii)