557: Group management POC for #129 r=TheLastProject a=Kethen

Proof of concept for #129, please review and comment on desired changes
A few new strings are added so translations will be needed

meanwhile I think all added group has to be trimmed to avoid having identical groups like in the example

https://user-images.githubusercontent.com/22017945/139289359-32f42b48-a40e-4039-8f0b-72bbe43cd0de.mp4



https://user-images.githubusercontent.com/22017945/139294845-28c88e03-871f-49d1-9b45-f0648f2f7b50.mp4






Co-authored-by: Katharine <kwchuiaa@connect.ust.hk>
Co-authored-by: Katharine Chui <kwchuiaa@connect.ust.hk>
This commit is contained in:
bors[bot]
2021-11-03 18:40:28 +00:00
committed by GitHub
8 changed files with 491 additions and 66 deletions

View File

@@ -1,14 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="protect.card_locker"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="protect.card_locker">
<uses-permission
android:name="android.permission.CAMERA"/>
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-sdk tools:overrideLibrary="com.google.zxing.client.android" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-feature
android:name="android.hardware.camera"
@@ -17,8 +16,6 @@
android:name="android.hardware.camera.autofocus"
android:required="false" />
<uses-sdk tools:overrideLibrary="com.google.zxing.client.android"/>
<application
android:name=".LoyaltyCardLockerApplication"
android:allowBackup="true"
@@ -27,98 +24,106 @@
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name="protect.card_locker.MainActivity"
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.App.Starting"
android:exported="true">
android:theme="@style/Theme.App.Starting">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER"/>
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".AboutActivity"
android:label="@string/about"
android:theme="@style/AppTheme.NoActionBar">
</activity>
android:theme="@style/AppTheme.NoActionBar"></activity>
<activity
android:name=".ManageGroupsActivity"
android:label="@string/groups"
android:theme="@style/AppTheme.NoActionBar">
</activity>
android:theme="@style/AppTheme.NoActionBar"></activity>
<activity
android:name=".ManageGroupActivity"
android:label="@string/group_edit"
android:theme="@style/AppTheme.NoActionBar"/>
<activity
android:name=".LoyaltyCardViewActivity"
android:exported="true"
android:theme="@style/AppTheme.NoActionBar"
android:windowSoftInputMode="stateHidden"
android:exported="true"/>
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".LoyaltyCardEditActivity"
android:exported="true"
android:theme="@style/AppTheme.NoActionBar"
android:windowSoftInputMode="stateHidden"
android:exported="true">
<intent-filter android:autoVerify="true" android:label="@string/app_name">
android:windowSoftInputMode="stateHidden">
<intent-filter
android:autoVerify="true"
android:label="@string/app_name">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Main card sharing URIs -->
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="@string/intent_import_card_from_url_host_catima_app"
<data
android:host="@string/intent_import_card_from_url_host_catima_app"
android:pathPrefix="@string/intent_import_card_from_url_path_prefix_catima_app" />
</intent-filter>
<intent-filter android:label="@string/app_name">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Old card sharing URIs -->
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="@string/intent_import_card_from_url_host_thelastproject"
<data
android:host="@string/intent_import_card_from_url_host_thelastproject"
android:pathPrefix="@string/intent_import_card_from_url_path_prefix_thelastproject" />
<data android:host="@string/intent_import_card_from_url_host_brarcher"
<data
android:host="@string/intent_import_card_from_url_host_brarcher"
android:pathPrefix="@string/intent_import_card_from_url_path_prefix_brarcher" />
</intent-filter>
</activity>
<activity
android:name=".ScanActivity"
android:label="@string/scanCardBarcode"
android:theme="@style/AppTheme.NoActionBar"/>
android:theme="@style/AppTheme.NoActionBar" />
<activity
android:name=".BarcodeSelectorActivity"
android:label="@string/selectBarcodeTitle"
android:theme="@style/AppTheme.NoActionBar"
android:windowSoftInputMode="stateHidden"/>
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".preferences.SettingsActivity"
android:label="@string/settings"
android:theme="@style/AppTheme.NoActionBar"/>
android:theme="@style/AppTheme.NoActionBar" />
<activity
android:name=".ImportExportActivity"
android:label="@string/importExport"
android:theme="@style/AppTheme.NoActionBar"/>
android:theme="@style/AppTheme.NoActionBar" />
<activity
android:name=".CardShortcutConfigure"
android:exported="true"
android:label="@string/cardShortcut"
android:theme="@style/AppTheme.NoActionBar"
android:exported="true">
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.CREATE_SHORTCUT"/>
<category android:name="android.intent.category.DEFAULT"/>
<action android:name="android.intent.action.CREATE_SHORTCUT" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:grantUriPermissions="true"
android:authorities="${applicationId}"
android:exported="false"
android:authorities="${applicationId}">
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_provider_paths"/>
android:resource="@xml/file_provider_paths" />
</provider>
</application>
</manifest>
</manifest>

