diff options
7 files changed, 279 insertions, 22 deletions
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainActivity.java index 4d76c7aea..fc5992b68 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainActivity.java @@ -1,6 +1,7 @@ package it.niedermann.nextcloud.deck.ui.main; import static java.util.Collections.emptyList; +import static it.niedermann.nextcloud.deck.util.MimeTypeUtil.TEXT_PLAIN; import android.content.Context; import android.content.DialogInterface; @@ -86,6 +87,7 @@ import it.niedermann.nextcloud.deck.ui.board.ArchiveBoardListener; import it.niedermann.nextcloud.deck.ui.board.DeleteBoardListener; import it.niedermann.nextcloud.deck.ui.board.edit.EditBoardDialogFragment; import it.niedermann.nextcloud.deck.ui.board.edit.EditBoardListener; +import it.niedermann.nextcloud.deck.ui.card.CardActionListener; import it.niedermann.nextcloud.deck.ui.card.CardAdapter; import it.niedermann.nextcloud.deck.ui.card.CreateCardListener; import it.niedermann.nextcloud.deck.ui.card.NewCardDialog; @@ -95,6 +97,8 @@ import it.niedermann.nextcloud.deck.ui.filter.FilterDialogFragment; import it.niedermann.nextcloud.deck.ui.filter.FilterViewModel; import it.niedermann.nextcloud.deck.ui.main.search.SearchAdapter; import it.niedermann.nextcloud.deck.ui.main.search.SearchResults; +import it.niedermann.nextcloud.deck.ui.movecard.MoveCardDialogFragment; +import it.niedermann.nextcloud.deck.ui.movecard.MoveCardListener; import it.niedermann.nextcloud.deck.ui.settings.PreferencesViewModel; import it.niedermann.nextcloud.deck.ui.stack.DeleteStackDialogFragment; import it.niedermann.nextcloud.deck.ui.stack.DeleteStackListener; @@ -105,10 +109,19 @@ import it.niedermann.nextcloud.deck.ui.stack.StackAdapter; import it.niedermann.nextcloud.deck.ui.stack.StackFragment; import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; import it.niedermann.nextcloud.deck.ui.theme.ThemedSnackbar; +import it.niedermann.nextcloud.deck.util.CardUtil; import it.niedermann.nextcloud.deck.util.CustomAppGlideModule; import it.niedermann.nextcloud.deck.util.OnTextChangedWatcher; -public class MainActivity extends AppCompatActivity implements DeleteStackListener, EditStackListener, DeleteBoardListener, EditBoardListener, ArchiveBoardListener, OnScrollListener, CreateCardListener { +public class MainActivity extends AppCompatActivity implements DeleteStackListener, + EditStackListener, + DeleteBoardListener, + EditBoardListener, + ArchiveBoardListener, + OnScrollListener, + CreateCardListener, + CardActionListener, + MoveCardListener { protected ActivityMainBinding binding; private NavHeaderMainBinding headerBinding; @@ -184,7 +197,7 @@ public class MainActivity extends AppCompatActivity implements DeleteStackListen navigationHandler = new MainActivityNavigationHandler(this, binding.drawerLayout, mainViewModel::saveCurrentBoardId); binding.navigationView.setNavigationItemSelectedListener(navigationHandler); - searchAdapter = new SearchAdapter(); + searchAdapter = new SearchAdapter(this); binding.searchResults.setAdapter(searchAdapter); binding.searchView.getEditText().addTextChangedListener(new OnTextChangedWatcher(value -> { if (TextUtils.isEmpty(value)) { @@ -432,7 +445,7 @@ public class MainActivity extends AppCompatActivity implements DeleteStackListen .filter(() -> binding.searchView.isShowing()) .flatMap(term -> new ReactiveLiveData<>(mainViewModel.searchCards(account.getId(), currentBoard.getLocalId(), term, Integer.MAX_VALUE)) .combineWith(() -> new MutableLiveData<>(term))) - .map(result -> new SearchResults(account, currentBoard.getBoard(), result.first, result.second)); + .map(result -> new SearchResults(account, currentBoard, result.first, result.second)); searchResults$.observe(this, searchResultsObserver); } } @@ -936,6 +949,100 @@ public class MainActivity extends AppCompatActivity implements DeleteStackListen .show(); } + @Override + public void onArchive(@NonNull FullCard fullCard) { + mainViewModel.archiveCard(fullCard, new IResponseCallback<>() { + @Override + public void onResponse(FullCard response) { + DeckLog.info("Successfully archived", Card.class.getSimpleName(), fullCard.getCard().getTitle()); + } + + @Override + public void onError(Throwable throwable) { + IResponseCallback.super.onError(throwable); + showExceptionDialog(throwable, fullCard.getAccountId()); + } + }); + } + + @Override + public void onDelete(@NonNull FullCard fullCard) { + mainViewModel.deleteCard(fullCard.getCard(), new IResponseCallback<>() { + @Override + public void onResponse(Void response) { + DeckLog.info("Successfully deleted card", fullCard.getCard().getTitle()); + } + + @Override + public void onError(Throwable throwable) { + if (SyncRepository.isNoOnVoidError(throwable)) { + IResponseCallback.super.onError(throwable); + showExceptionDialog(throwable, fullCard.getAccountId()); + } + } + }); + } + + @Override + public void onAssignCurrentUser(@NonNull FullCard fullCard) { + mainViewModel.assignUserToCard(fullCard); + } + + @Override + public void onUnassignCurrentUser(@NonNull FullCard fullCard) { + mainViewModel.unassignUserFromCard(fullCard); + } + + @Override + public void onMove(@NonNull FullBoard fullBoard, @NonNull FullCard fullCard) { + DeckLog.verbose("[Move card] Launch move dialog for " + Card.class.getSimpleName() + " \"" + fullCard.getCard().getTitle() + "\" (#" + fullCard.getLocalId() + ") from " + Stack.class.getSimpleName() + " #" + fullCard.getCard().getStackId()); + MoveCardDialogFragment + .newInstance(fullCard.getAccountId(), fullBoard.getBoard().getLocalId(), fullCard.getCard().getTitle(), fullCard.getLocalId(), CardUtil.cardHasCommentsOrAttachments(fullCard)) + .show(getSupportFragmentManager(), MoveCardDialogFragment.class.getSimpleName()); + } + + @Override + public void onShareLink(@NonNull FullBoard fullBoard, @NonNull FullCard fullCard) { + mainViewModel.getAccountFuture(fullCard.getAccountId()).thenAcceptAsync(account -> { + final int shareLinkRes = account.getServerDeckVersionAsObject().getShareLinkResource(); + final var shareIntent = new Intent() + .setAction(Intent.ACTION_SEND) + .setType(TEXT_PLAIN) + .putExtra(Intent.EXTRA_SUBJECT, fullCard.getCard().getTitle()) + .putExtra(Intent.EXTRA_TITLE, fullCard.getCard().getTitle()) + .putExtra(Intent.EXTRA_TEXT, account.getUrl() + getString(shareLinkRes, fullBoard.getBoard().getId(), fullCard.getCard().getId())); + startActivity(Intent.createChooser(shareIntent, fullCard.getCard().getTitle())); + }, ContextCompat.getMainExecutor(this)); + } + + @Override + public void onShareContent(@NonNull FullCard fullCard) { + final var shareIntent = new Intent() + .setAction(Intent.ACTION_SEND) + .setType(TEXT_PLAIN) + .putExtra(Intent.EXTRA_SUBJECT, fullCard.getCard().getTitle()) + .putExtra(Intent.EXTRA_TITLE, fullCard.getCard().getTitle()) + .putExtra(Intent.EXTRA_TEXT, CardUtil.getCardContentAsString(this, fullCard)); + startActivity(Intent.createChooser(shareIntent, fullCard.getCard().getTitle())); + } + + @Override + public void move(long originAccountId, long originCardLocalId, long targetAccountId, long targetBoardLocalId, long targetStackLocalId) { + mainViewModel.moveCard(originAccountId, originCardLocalId, targetAccountId, targetBoardLocalId, targetStackLocalId, new IResponseCallback<>() { + @Override + public void onResponse(Void response) { + DeckLog.log("Moved", Card.class.getSimpleName(), originCardLocalId, "to", Stack.class.getSimpleName(), targetStackLocalId); + } + + @Override + public void onError(Throwable throwable) { + IResponseCallback.super.onError(throwable); + if (SyncRepository.isNoOnVoidError(throwable)) { + ExceptionDialogFragment.newInstance(throwable, null).show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName()); + } + } + }); + } /** * Displays a {@link ThemedSnackbar} for an exception of a failed sync, but only if the cause wasn't maintenance mode (this should be handled by a TextView instead of a snackbar). diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainViewModel.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainViewModel.java index e9afe5419..8fe3ce0a5 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainViewModel.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/MainViewModel.java @@ -24,7 +24,9 @@ import it.niedermann.nextcloud.deck.DeckLog; import it.niedermann.nextcloud.deck.model.Account; import it.niedermann.nextcloud.deck.model.Attachment; import it.niedermann.nextcloud.deck.model.Board; +import it.niedermann.nextcloud.deck.model.Card; import it.niedermann.nextcloud.deck.model.Stack; +import it.niedermann.nextcloud.deck.model.User; import it.niedermann.nextcloud.deck.model.full.FullBoard; import it.niedermann.nextcloud.deck.model.full.FullCard; import it.niedermann.nextcloud.deck.model.full.FullStack; @@ -55,7 +57,7 @@ public class MainViewModel extends BaseViewModel { } } - private Exception getInvalidSyncManagerException() { + private IllegalStateException getInvalidSyncManagerException() { return new IllegalStateException("SyncManager is null"); } @@ -258,4 +260,55 @@ public class MainViewModel extends BaseViewModel { public LiveData<Long> getCurrentStackId$(long accountId, long boardId) { return baseRepository.getCurrentStackId$(accountId, boardId); } + + public CompletableFuture<Account> getAccountFuture(long accountId) { + return supplyAsync(() -> baseRepository.readAccountDirectly(accountId)); + } + + + public void archiveCard(@NonNull FullCard card, @NonNull IResponseCallback<FullCard> callback) { + if (syncRepository == null) { + callback.onError(getInvalidSyncManagerException()); + } else { + syncRepository.archiveCard(card, callback); + } + } + + public void deleteCard(@NonNull Card card, @NonNull IResponseCallback<Void> callback) { + if (syncRepository == null) { + callback.onError(getInvalidSyncManagerException()); + } else { + syncRepository.deleteCard(card, callback); + } + } + + public void assignUserToCard(@NonNull FullCard fullCard) { + if (syncRepository == null) { + throw getInvalidSyncManagerException(); + } else { + final var syncRepositoryRef = syncRepository; + getAccountFuture(fullCard.getAccountId()).thenAcceptAsync(account -> syncRepositoryRef.assignUserToCard(getUserByUidDirectly(fullCard.getCard().getAccountId(), account.getUserName()), fullCard.getCard())); + } + } + + public void unassignUserFromCard(@NonNull FullCard fullCard) { + if (syncRepository == null) { + throw getInvalidSyncManagerException(); + } else { + final var syncRepositoryRef = syncRepository; + getAccountFuture(fullCard.getAccountId()).thenAcceptAsync(account -> syncRepositoryRef.unassignUserFromCard(getUserByUidDirectly(fullCard.getCard().getAccountId(), account.getUserName()), fullCard.getCard())); + } + } + + private User getUserByUidDirectly(long accountId, String uid) { + return baseRepository.getUserByUidDirectly(accountId, uid); + } + + public void moveCard(long originAccountId, long originCardLocalId, long targetAccountId, long targetBoardLocalId, long targetStackLocalId, @NonNull IResponseCallback<Void> callback) { + if (syncRepository == null) { + callback.onError(getInvalidSyncManagerException()); + } else { + syncRepository.moveCard(originAccountId, originCardLocalId, targetAccountId, targetBoardLocalId, targetStackLocalId, callback); + } + } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/search/SearchAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/search/SearchAdapter.java index cd107e629..5e6422665 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/search/SearchAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/search/SearchAdapter.java @@ -1,6 +1,7 @@ package it.niedermann.nextcloud.deck.ui.main.search; import android.view.LayoutInflater; +import android.view.MenuItem; import android.view.ViewGroup; import androidx.annotation.NonNull; @@ -12,27 +13,36 @@ import java.util.Comparator; import java.util.List; import it.niedermann.nextcloud.deck.DeckLog; +import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemSearchCardBinding; import it.niedermann.nextcloud.deck.databinding.ItemSearchStackBinding; import it.niedermann.nextcloud.deck.model.Account; -import it.niedermann.nextcloud.deck.model.Board; import it.niedermann.nextcloud.deck.model.Stack; +import it.niedermann.nextcloud.deck.model.full.FullBoard; import it.niedermann.nextcloud.deck.model.full.FullCard; import it.niedermann.nextcloud.deck.model.interfaces.IRemoteEntity; +import it.niedermann.nextcloud.deck.ui.card.CardActionListener; +import it.niedermann.nextcloud.deck.ui.card.CardOptionsItemSelectedListener; -public class SearchAdapter extends RecyclerView.Adapter<SearchViewHolder> { +public class SearchAdapter extends RecyclerView.Adapter<SearchViewHolder> implements CardOptionsItemSelectedListener { private static final int TYPE_STACK = 0; private static final int TYPE_CARD = 1; + @NonNull + private CardActionListener cardActionListener; @Nullable private Account account; @Nullable - private Board board; + private FullBoard fullBoard; private final List<IRemoteEntity> items = new ArrayList<>(); @NonNull private String term = ""; + public SearchAdapter(@NonNull CardActionListener cardActionListener) { + this.cardActionListener = cardActionListener; + } + @NonNull @Override public SearchViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { @@ -64,16 +74,16 @@ public class SearchAdapter extends RecyclerView.Adapter<SearchViewHolder> { final var searchStackViewHolder = (SearchStackViewHolder) holder; searchStackViewHolder.bind(stack); - if (board == null) { + if (fullBoard == null) { DeckLog.logError(new IllegalStateException("board is null")); return; } - searchStackViewHolder.applyTheme(board.getColor()); + searchStackViewHolder.applyTheme(fullBoard.getBoard().getColor()); }); break; } case TYPE_CARD: { - if (account == null || board == null) { + if (account == null || fullBoard == null) { DeckLog.logError(new IllegalStateException("account or board is null")); break; } @@ -85,8 +95,8 @@ public class SearchAdapter extends RecyclerView.Adapter<SearchViewHolder> { .map(item -> (FullCard) item) .ifPresent(fullCard -> { final var searchCardViewHolder = (SearchCardViewHolder) holder; - searchCardViewHolder.bind(account, board.getLocalId(), fullCard); - searchCardViewHolder.applyTheme(board.getColor(), term); + searchCardViewHolder.bind(account, fullBoard.getLocalId(), fullCard, fullBoard.getId(), R.menu.card_menu, this); + searchCardViewHolder.applyTheme(fullBoard.getBoard().getColor(), term); }); break; } @@ -123,7 +133,7 @@ public class SearchAdapter extends RecyclerView.Adapter<SearchViewHolder> { public void setItems(@NonNull SearchResults results) { this.account = results.account; - this.board = results.board; + this.fullBoard = results.fullBoard; this.term = results.term; this.items.clear(); @@ -137,4 +147,39 @@ public class SearchAdapter extends RecyclerView.Adapter<SearchViewHolder> { notifyDataSetChanged(); } + + @Override + public boolean onCardOptionsItemSelected(@NonNull MenuItem menuItem, @NonNull FullCard fullCard) { + final int itemId = menuItem.getItemId(); + if (itemId == R.id.share_link) { + if (fullBoard == null) { + DeckLog.warn("Can not share link to card", fullCard.getCard().getTitle(), "because fullBoard is null"); + return false; + } + cardActionListener.onShareLink(fullBoard, fullCard); + return true; + } else if (itemId == R.id.share_content) { + cardActionListener.onShareContent(fullCard); + } else if (itemId == R.id.action_card_assign) { + cardActionListener.onAssignCurrentUser(fullCard); + return true; + } else if (itemId == R.id.action_card_unassign) { + cardActionListener.onUnassignCurrentUser(fullCard); + return true; + } else if (itemId == R.id.action_card_move) { + if (fullBoard == null) { + DeckLog.warn("Can not move card", fullCard.getCard().getTitle(), "because fullBoard is null"); + return false; + } + cardActionListener.onMove(fullBoard, fullCard); + return true; + } else if (itemId == R.id.action_card_archive) { + cardActionListener.onArchive(fullCard); + return true; + } else if (itemId == R.id.action_card_delete) { + cardActionListener.onDelete(fullCard); + return true; + } + return true; + } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/search/SearchCardViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/search/SearchCardViewHolder.java index 9e088f3da..868d9c877 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/search/SearchCardViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/search/SearchCardViewHolder.java @@ -3,7 +3,10 @@ package it.niedermann.nextcloud.deck.ui.main.search; import android.text.TextUtils; import android.view.View; +import androidx.annotation.MenuRes; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.PopupMenu; import com.bumptech.glide.Glide; import com.bumptech.glide.load.resource.bitmap.CenterCrop; @@ -11,11 +14,17 @@ import com.bumptech.glide.load.resource.bitmap.RoundedCorners; import com.bumptech.glide.request.RequestOptions; import com.nextcloud.android.common.ui.theme.utils.ColorRole; +import org.jetbrains.annotations.Contract; + +import java.util.List; + import it.niedermann.android.util.DimensionUtil; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemSearchCardBinding; import it.niedermann.nextcloud.deck.model.Account; +import it.niedermann.nextcloud.deck.model.User; import it.niedermann.nextcloud.deck.model.full.FullCard; +import it.niedermann.nextcloud.deck.ui.card.CardOptionsItemSelectedListener; import it.niedermann.nextcloud.deck.ui.card.EditActivity; import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; import it.niedermann.nextcloud.deck.util.AttachmentUtil; @@ -31,7 +40,7 @@ public class SearchCardViewHolder extends SearchViewHolder { this.binding = binding; } - public void bind(@NonNull Account account, long localBoardId, @NonNull FullCard fullCard) { + public void bind(@NonNull Account account, long localBoardId, @NonNull FullCard fullCard, @Nullable Long boardRemoteId, @MenuRes int optionsMenu, @NonNull CardOptionsItemSelectedListener optionsItemsSelectedListener) { final var context = binding.getRoot().getContext(); binding.getRoot().setOnClickListener(v -> context.startActivity(EditActivity.createEditCardIntent(context, account, localBoardId, fullCard.getLocalId()))); @@ -63,6 +72,35 @@ public class SearchCardViewHolder extends SearchViewHolder { } else { binding.coverImages.setVisibility(View.GONE); } + + binding.cardMenu.setOnClickListener(view -> { + final var popup = new PopupMenu(context, view); + popup.inflate(optionsMenu); + final var menu = popup.getMenu(); + if (containsUser(fullCard.getAssignedUsers(), account.getUserName())) { + menu.removeItem(menu.findItem(R.id.action_card_assign).getItemId()); + } else { + menu.removeItem(menu.findItem(R.id.action_card_unassign).getItemId()); + } + if (boardRemoteId == null || fullCard.getCard().getId() == null) { + menu.removeItem(R.id.share_link); + } + + popup.setOnMenuItemClickListener(item -> optionsItemsSelectedListener.onCardOptionsItemSelected(item, fullCard)); + popup.show(); + }); + } + + @Contract("null, _ -> false") + private static boolean containsUser(List<User> userList, String username) { + if (userList != null) { + for (final var user : userList) { + if (user.getPrimaryKey().equals(username)) { + return true; + } + } + } + return false; } public void applyTheme(int color, String term) { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/search/SearchResults.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/search/SearchResults.java index 128fd8c63..191db7802 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/search/SearchResults.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/search/SearchResults.java @@ -8,8 +8,8 @@ import java.util.List; import java.util.Map; import it.niedermann.nextcloud.deck.model.Account; -import it.niedermann.nextcloud.deck.model.Board; import it.niedermann.nextcloud.deck.model.Stack; +import it.niedermann.nextcloud.deck.model.full.FullBoard; import it.niedermann.nextcloud.deck.model.full.FullCard; public class SearchResults { @@ -18,7 +18,7 @@ public class SearchResults { public final Account account; @Nullable - public final Board board; + public final FullBoard fullBoard; @NonNull public final Map<Stack, List<FullCard>> result; @@ -30,9 +30,9 @@ public class SearchResults { this(null, null, Collections.emptyMap(), ""); } - public SearchResults(@Nullable Account account, @Nullable Board board, @NonNull Map<Stack, List<FullCard>> result, @NonNull String term) { + public SearchResults(@Nullable Account account, @Nullable FullBoard fullBoard, @NonNull Map<Stack, List<FullCard>> result, @NonNull String term) { this.account = account; - this.board = board; + this.fullBoard = fullBoard; this.result = result; this.term = term; } diff --git a/app/src/main/res/layout/item_search_card.xml b/app/src/main/res/layout/item_search_card.xml index 9672259d1..c3f30ec5f 100644 --- a/app/src/main/res/layout/item_search_card.xml +++ b/app/src/main/res/layout/item_search_card.xml @@ -24,12 +24,12 @@ android:id="@+id/title" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="@dimen/spacer_2x" + android:layout_marginHorizontal="@dimen/spacer_2x" android:ellipsize="end" android:maxLines="1" android:textSize="18sp" app:layout_constraintBottom_toTopOf="@id/description" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toStartOf="@+id/card_menu" app:layout_constraintStart_toEndOf="@id/coverImages" app:layout_constraintTop_toTopOf="parent" app:layout_goneMarginStart="0dp" @@ -39,14 +39,27 @@ android:id="@+id/description" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="@dimen/spacer_2x" + android:layout_marginHorizontal="@dimen/spacer_2x" android:layout_marginTop="@dimen/spacer_1hx" android:ellipsize="end" android:maxLines="1" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toStartOf="@+id/card_menu" app:layout_constraintStart_toEndOf="@id/coverImages" app:layout_constraintTop_toBottomOf="@id/title" app:layout_goneMarginStart="0dp" tools:text="@tools:sample/lorem" /> + + <ImageView + android:id="@+id/card_menu" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/label_menu" + android:padding="@dimen/spacer_1hx" + app:layout_constraintBottom_toBottomOf="@+id/coverImages" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@+id/coverImages" + app:srcCompat="@drawable/ic_menu" + app:tint="?attr/colorAccent" /> </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/1023003.txt b/fastlane/metadata/android/en-US/changelogs/1023003.txt index c64b75d31..cd8f35449 100644 --- a/fastlane/metadata/android/en-US/changelogs/1023003.txt +++ b/fastlane/metadata/android/en-US/changelogs/1023003.txt @@ -1 +1,2 @@ +- 🔎 Provide card actions (move, delete, share, …) in search view (#1484) - ⚙️ Updated dependencies
\ No newline at end of file |