Rework OTP editing (WIP)

This commit is contained in:
MrLetsplay 2023-09-20 19:53:34 +02:00
parent 041513fb30
commit 82cc4760cf
Signed by: mr
SSH Key Fingerprint: SHA256:92jBH80vpXyaZHjaIl47pjRq+Yt7XGTArqQg1V7hSqg
18 changed files with 356 additions and 129 deletions

View File

@ -167,6 +167,13 @@ public class MainActivity extends BaseActivity {
return true;
}
if(fragment instanceof GroupFragment) {
GroupFragment frag = (GroupFragment) fragment;
getMenuInflater().inflate(frag.isEditing() ? R.menu.menu_otps_edit : R.menu.menu_otps, menu);
if(frag.isEditing() && frag.hasSelectedMultipleItems()) menu.removeItem(R.id.action_edit_group);
return true;
}
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@ -197,6 +204,14 @@ public class MainActivity extends BaseActivity {
}
}
if(fragment instanceof GroupFragment) {
GroupFragment groupFragment = (GroupFragment) fragment;
if(groupFragment.isEditing()) {
groupFragment.finishEditing();
return;
}
}
if(!(fragment instanceof HomeFragment)) {
NavigationUtil.navigate(this, HomeFragment.class, null);
}
@ -256,7 +271,7 @@ public class MainActivity extends BaseActivity {
if(!(fragment instanceof GroupFragment)) return;
((GroupFragment) fragment).addOTP(data);
}, () -> inputCode(), false);
}, false);
}
private void showHOTPDialog() {
@ -265,7 +280,7 @@ public class MainActivity extends BaseActivity {
if(!(fragment instanceof GroupFragment)) return;
((GroupFragment) fragment).addOTP(data);
}, () -> inputCode(), false);
}, false);
}
public void addGroup(MenuItem item) {
@ -289,6 +304,41 @@ public class MainActivity extends BaseActivity {
}
}
public void addOTP(MenuItem item) {
Fragment frag = NavigationUtil.getCurrentFragment(this);
if(frag instanceof GroupFragment) {
((GroupFragment) frag).addOTP();
}
}
public void viewOTP(MenuItem item) {
Fragment frag = NavigationUtil.getCurrentFragment(this);
if(frag instanceof GroupFragment) {
((GroupFragment) frag).viewOTP();
}
}
public void editOTP(MenuItem item) {
Fragment frag = NavigationUtil.getCurrentFragment(this);
if(frag instanceof GroupFragment) {
((GroupFragment) frag).editOTP();
}
}
public void moveOTP(MenuItem item) {
Fragment frag = NavigationUtil.getCurrentFragment(this);
if(frag instanceof GroupFragment) {
((GroupFragment) frag).moveOTP();
}
}
public void deleteOTP(MenuItem item) {
Fragment frag = NavigationUtil.getCurrentFragment(this);
if(frag instanceof GroupFragment) {
((GroupFragment) frag).deleteOTP();
}
}
@Override
protected void onPause() {
super.onPause();

View File

@ -5,6 +5,7 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
@ -16,6 +17,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.cringe_studios.cringe_authenticator.R;
import com.cringe_studios.cringe_authenticator.crypto.CryptoException;
import com.cringe_studios.cringe_authenticator.databinding.FragmentGroupBinding;
import com.cringe_studios.cringe_authenticator.grouplist.GroupListItem;
import com.cringe_studios.cringe_authenticator.model.OTPData;
import com.cringe_studios.cringe_authenticator.otplist.OTPListAdapter;
import com.cringe_studios.cringe_authenticator.otplist.OTPListItem;
@ -63,8 +65,7 @@ public class GroupFragment extends NamedFragment {
FabUtil.showFabs(requireActivity());
otpListAdapter = new OTPListAdapter(requireContext(), data -> showOTPDialog(data));
otpListAdapter = new OTPListAdapter(requireContext(), binding.itemList);
binding.itemList.setAdapter(otpListAdapter);
loadOTPs();
@ -80,52 +81,6 @@ public class GroupFragment extends NamedFragment {
return binding.getRoot();
}
private void showOTPDialog(OTPData data) {
new StyledDialogBuilder(requireContext())
.setTitle(R.string.edit_otp_title)
.setItems(R.array.view_edit_move_delete, (dialog, which) -> {
switch(which) {
case 0:
DialogUtil.showViewCodeDialog(getLayoutInflater(), data, () -> showOTPDialog(data));
break;
case 1:
DialogUtil.showEditCodeDialog(getLayoutInflater(), data, newData -> {
otpListAdapter.replace(data, newData);
saveOTPs();
}, () -> showOTPDialog(data));
break;
case 2:
DialogUtil.showChooseGroupDialog(requireContext(), group -> {
OTPDatabase.promptLoadDatabase(requireActivity(), () -> {
try {
OTPDatabase.getLoadedDatabase().addOTP(group, data);
OTPDatabase.saveDatabase(requireContext(), SettingsUtil.getCryptoParameters(requireContext()));
otpListAdapter.remove(data);
saveOTPs();
} catch (OTPDatabaseException | CryptoException e) {
DialogUtil.showErrorDialog(requireContext(), e.toString());
}
}, null);
saveOTPs();
}, null);
break;
case 3:
new StyledDialogBuilder(requireContext())
.setTitle(R.string.otp_delete_title)
.setMessage(R.string.otp_delete_message)
.setPositiveButton(R.string.yes, (d, w) -> {
otpListAdapter.remove(data);
saveOTPs();
})
.setNegativeButton(R.string.no, (d, w) -> {})
.show();
break;
}
})
.setNegativeButton(R.string.cancel, (dialog, which) -> {})
.show();
}
private void saveOTPs() {
OTPDatabase.promptLoadDatabase(requireActivity(), () -> {
try {
@ -178,6 +133,89 @@ public class GroupFragment extends NamedFragment {
}
}
public void addOTP() {
// TODO
}
public void viewOTP() {
if(!otpListAdapter.isEditing()) return;
List<OTPListItem> items = otpListAdapter.getSelectedCodes();
if(items.size() != 1) return;
OTPData data = items.get(0).getOTPData();
DialogUtil.showViewCodeDialog(getLayoutInflater(), data);
}
public void editOTP() {
if(!otpListAdapter.isEditing()) return;
List<OTPListItem> items = otpListAdapter.getSelectedCodes();
if(items.size() != 1) return;
OTPData data = items.get(0).getOTPData();
DialogUtil.showEditCodeDialog(getLayoutInflater(), data, newData -> {
otpListAdapter.replace(data, newData);
saveOTPs();
otpListAdapter.finishEditing();
});
}
public void moveOTP() {
if(!otpListAdapter.isEditing()) return;
List<OTPListItem> items = otpListAdapter.getSelectedCodes();
DialogUtil.showChooseGroupDialog(requireContext(), group -> {
OTPDatabase.promptLoadDatabase(requireActivity(), () -> {
try {
for(OTPListItem item : items) {
OTPData data = item.getOTPData();
OTPDatabase.getLoadedDatabase().addOTP(group, data);
OTPDatabase.saveDatabase(requireContext(), SettingsUtil.getCryptoParameters(requireContext()));
otpListAdapter.remove(data);
}
saveOTPs();
} catch (OTPDatabaseException | CryptoException e) {
DialogUtil.showErrorDialog(requireContext(), e.toString());
}
}, null);
saveOTPs();
}, null);
}
public void deleteOTP() {
if(!otpListAdapter.isEditing()) return;
List<OTPListItem> items = otpListAdapter.getSelectedCodes();
new StyledDialogBuilder(requireContext())
.setTitle(R.string.otp_delete_title)
.setMessage(R.string.otp_delete_message)
.setPositiveButton(R.string.yes, (d, w) -> {
for(OTPListItem item : items) {
otpListAdapter.remove(item.getOTPData());
}
saveOTPs();
})
.setNegativeButton(R.string.no, (d, w) -> {})
.show();
}
public boolean isEditing() {
return otpListAdapter.isEditing();
}
public void finishEditing() {
otpListAdapter.finishEditing();
}
public boolean hasSelectedMultipleItems() {
return otpListAdapter.getSelectedCodes().size() > 1;
}
@Override
public void onDestroyView() {
super.onDestroyView();

View File

@ -54,31 +54,6 @@ public class MenuFragment extends NamedFragment {
return binding.getRoot();
}
private void showGroupDialog(String group) {
new StyledDialogBuilder(requireContext())
.setTitle(R.string.edit_group_title)
.setItems(R.array.rename_delete, (dialog, which) -> {
switch(which) {
case 0:
DialogUtil.showCreateGroupDialog(getLayoutInflater(), SettingsUtil.getGroupName(requireContext(), group), newName -> {
renameGroup(group, newName);
}, null);
break;
case 1:
new StyledDialogBuilder(requireContext())
.setTitle(R.string.group_delete_title)
.setMessage(R.string.group_delete_message)
.setPositiveButton(R.string.yes, (d, w) -> removeGroup(group))
.setNegativeButton(R.string.no, (d, w) -> {})
.show();
break;
}
})
.setNegativeButton(R.string.cancel, (dialog, which) -> {})
.show();
}
private void loadGroups() {
List<String> items = SettingsUtil.getGroups(requireContext());
@ -114,7 +89,7 @@ public class MenuFragment extends NamedFragment {
new StyledDialogBuilder(requireContext())
.setTitle(R.string.group_delete_title)
.setMessage("Delete selected groups?")
.setMessage(R.string.group_delete_message)
.setPositiveButton(R.string.yes, (d, w) -> {
for(GroupListItem item : groupListAdapter.getSelectedGroups()) {
removeGroup(item.getGroupId());

View File

@ -68,6 +68,7 @@ public class GroupListAdapter extends RecyclerView.Adapter<GroupListItem> {
String group = items.get(position);
holder.setGroupId(group);
holder.setSelected(false);
holder.getBinding().button.setText(SettingsUtil.getGroupName(context, group));
@ -80,10 +81,6 @@ public class GroupListAdapter extends RecyclerView.Adapter<GroupListItem> {
((BaseActivity) context).invalidateMenu();
}
});
/*holder.getBinding().button.setOnLongClickListener(view -> {
showMenuCallback.accept(group);
return true;
});*/
holder.getBinding().button.setOnLongClickListener(view -> {
if(editing) return true;

View File

@ -40,7 +40,7 @@ public class GroupListItem extends RecyclerView.ViewHolder {
this.selected = selected;
if(selected) {
binding.menuItemBackground.setBackground(new ColorDrawable(0xFFFF00FF));
binding.menuItemBackground.setBackground(new ColorDrawable(binding.getRoot().getContext().getResources().getColor(R.color.selected_highlight)));
}else {
binding.menuItemBackground.setBackground(null);
}

View File

@ -11,8 +11,10 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.util.Consumer;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import com.cringe_studios.cringe_authenticator.BaseActivity;
import com.cringe_studios.cringe_authenticator.R;
import com.cringe_studios.cringe_authenticator.databinding.OtpCodeBinding;
import com.cringe_studios.cringe_authenticator.model.OTPData;
@ -21,26 +23,29 @@ import com.cringe_studios.cringe_authenticator_library.OTPException;
import com.cringe_studios.cringe_authenticator_library.OTPType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class OTPListAdapter extends RecyclerView.Adapter<OTPListItem> {
private Context context;
private RecyclerView recyclerView;
private LayoutInflater inflater;
private List<OTPData> items;
private Handler handler;
private Consumer<OTPData> showMenuCallback;
private boolean editing;
public OTPListAdapter(Context context, Consumer<OTPData> showMenuCallback) {
public OTPListAdapter(Context context, RecyclerView recyclerView) {
this.context = context;
this.recyclerView = recyclerView;
this.inflater = LayoutInflater.from(context);
this.items = new ArrayList<>();
this.handler = new Handler(Looper.getMainLooper());
this.showMenuCallback = showMenuCallback;
}
@NonNull
@ -55,32 +60,44 @@ public class OTPListAdapter extends RecyclerView.Adapter<OTPListItem> {
OTPData data = items.get(position);
holder.setOTPData(data);
holder.setSelected(false);
holder.getBinding().label.setText(String.format("%s%s", data.getIssuer() == null || data.getIssuer().isEmpty() ? "" : data.getIssuer() + ": ", data.getName()));
holder.getBinding().progress.setVisibility(data.getType() == OTPType.TOTP ? View.VISIBLE : View.INVISIBLE);
holder.getBinding().getRoot().setOnClickListener(view -> {
if(!view.isClickable()) return;
if(!editing) {
if (!view.isClickable()) return;
if(data.getType() != OTPType.HOTP) return;
if (data.getType() != OTPType.HOTP) return;
// Click delay for HOTP
view.setClickable(false);
data.incrementCounter();
// Click delay for HOTP
view.setClickable(false);
data.incrementCounter();
try {
holder.getBinding().otpCode.setText(OTPListItem.formatCode(data.getPin()));
}catch(OTPException e) {
DialogUtil.showErrorDialog(context, context.getString(R.string.otp_add_error, e.getMessage() != null ? e.getMessage() : e.toString()));
return;
try {
holder.getBinding().otpCode.setText(OTPListItem.formatCode(data.getPin()));
} catch (OTPException e) {
DialogUtil.showErrorDialog(context, context.getString(R.string.otp_add_error, e.getMessage() != null ? e.getMessage() : e.toString()));
return;
}
Toast.makeText(view.getContext(), R.string.hotp_generated_new_code, Toast.LENGTH_SHORT).show();
handler.postDelayed(() -> view.setClickable(true), 5000);
}else {
holder.setSelected(!holder.isSelected());
if(getSelectedCodes().isEmpty()) editing = false;
((BaseActivity) context).invalidateMenu();
}
Toast.makeText(view.getContext(), R.string.hotp_generated_new_code, Toast.LENGTH_SHORT).show();
handler.postDelayed(() -> view.setClickable(true), 5000);
});
holder.getBinding().getRoot().setOnLongClickListener(view -> {
showMenuCallback.accept(holder.getOTPData());
if(editing) return true;
holder.setSelected(true);
editing = true;
((BaseActivity) context).invalidateMenu();
return true;
});
}
@ -113,4 +130,59 @@ public class OTPListAdapter extends RecyclerView.Adapter<OTPListItem> {
notifyItemRemoved(index);
}
public boolean isEditing() {
return editing;
}
public void finishEditing() {
if(!editing) return;
editing = false;
for(OTPListItem item : getSelectedCodes()) {
item.setSelected(false);
}
((BaseActivity) context).invalidateMenu();
}
public List<OTPListItem> getSelectedCodes() {
if(!editing) return Collections.emptyList();
List<OTPListItem> selected = new ArrayList<>();
for(int i = 0; i < items.size(); i++) {
OTPListItem vh = (OTPListItem) recyclerView.findViewHolderForAdapterPosition(i);
if(vh == null) continue;
if(vh.isSelected()) selected.add(vh);
}
return selected;
}
private void attachTouchHelper(RecyclerView view) {
new ItemTouchHelper(new OTPListAdapter.TouchHelperCallback()).attachToRecyclerView(view);
}
private class TouchHelperCallback extends ItemTouchHelper.Callback {
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
return makeMovementFlags(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0);
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
Collections.swap(items, viewHolder.getAdapterPosition(), target.getAdapterPosition());
notifyItemMoved(viewHolder.getAdapterPosition(), target.getAdapterPosition());
//saveGroups.run();
return true;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {}
@Override
public boolean isLongPressDragEnabled() {
return editing;
}
}
}

View File

@ -1,8 +1,11 @@
package com.cringe_studios.cringe_authenticator.otplist;
import android.graphics.drawable.ColorDrawable;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.cringe_studios.cringe_authenticator.R;
import com.cringe_studios.cringe_authenticator.databinding.OtpCodeBinding;
import com.cringe_studios.cringe_authenticator.model.OTPData;
@ -12,6 +15,8 @@ public class OTPListItem extends RecyclerView.ViewHolder {
private OTPData otpData;
private boolean selected;
public OTPListItem(OtpCodeBinding binding) {
super(binding.getRoot());
this.binding = binding;
@ -43,4 +48,18 @@ public class OTPListItem extends RecyclerView.ViewHolder {
return b.toString();
}
public void setSelected(boolean selected) {
this.selected = selected;
if(selected) {
binding.otpCodeBackground.setBackground(new ColorDrawable(binding.getRoot().getContext().getResources().getColor(R.color.selected_highlight)));
}else {
binding.otpCodeBackground.setBackground(null);
}
}
public boolean isSelected() {
return selected;
}
}

View File

@ -28,12 +28,11 @@ public class DialogUtil {
private static final Integer[] DIGITS = new Integer[]{6, 7, 8, 9, 10, 11, 12};
private static void showCodeDialog(Context context, View view, DialogCallback ok, Runnable back) {
private static void showCodeDialog(Context context, View view, DialogCallback ok) {
AlertDialog dialog = new StyledDialogBuilder(context)
.setTitle(R.string.code_input_title)
.setView(view)
.setPositiveButton(R.string.ok, (btnView, which) -> {})
.setNeutralButton(R.string.back, (btnView, which) -> back.run())
.setNegativeButton(R.string.cancel, (btnView, which) -> {})
.create();
@ -60,7 +59,7 @@ public class DialogUtil {
showErrorDialog(context, errorMessage, null);
}
public static void showTOTPDialog(LayoutInflater inflater, OTPData initialData, Consumer<OTPData> callback, Runnable back, boolean view) {
public static void showTOTPDialog(LayoutInflater inflater, OTPData initialData, Consumer<OTPData> callback, boolean view) {
Context context = inflater.getContext();
DialogInputCodeTotpBinding binding = DialogInputCodeTotpBinding.inflate(inflater);
@ -122,10 +121,10 @@ public class DialogUtil {
showErrorDialog(context, context.getString(R.string.input_code_invalid_number));
return false;
}
}, back);
});
}
public static void showHOTPDialog(LayoutInflater inflater, OTPData initialData, Consumer<OTPData> callback, Runnable back, boolean view) {
public static void showHOTPDialog(LayoutInflater inflater, OTPData initialData, Consumer<OTPData> callback, boolean view) {
Context context = inflater.getContext();
DialogInputCodeHotpBinding binding = DialogInputCodeHotpBinding.inflate(inflater);
@ -187,21 +186,21 @@ public class DialogUtil {
showErrorDialog(context, context.getString(R.string.input_code_invalid_number));
return false;
}
}, back);
});
}
public static void showViewCodeDialog(LayoutInflater inflater, @NonNull OTPData initialData, Runnable back) {
public static void showViewCodeDialog(LayoutInflater inflater, @NonNull OTPData initialData) {
// TODO: use better dialogs
switch(initialData.getType()) {
case HOTP: showHOTPDialog(inflater, initialData, d -> {}, back, true); break;
case TOTP: showTOTPDialog(inflater, initialData, d -> {}, back, true); break;
case HOTP: showHOTPDialog(inflater, initialData, d -> {}, true); break;
case TOTP: showTOTPDialog(inflater, initialData, d -> {}, true); break;
}
}
public static void showEditCodeDialog(LayoutInflater inflater, @NonNull OTPData initialData, Consumer<OTPData> callback, Runnable back) {
public static void showEditCodeDialog(LayoutInflater inflater, @NonNull OTPData initialData, Consumer<OTPData> callback) {
switch(initialData.getType()) {
case HOTP: showHOTPDialog(inflater, initialData, callback, back, false); break;
case TOTP: showTOTPDialog(inflater, initialData, callback, back, false); break;
case HOTP: showHOTPDialog(inflater, initialData, callback, false); break;
case TOTP: showTOTPDialog(inflater, initialData, callback, false); break;
}
}

View File

@ -1,4 +1,4 @@
<vector android:height="24dp" android:tint="#000000"
<vector android:height="24dp" android:tint="?android:attr/textColor"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M10,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h5v2h2L12,1h-2v2zM10,18L5,18l5,-6v6zM19,3h-5v2h5v13l-5,-6v9h5c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2z"/>

View File

@ -8,8 +8,7 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/itemList"
@ -23,7 +22,7 @@
<Space
android:layout_width="0dp"
android:layout_height="65dp"
android:layout_height="76dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/itemList" />

View File

@ -35,5 +35,12 @@
android:orientation="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<Space
android:layout_width="0dp"
android:layout_height="76dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/menu_items" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -7,9 +7,7 @@
android:orientation="vertical"
android:padding="5dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:clickable="true"
android:focusable="true" >
android:paddingEnd="16dp" >
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/button"

View File

@ -2,17 +2,21 @@
<LinearLayout 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:id="@+id/otp_code_background"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@drawable/button_themed"
android:orientation="vertical"
android:padding="5dp">
android:padding="5dp"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
android:orientation="horizontal"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:background="@drawable/button_themed">
<ImageView
android:id="@+id/imageView5"

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_add_otp"
android:orderInCategory="100"
android:icon="@drawable/baseline_add_24"
android:title="@string/action_new_group"
android:onClick="addOTP"
app:showAsAction="always" />
<item
android:id="@+id/action_settings"
android:orderInCategory="100"
android:title="@string/action_settings"
app:showAsAction="never"
android:onClick="openSettings" />
<item
android:id="@+id/action_about"
android:orderInCategory="100"
android:title="@string/action_about"
app:showAsAction="never"
android:onClick="openAbout" />
</menu>

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_view_otp"
android:orderInCategory="100"
android:icon="@drawable/baseline_compare_24"
android:title="View OTP"
android:onClick="viewOTP"
app:showAsAction="always" />
<item
android:id="@+id/action_edit_otp"
android:orderInCategory="100"
android:icon="@drawable/baseline_edit_24"
android:title="Edit OTP"
android:onClick="editOTP"
app:showAsAction="always" />
<item
android:id="@+id/action_move_otp"
android:orderInCategory="100"
android:icon="@drawable/baseline_qr_code_scanner_24"
android:title="Move OTP"
android:onClick="moveOTP"
app:showAsAction="always" />
<item
android:id="@+id/action_delete_otp"
android:orderInCategory="100"
android:icon="@drawable/baseline_delete_24"
android:title="Delete OTP"
android:onClick="deleteOTP"
app:showAsAction="always" />
<item
android:id="@+id/action_settings"
android:orderInCategory="100"
android:title="@string/action_settings"
app:showAsAction="never"
android:onClick="openSettings" />
<item
android:id="@+id/action_about"
android:orderInCategory="100"
android:title="@string/action_about"
app:showAsAction="never"
android:onClick="openAbout" />
</menu>

View File

@ -21,8 +21,8 @@
<string name="qr_scanner_failed">Scannen fehlgeschlagen: %s</string>
<string name="intro_video_failed">Abspielen des Videos fehlgeschlagen</string>
<string name="edit_otp_title">OTP bearbeiten</string>
<string name="group_delete_title">Gruppe(n) löschen</string>
<string name="group_delete_message">Willst du die ausgewählte(n) Gruppe(n) löschen?\n\nHinweis: Dadurch werden alle darin enthaltenen OTPs gelöscht!</string>
<string name="group_delete_title">Gruppen löschen</string>
<string name="group_delete_message">Willst du die ausgewählten Gruppen löschen?\n\nHinweis: Dadurch werden alle darin enthaltenen OTPs gelöscht!</string>
<string name="hotp_generated_new_code">Neuen Code generiert</string>
<string name="uri_handler_failed_title">Hinzufügen des Codes fehlgeschlagen</string>
<string name="code_input_title">Code eingeben</string>
@ -30,7 +30,7 @@
<string name="input_code_invalid_number">Ungültige Zahl</string>
<string name="back">Zurück</string>
<string name="otp_delete_title">OTP löschen</string>
<string name="otp_delete_message">Willst du das OTP löschen?</string>
<string name="otp_delete_message">Willst du die ausgewählten OTPs löschen?</string>
<string name="edit_group_title">Gruppe bearbeiten</string>
<string name="settings_enable_intro_video">Intro-Video zeigen</string>
<string name="settings_biometric_lock">Biometrische Authentifizierung aktivieren</string>

View File

@ -16,4 +16,6 @@
<color name="color_yellow">#FFE500</color>
<color name="color_turquoise">#00FFF7</color>
<color name="color_green">#00FF0A</color>
<color name="selected_highlight">#33008BFF</color>
</resources>

View File

@ -21,16 +21,16 @@
<string name="qr_scanner_failed">Scan failed: %s</string>
<string name="intro_video_failed">Failed to play video</string>
<string name="edit_otp_title">Edit OTP</string>
<string name="group_delete_title">Delete Group(s)</string>
<string name="group_delete_message">Do you want to delete the group(s)?\n\nNote: This will delete all of the contained OTPs!</string>
<string name="group_delete_title">Delete Groups</string>
<string name="group_delete_message">Do you want to delete the groups?\n\nNote: This will delete all of the contained OTPs!</string>
<string name="hotp_generated_new_code">Generated new code</string>
<string name="uri_handler_failed_title">Failed to add code</string>
<string name="code_input_title">Input Code</string>
<string name="failed_title">Action failed</string>
<string name="input_code_invalid_number">Invalid number entered</string>
<string name="back">Back</string>
<string name="otp_delete_title">Delete OTP</string>
<string name="otp_delete_message">Do you want to delete the OTP?</string>
<string name="otp_delete_title">Delete OTP(s)</string>
<string name="otp_delete_message">Do you want to delete the selected OTP(s)?</string>
<string name="edit_group_title">Edit Group</string>
<string name="settings_enable_intro_video">Enable intro video</string>
<string name="settings_biometric_lock">Require biometric unlock</string>