View File

@@ -2,6 +2,8 @@ package protect.card_locker;
import android.database.Cursor;
import androidx.annotation.Nullable;
public class Group
{
public final String _id;
@@ -19,4 +21,22 @@ public class Group
return new Group(_id, order);
}
@Override
public boolean equals(@Nullable Object obj) {
if (obj == null){
return false;
}
if (!(obj instanceof Group)){
return false;
}
Group anotherGroup = (Group)obj;
return _id.equals(anotherGroup._id) && order == anotherGroup.order;
}
@Override
public int hashCode(){
String combined = _id + "_" + order;
return combined.hashCode();
}
}

View File

@@ -35,8 +35,8 @@ public class LoyaltyCardCursorAdapter extends BaseCursorAdapter<LoyaltyCardCurso
boolean mDarkModeEnabled;
private Context mContext;
private CardAdapterListener mListener;
private SparseBooleanArray mSelectedItems;
private SparseBooleanArray mAnimationItemsIndex;
protected SparseBooleanArray mSelectedItems;
protected SparseBooleanArray mAnimationItemsIndex;
private boolean mReverseAllAnimations = false;
public LoyaltyCardCursorAdapter(Context inputContext, Cursor inputCursor, CardAdapterListener inputListener) {

View File

@@ -0,0 +1,223 @@
package protect.card_locker;
import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.RecyclerView;
public class ManageGroupActivity extends CatimaAppCompatActivity implements ManageGroupCursorAdapter.CardAdapterListener
{
private final DBHelper mDB = new DBHelper(this);
private ManageGroupCursorAdapter mAdapter;
private final String SAVE_INSTANCE_ADAPTER_STATE = "adapterState";
private final String SAVE_INSTANCE_CURRENT_GROUP_NAME = "currentGroupName";
protected Group mGroup = null;
private RecyclerView mCardList;
private TextView mHelpText;
private EditText mGroupNameText;
private boolean mGroupNameNotInUse;
@Override
protected void onCreate(Bundle inputSavedInstanceState)
{
super.onCreate(inputSavedInstanceState);
setContentView(R.layout.activity_manage_group);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
mHelpText = findViewById(R.id.helpText);
mCardList = findViewById(R.id.list);
FloatingActionButton saveButton = findViewById(R.id.fabSave);
mGroupNameText = findViewById(R.id.editTextGroupName);
mGroupNameText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
mGroupNameNotInUse = true;
mGroupNameText.setError(null);
String currentGroupName = mGroupNameText.getText().toString().trim();
if(currentGroupName.length() == 0){
mGroupNameText.setError(getResources().getText(R.string.group_name_is_empty));
return;
}
if (!mGroup._id.equals(currentGroupName)) {
if (mDB.getGroup(currentGroupName) != null) {
mGroupNameNotInUse = false;
mGroupNameText.setError(getResources().getText(R.string.group_name_already_in_use));
} else {
mGroupNameNotInUse = true;
}
}
}
});
Intent intent = getIntent();
String groupId = intent.getStringExtra("group");
if (groupId == null){
throw(new IllegalArgumentException("this activity expects a group loaded into it's intent"));
}
Log.d("groupId", "groupId: " + groupId);
mGroup = mDB.getGroup(groupId);
if (mGroup == null){
throw(new IllegalArgumentException("cannot load group " + groupId + " from database"));
}
mGroupNameText.setText(mGroup._id);
setTitle(getString(R.string.editGroup, mGroup._id));
mAdapter = new ManageGroupCursorAdapter(this, null, this, mGroup);
mCardList.setAdapter(mAdapter);
registerForContextMenu(mCardList);
if (inputSavedInstanceState != null) {
mAdapter.importInGroupState(integerArrayToAdapterState(inputSavedInstanceState.getIntegerArrayList(SAVE_INSTANCE_ADAPTER_STATE)));
mGroupNameText.setText(inputSavedInstanceState.getString(SAVE_INSTANCE_CURRENT_GROUP_NAME));
}
ActionBar actionBar = getSupportActionBar();
if (actionBar == null){
throw(new RuntimeException("mActionBar is not expected to be null here"));
}
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setDisplayShowHomeEnabled(true);
saveButton.setOnClickListener(v -> {
String currentGroupName = mGroupNameText.getText().toString().trim();
if(!currentGroupName.equals(mGroup._id)){
if(currentGroupName.length() == 0){
Toast.makeText(getApplicationContext(), R.string.group_name_is_empty, Toast.LENGTH_SHORT).show();
return;
}
if(!mGroupNameNotInUse) {
Toast.makeText(getApplicationContext(), R.string.group_name_already_in_use, Toast.LENGTH_SHORT).show();
return;
}
}
mAdapter.commitToDatabase();
if(!currentGroupName.equals(mGroup._id)){
mDB.updateGroup(mGroup._id, currentGroupName);
}
Toast.makeText(getApplicationContext(), R.string.group_updated, Toast.LENGTH_SHORT).show();
finish();
});
// this setText is here because content_main.xml is reused from main activity
mHelpText.setText(getResources().getText(R.string.noGiftCardsGroup));
updateLoyaltyCardList();
}
private ArrayList<Integer> adapterStateToIntegerArray(HashMap<Integer, Boolean> adapterState){
ArrayList<Integer> ret = new ArrayList<>(adapterState.size() * 2);
for (Map.Entry<Integer, Boolean> entry : adapterState.entrySet()) {
ret.add(entry.getKey());
ret.add(entry.getValue()?1:0);
}
return ret;
}
private HashMap<Integer, Boolean> integerArrayToAdapterState(ArrayList<Integer> in) {
HashMap<Integer, Boolean> ret = new HashMap<>();
if (in.size() % 2 != 0){
throw(new RuntimeException("failed restoring adapterState from integer array list"));
}
for(int i = 0;i < in.size(); i += 2){
ret.put(in.get(i), in.get(i+1) == 1);
}
return ret;
}
@Override
protected void onSaveInstanceState (@NonNull Bundle outState){
super.onSaveInstanceState(outState);
outState.putIntegerArrayList(SAVE_INSTANCE_ADAPTER_STATE, adapterStateToIntegerArray(mAdapter.exportInGroupState()));
outState.putString(SAVE_INSTANCE_CURRENT_GROUP_NAME, mGroupNameText.getText().toString());
}
private void updateLoyaltyCardList() {
mAdapter.swapCursor(mDB.getLoyaltyCardCursor());
if(mAdapter.getCountFromCursor() == 0)
{
mCardList.setVisibility(View.GONE);
mHelpText.setVisibility(View.VISIBLE);
}else {
mCardList.setVisibility(View.VISIBLE);
mHelpText.setVisibility(View.GONE);
}
}
private void leaveWithoutSaving(){
if (hasChanged()){
AlertDialog.Builder builder = new AlertDialog.Builder(ManageGroupActivity.this);
builder.setTitle(R.string.leaveWithoutSaveTitle);
builder.setMessage(R.string.leaveWithoutSaveConfirmation);
builder.setPositiveButton(R.string.confirm, (dialog, which) -> finish());
builder.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss());
AlertDialog dialog = builder.create();
dialog.show();
}else{
finish();
}
}
@Override
public void onBackPressed()
{
leaveWithoutSaving();
}
@Override
public boolean onSupportNavigateUp() {
onBackPressed();
return true;
}
private boolean hasChanged(){
return mAdapter.hasChanged() || !mGroup._id.equals(mGroupNameText.getText().toString().trim());
}
@Override
public void onRowLongClicked(int inputPosition)
{
// do nothing for now
}
@Override
public void onRowClicked(int inputPosition)
{
mAdapter.toggleSelection(inputPosition);
}
}

View File

@@ -0,0 +1,111 @@
package protect.card_locker;
import android.content.Context;
import android.database.Cursor;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ManageGroupCursorAdapter extends LoyaltyCardCursorAdapter {
private HashMap<Integer, Integer> mIndexCardMap;
private HashMap<Integer, Boolean> mInGroupOverlay;
private HashMap<Integer, Boolean> mIsLoyaltyCardInGroupCache;
private HashMap<Integer, List<Group>> mGetGroupCache;
final private Group mGroup;
final private DBHelper mDb;
public ManageGroupCursorAdapter(Context inputContext, Cursor inputCursor, CardAdapterListener inputListener, Group group){
super(inputContext, inputCursor, inputListener);
mGroup = new Group(group._id, group.order);
mInGroupOverlay = new HashMap<>();
mDb = new DBHelper(inputContext);
}
@Override
public void swapCursor(Cursor inputCursor) {
super.swapCursor(inputCursor);
mIndexCardMap = new HashMap<>();
mIsLoyaltyCardInGroupCache = new HashMap<>();
mGetGroupCache = new HashMap<>();
}
@Override
public void onBindViewHolder(LoyaltyCardListItemViewHolder inputHolder, Cursor inputCursor){
LoyaltyCard loyaltyCard = LoyaltyCard.toLoyaltyCard(inputCursor);
Boolean overlayValue = mInGroupOverlay.get(loyaltyCard.id);
if((overlayValue != null? overlayValue: isLoyaltyCardInGroup(loyaltyCard.id))) {
mAnimationItemsIndex.put(inputCursor.getPosition(), true);
mSelectedItems.put(inputCursor.getPosition(), true);
}
mIndexCardMap.put(inputCursor.getPosition(), loyaltyCard.id);
super.onBindViewHolder(inputHolder, inputCursor);
}
private List<Group> getGroups(int cardId){
List<Group> cache = mGetGroupCache.get(cardId);
if(cache != null){
return cache;
}
List<Group> groups = mDb.getLoyaltyCardGroups(cardId);
mGetGroupCache.put(cardId, groups);
return groups;
}
private boolean isLoyaltyCardInGroup(int cardId){
Boolean cache = mIsLoyaltyCardInGroupCache.get(cardId);
if(cache != null){
return cache;
}
List<Group> groups = getGroups(cardId);
if (groups.contains(mGroup)){
mIsLoyaltyCardInGroupCache.put(cardId, true);
return true;
}
mIsLoyaltyCardInGroupCache.put(cardId, false);
return false;
}
@Override
public void toggleSelection(int inputPosition){
super.toggleSelection(inputPosition);
Integer cardId = mIndexCardMap.get(inputPosition);
if (cardId == null){
throw(new RuntimeException("cardId should not be null here"));
}
Boolean overlayValue = mInGroupOverlay.get(cardId);
if (overlayValue == null){
mInGroupOverlay.put(cardId, !isLoyaltyCardInGroup(cardId));
}else{
mInGroupOverlay.remove(cardId);
}
}
public boolean hasChanged() {
return mInGroupOverlay.size() > 0;
}
public void commitToDatabase(){
for(Map.Entry<Integer, Boolean> entry: mInGroupOverlay.entrySet()){
int cardId = entry.getKey();
List<Group> groups = getGroups(cardId);
if(entry.getValue()){
groups.add(mGroup);
}else{
groups.remove(mGroup);
}
mDb.setLoyaltyCardGroups(cardId, groups);
}
}
public void importInGroupState(HashMap<Integer, Boolean> cardIdInGroupMap) {
mInGroupOverlay = new HashMap<>(cardIdInGroupMap);
}
public HashMap<Integer, Boolean> exportInGroupState(){
return new HashMap<>(mInGroupOverlay);
}
public int getCountFromCursor() {
return super.getCursor().getCount();
}
}

View File

@@ -1,6 +1,7 @@
package protect.card_locker;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.text.InputType;
@@ -9,6 +10,7 @@ import android.view.View;
import android.view.WindowManager;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
@@ -117,7 +119,16 @@ public class ManageGroupsActivity extends CatimaAppCompatActivity implements Gro
builder.setView(input);
builder.setPositiveButton(getString(R.string.ok), (dialog, which) -> {
mDb.insertGroup(input.getText().toString());
String inputString = input.getText().toString().trim();
if(inputString.length() == 0){
Toast.makeText(getApplicationContext(), R.string.group_name_is_empty, Toast.LENGTH_SHORT).show();
return;
}
if (mDb.getGroup(inputString) != null) {
Toast.makeText(getApplicationContext(), R.string.group_name_already_in_use, Toast.LENGTH_SHORT).show();
return;
}
mDb.insertGroup(inputString);
updateGroupList();
});
builder.setNegativeButton(getString(R.string.cancel), (dialog, which) -> dialog.cancel());
@@ -176,25 +187,9 @@ public class ManageGroupsActivity extends CatimaAppCompatActivity implements Gro
@Override
public void onEditButtonClicked(View view) {
final String groupName = getGroupName(view);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.enter_group_name);
final EditText input = new EditText(this);
input.setInputType(InputType.TYPE_CLASS_TEXT);
input.setText(groupName);
builder.setView(input);
builder.setPositiveButton(getString(R.string.ok), (dialog, which) -> {
String newGroupName = input.getText().toString();
mDb.updateGroup(groupName, newGroupName);
updateGroupList();
});
builder.setNegativeButton(getString(R.string.cancel), (dialog, which) -> dialog.cancel());
AlertDialog dialog = builder.create();
dialog.show();
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
input.requestFocus();
Intent intent = new Intent(this, ManageGroupActivity.class);
intent.putExtra("group", getGroupName(view));
startActivity(intent);
}
@Override

View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="protect.card_locker.ManageGroupActivity">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabSave"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
app:srcCompat="@drawable/save_24dp"
android:contentDescription="@string/save"
android:layout_margin="16dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/groups"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:tabMode="scrollable" />
</com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/storeNameField"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/storeName">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editTextGroupName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true" />
</com.google.android.material.textfield.TextInputLayout>
<include
android:id="@+id/include"
layout="@layout/content_main" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2" xmlns:tools="http://schemas.android.com/tools">
<resources xmlns:tools="http://schemas.android.com/tools" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="action_search">Search</string>
<string name="action_add">Add</string>
<plurals name="selectedCardCount">
@@ -7,6 +7,7 @@
<item quantity="other"><xliff:g>%d</xliff:g> cards selected</item>
</plurals>
<string name="noGiftCards">Click the + plus button to add a card, or import some from the ⋮ menu first.</string>
<string name="noGiftCardsGroup">You don\'t have any loyalty cards yet. Once you\'ve added some you can add them to the group here.</string>
<string name="noMatchingGiftCards">Didn\'t find anything. Try changing your search.</string>
<string name="storeName">Name</string>
<string name="note">Note</string>
@@ -123,12 +124,16 @@
<string name="exportSuccessful">Card data exported</string>
<string name="enter_group_name">Enter group name</string>
<string name="groups">Groups</string>
<string name="group_edit">Edit Group</string>
<string name="noGroups">Click the + plus button to add groups for categorization first.</string>
<string name="noGroupCards">This group does not contain any cards</string>
<plurals name="groupCardCount">
<item quantity="one"><xliff:g>%d</xliff:g> card</item>
<item quantity="other"><xliff:g>%d</xliff:g> cards</item>
</plurals>
<string name="group_name_already_in_use">Group name already in use</string>
<string name="group_name_is_empty">Group name cannot be empty</string>
<string name="group_updated">Group updated</string>
<string name="all">All</string>
<string name="deleteConfirmationGroup">Delete group?</string>
<string name="failedOpeningFileManager">Install a file manager first.</string>
@@ -139,6 +144,7 @@
<string name="addManually">Manually enter card ID</string>
<string name="addFromImage">Select image from gallery</string>
<string name="groupsList">Groups: <xliff:g>%s</xliff:g></string>
<string name="editGroup">Editing Group: <xliff:g>%s</xliff:g></string>
<string name="expiryStateSentence">Expires: <xliff:g>%s</xliff:g></string>
<string name="expiryStateSentenceExpired">Expired: <xliff:g>%s</xliff:g></string>
<string name="balanceSentence">Balance: <xliff:g>%s</xliff:g></string>