diff options
author | Stefan Niedermann <info@niedermann.it> | 2023-03-24 13:28:50 +0300 |
---|---|---|
committer | Stefan Niedermann <info@niedermann.it> | 2023-03-24 13:28:50 +0300 |
commit | 4f31f75f6098ad2c1e3f418e8d7f7d8902a0d273 (patch) | |
tree | f9cd0d0397dd00a412fcdf76884bc83be8a6c036 | |
parent | 41ae988679aa6f84dfb56479eabd9e1c1209405c (diff) |
feat: Implement new SearchBar
Signed-off-by: Stefan Niedermann <info@niedermann.it>
39 files changed, 864 insertions, 289 deletions
diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/database/DataBaseAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/database/DataBaseAdapter.java index dc5883ba9..e0b754866 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/database/DataBaseAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/database/DataBaseAdapter.java @@ -35,6 +35,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; @@ -114,9 +115,9 @@ public class DataBaseAdapter { @VisibleForTesting protected DataBaseAdapter(@NonNull Context applicationContext, - @NonNull DeckDatabase db, - @NonNull ExecutorService widgetNotifierExecutor, - @NonNull ExecutorService executor) { + @NonNull DeckDatabase db, + @NonNull ExecutorService widgetNotifierExecutor, + @NonNull ExecutorService executor) { this.context = applicationContext; this.db = db; this.widgetNotifierExecutor = widgetNotifierExecutor; @@ -866,6 +867,44 @@ public class DataBaseAdapter { .distinctUntilChanged(); } + /** + * Search all {@link FullCard}s grouped by {@link Stack}s which contain the term in {@link Card#getTitle()} or {@link Card#getDescription()}. + * {@link Stack}s are sorted by {@link Stack#getOrder()}, {@link Card}s for each {@link Stack} are sorted by {@link Card#getOrder()}. + */ + public LiveData<Map<Stack, List<FullCard>>> searchCards(final long accountId, final long localBoardId, @NonNull String term, int limitPerStack) { + String sqlSearchTerm = term.trim(); + if (sqlSearchTerm.isEmpty()) { + throw new IllegalArgumentException("empty search term"); + } + sqlSearchTerm = "%" + sqlSearchTerm + "%"; + + + return new ReactiveLiveData<>(db.getCardDao().searchCard(accountId, localBoardId, sqlSearchTerm)) + .map(result -> mapToStacksForCardSearch(result, limitPerStack), executor); + } + + private Map<Stack, List<FullCard>> mapToStacksForCardSearch(List<FullCard> matches, int limitPerStack) { + Map<Stack, List<FullCard>> result = new HashMap<>(); + if (matches != null && !matches.isEmpty()) { + // results are sorted by stack -> jackpot: + Stack lastStack = null; + for (FullCard card : matches) { + // find right bucket + if (lastStack == null || !Objects.equals(lastStack.getLocalId(), card.getCard().getStackId())) { + lastStack = db.getStackDao().getStackByLocalIdDirectly(card.getCard().getStackId()); + } + // check if bucket exists + List<FullCard> fullCards = result.computeIfAbsent(lastStack, k -> new ArrayList<>()); + // create bucket + if (fullCards.size() < limitPerStack) { + // put into bucket + fullCards.add(card); + } + } + } + return result; + } + public LiveData<List<User>> findProposalsForUsersToAssign(final long accountId, long boardId, long notAssignedToLocalCardId, final int topX) { return new ReactiveLiveData<>(db.getUserDao().findProposalsForUsersToAssign(accountId, boardId, notAssignedToLocalCardId, topX)) .distinctUntilChanged(); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/database/dao/CardDao.java b/app/src/main/java/it/niedermann/nextcloud/deck/database/dao/CardDao.java index fbbb1708b..5d25f943f 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/database/dao/CardDao.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/database/dao/CardDao.java @@ -17,23 +17,23 @@ import it.niedermann.nextcloud.deck.model.full.FullCardWithProjects; public interface CardDao extends GenericDao<Card> { String QUERY_UPCOMING_CARDS = "SELECT c.* FROM card c " + - "join stack s on s.localId = c.stackId " + - "join board b on b.localId = s.boardId " + + "join stack s on s.localId = c.stackId " + + "join board b on b.localId = s.boardId " + "WHERE b.archived = 0 and c.archived = 0 and b.status <> 3 and s.status <> 3 and c.status <> 3 " + - "and (c.deletedAt is null or c.deletedAt = 0) " + - "and (s.deletedAt is null or s.deletedAt = 0) " + - "and (b.deletedAt is null or b.deletedAt = 0) " + - // FUll Logic: (hasDueDate AND isIn_PRIVATE_Board) OR (isInSharedBoard AND (assignedToMe OR (hasDueDate AND noAssignees))) - "and (" + - "(c.dueDate is not null AND NOT exists(select 1 from AccessControl ac where ac.boardId = b.localId and ac.status <> 3))" + //(hasDueDate AND isInPrivateBoard) - "OR (" + - "exists(select 1 from AccessControl ac where ac.boardId = b.localId and ac.status <> 3) " + //OR (isInSharedBoard AND - "AND (" + - "(c.dueDate is not null AND not exists(select 1 from JoinCardWithUser j where j.cardId = c.localId)) " + // hasDueDate AND noAssignees OR - "OR exists(select 1 from JoinCardWithUser j where j.cardId = c.localId and j.userId in (select u.localId from user u where u.uid in (select a.userName from Account a)))" + //(assignedToMe - ")" + - ")" + - ")" + + "and (c.deletedAt is null or c.deletedAt = 0) " + + "and (s.deletedAt is null or s.deletedAt = 0) " + + "and (b.deletedAt is null or b.deletedAt = 0) " + + // FUll Logic: (hasDueDate AND isIn_PRIVATE_Board) OR (isInSharedBoard AND (assignedToMe OR (hasDueDate AND noAssignees))) + "and (" + + "(c.dueDate is not null AND NOT exists(select 1 from AccessControl ac where ac.boardId = b.localId and ac.status <> 3))" + //(hasDueDate AND isInPrivateBoard) + "OR (" + + "exists(select 1 from AccessControl ac where ac.boardId = b.localId and ac.status <> 3) " + //OR (isInSharedBoard AND + "AND (" + + "(c.dueDate is not null AND not exists(select 1 from JoinCardWithUser j where j.cardId = c.localId)) " + // hasDueDate AND noAssignees OR + "OR exists(select 1 from JoinCardWithUser j where j.cardId = c.localId and j.userId in (select u.localId from user u where u.uid in (select a.userName from Account a)))" + //(assignedToMe + ")" + + ")" + + ")" + "ORDER BY c.dueDate asc"; @Query("SELECT * FROM card WHERE stackId = :localStackId order by `order`, createdAt asc") @@ -53,7 +53,8 @@ public interface CardDao extends GenericDao<Card> { @Query("SELECT * FROM card WHERE accountId = :accountId and localId = :localId") FullCard getFullCardByLocalIdDirectly(final long accountId, final long localId); - @Transaction // v not deleted! + @Transaction + // v not deleted! @Query("SELECT * FROM card WHERE accountId = :accountId AND archived = 0 AND stackId = :localStackId and status<>3 order by `order`, createdAt asc") LiveData<List<FullCard>> getFullCardsForStack(final long accountId, final long localStackId); @@ -72,6 +73,7 @@ public interface CardDao extends GenericDao<Card> { @Transaction @Query("SELECT * FROM card WHERE accountId = :accountId and localId = :localCardId") LiveData<FullCard> getFullCardByLocalId(final long accountId, final long localCardId); + @Transaction @Query("SELECT * FROM card WHERE accountId = :accountId and localId = :localCardId") LiveData<FullCardWithProjects> getFullCardWithProjectsByLocalId(final long accountId, final long localCardId); @@ -124,4 +126,17 @@ public interface CardDao extends GenericDao<Card> { @Transaction @Query(QUERY_UPCOMING_CARDS) List<FullCard> getUpcomingCardsDirectly(); + + @Transaction + @Query("SELECT c.* FROM card c " + + "inner join Stack s on c.stackId = s.localId " + + "WHERE s.boardId = :localBoardId " + + "and (c.title like :term or c.description like :term) " + + "and c.accountId = :accountId " + + "and s.accountId = :accountId " + + "and c.status <> 3 " + + "and s.status <> 3 " + + "and c.archived = 0 " + + "order by s.`order`, c.`order`") + LiveData<List<FullCard>> searchCard(long accountId, long localBoardId, String term); }
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/model/internal/FilterInformation.java b/app/src/main/java/it/niedermann/nextcloud/deck/model/internal/FilterInformation.java index 9a8d389c6..da0501779 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/model/internal/FilterInformation.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/model/internal/FilterInformation.java @@ -14,7 +14,7 @@ import it.niedermann.nextcloud.deck.model.ocs.projects.OcsProject; public class FilterInformation implements Serializable { - public enum EArchiveStatus{ + public enum EArchiveStatus { ALL, ARCHIVED, NON_ARCHIVED } @@ -168,12 +168,12 @@ public class FilterInformation implements Serializable { if (filterInformation == null) { return false; } - return filterInformation.getDueType() != EDueType.NO_FILTER - || filterInformation.getUsers().size() > 0 - || filterInformation.getProjects().size() > 0 - || filterInformation.getLabels().size() > 0 - || filterInformation.noAssignedUser - || filterInformation.noAssignedProject - || filterInformation.noAssignedLabel; + return !(filterInformation.getDueType() == EDueType.NO_FILTER + && filterInformation.getUsers().isEmpty() + && filterInformation.getProjects().isEmpty() + && filterInformation.getLabels().isEmpty() + && !filterInformation.noAssignedUser + && !filterInformation.noAssignedProject + && !filterInformation.noAssignedLabel); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/repository/BaseRepository.java b/app/src/main/java/it/niedermann/nextcloud/deck/repository/BaseRepository.java index 981b16ec8..1964a3afa 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/repository/BaseRepository.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/repository/BaseRepository.java @@ -14,6 +14,7 @@ import androidx.lifecycle.LiveData; import java.time.Instant; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; import java.util.Optional; import java.util.concurrent.CompletableFuture; @@ -297,6 +298,12 @@ public class BaseRepository { return dataBaseAdapter.getAccessControlByLocalBoardId(accountId, id); } + // -- Card search -- + + public LiveData<Map<Stack, List<FullCard>>> searchCards(final long accountId, final long localBoardId, @NonNull String term, int limit) { + return dataBaseAdapter.searchCards(accountId, localBoardId, term, limit); + } + // --- User search --- public LiveData<List<User>> findProposalsForUsersToAssignForACL(final long accountId, long boardId, final int topX) { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherDialog.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherDialog.java index 7dd47213c..7f976970a 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherDialog.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherDialog.java @@ -77,9 +77,9 @@ public class AccountSwitcherDialog extends DialogFragment { Glide.with(requireContext()) .load(currentAccount.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.currentAccountItemAvatar.getContext(), R.dimen.avatar_size))) + .apply(RequestOptions.circleCropTransform()) .placeholder(R.drawable.ic_baseline_account_circle_24) .error(R.drawable.ic_baseline_account_circle_24) - .apply(RequestOptions.circleCropTransform()) .into(binding.currentAccountItemAvatar); applyTheme(currentAccount.getColor()); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherViewHolder.java index 253e9eb60..6d635b9d6 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/accountswitcher/AccountSwitcherViewHolder.java @@ -35,9 +35,9 @@ public class AccountSwitcherViewHolder extends RecyclerView.ViewHolder { binding.accountHost.setText(Uri.parse(account.getUrl()).getHost()); Glide.with(itemView.getContext()) .load(account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.accountItemAvatar.getContext(), R.dimen.avatar_size))) + .apply(RequestOptions.circleCropTransform()) .placeholder(R.drawable.ic_baseline_account_circle_24) .error(R.drawable.ic_baseline_account_circle_24) - .apply(RequestOptions.circleCropTransform()) .into(binding.accountItemAvatar); itemView.setOnClickListener((v) -> onAccountClick.accept(account)); binding.delete.setVisibility(View.GONE); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlAdapter.java index 8cd5c6548..bb6add7f4 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/board/accesscontrol/AccessControlAdapter.java @@ -83,9 +83,9 @@ public class AccessControlAdapter extends RecyclerView.Adapter<RecyclerView.View ownerHolder.binding.owner.setText(ac.getUser().getDisplayname()); Glide.with(ownerHolder.binding.avatar.getContext()) .load(account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(ownerHolder.binding.avatar.getContext(), R.dimen.avatar_size), ac.getUser().getUid())) + .apply(RequestOptions.circleCropTransform()) .placeholder(R.drawable.ic_person_grey600_24dp) .error(R.drawable.ic_person_grey600_24dp) - .apply(RequestOptions.circleCropTransform()) .into(ownerHolder.binding.avatar); break; } @@ -94,9 +94,9 @@ public class AccessControlAdapter extends RecyclerView.Adapter<RecyclerView.View final var acHolder = (AccessControlViewHolder) holder; Glide.with(acHolder.binding.avatar.getContext()) .load(account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(acHolder.binding.avatar.getContext(), R.dimen.avatar_size), ac.getUser().getUid())) + .apply(RequestOptions.circleCropTransform()) .placeholder(R.drawable.ic_person_grey600_24dp) .error(R.drawable.ic_person_grey600_24dp) - .apply(RequestOptions.circleCropTransform()) .into(acHolder.binding.avatar); acHolder.binding.username.setText(ac.getUser().getDisplayname()); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/AbstractCardViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/AbstractCardViewHolder.java index fb2ee19b2..697b21e1e 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/AbstractCardViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/AbstractCardViewHolder.java @@ -137,7 +137,8 @@ public abstract class AbstractCardViewHolder extends RecyclerView.ViewHolder { coverImagesHolder.addView(coverImageView); Glide.with(coverImageView) .load(new SingleSignOnUrl(account.getName(), AttachmentUtil.getThumbnailUrl(account, fullCard.getId(), coverImage, coverWidth, coverHeight))) - .placeholder(R.color.bg_info_box) + .placeholder(R.drawable.ic_image_grey600_24dp) + .error(R.drawable.ic_image_grey600_24dp) .into(coverImageView); } }); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java index b49f38b0c..b3b539614 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/EditActivity.java @@ -44,6 +44,7 @@ public class EditActivity extends AppCompatActivity { /** * @deprecated This is only here to maintain compatibility with {@link Version#supportsComments()} */ + @Deprecated private static final int[] tabTitles = new int[]{ R.string.card_edit_details, R.string.card_edit_attachments, @@ -60,6 +61,7 @@ public class EditActivity extends AppCompatActivity { /** * @deprecated This is only here to maintain compatibility with {@link Version#supportsComments()} */ + @Deprecated private static final int[] tabIcons = new int[]{ R.drawable.ic_home_grey600_24dp, R.drawable.ic_attach_file_grey600_24dp, diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/UserAutoCompleteAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/UserAutoCompleteAdapter.java index 11012bb75..286c95018 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/UserAutoCompleteAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/UserAutoCompleteAdapter.java @@ -83,9 +83,9 @@ public class UserAutoCompleteAdapter extends AutoCompleteAdapter<User> { Glide.with(binding.icon.getContext()) .load(account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.icon.getContext(), R.dimen.avatar_size), getItem(position).getUid())) + .apply(RequestOptions.circleCropTransform()) .placeholder(R.drawable.ic_person_grey600_24dp) .error(R.drawable.ic_person_grey600_24dp) - .apply(RequestOptions.circleCropTransform()) .into(binding.icon); binding.label.setText(getItem(position).getDisplayname()); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactItemViewHolder.java index f403fed21..7630d2d9e 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactItemViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/ContactItemViewHolder.java @@ -1,5 +1,9 @@ package it.niedermann.nextcloud.deck.ui.card.attachments.picker; +import static android.view.View.GONE; +import static android.view.View.VISIBLE; +import static it.niedermann.nextcloud.deck.util.VCardUtil.getColorBasedOnDisplayName; + import android.graphics.Bitmap; import android.graphics.drawable.ColorDrawable; import android.net.Uri; @@ -19,10 +23,6 @@ import java.util.function.BiConsumer; import it.niedermann.nextcloud.deck.R; import it.niedermann.nextcloud.deck.databinding.ItemPickerUserBinding; -import static android.view.View.GONE; -import static android.view.View.VISIBLE; -import static it.niedermann.nextcloud.deck.util.VCardUtil.getColorBasedOnDisplayName; - public class ContactItemViewHolder extends RecyclerView.ViewHolder { private final ItemPickerUserBinding binding; @@ -51,8 +51,9 @@ public class ContactItemViewHolder extends RecyclerView.ViewHolder { binding.initials.setText(null); Glide.with(itemView.getContext()) .load(image) - .placeholder(R.drawable.ic_person_grey600_24dp) .apply(RequestOptions.circleCropTransform()) + .placeholder(R.drawable.ic_person_grey600_24dp) + .error(R.drawable.ic_person_grey600_24dp) .into(binding.avatar); } } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemViewHolder.java index 346fca9c3..402329767 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/attachments/picker/GalleryItemViewHolder.java @@ -30,6 +30,7 @@ public class GalleryItemViewHolder extends RecyclerView.ViewHolder { Glide.with(itemView.getContext()) .load(image) .placeholder(R.drawable.ic_image_grey600_24dp) + .error(R.drawable.ic_image_grey600_24dp) .into(binding.preview); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsFragment.java index c4d87740c..f46aafd1c 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsFragment.java @@ -93,9 +93,9 @@ public class CardCommentsFragment extends Fragment implements CommentEditedListe binding.replyCommentCancelButton.setOnClickListener((v) -> commentsViewModel.setReplyToComment(null)); Glide.with(binding.avatar.getContext()) .load(editCardViewModel.getAccount().getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.icon_size_details))) + .apply(RequestOptions.circleCropTransform()) .placeholder(R.drawable.ic_person_grey600_24dp) .error(R.drawable.ic_person_grey600_24dp) - .apply(RequestOptions.circleCropTransform()) .into(binding.avatar); commentsViewModel.getReplyToComment().observe(getViewLifecycleOwner(), (comment) -> { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsMentionProposer.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsMentionProposer.java index c41420147..403e26886 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsMentionProposer.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/CardCommentsMentionProposer.java @@ -92,9 +92,9 @@ public class CardCommentsMentionProposer implements TextWatcher { Glide.with(avatar.getContext()) .load(account.getAvatarUrl(avatarSize, user.getUid())) + .apply(RequestOptions.circleCropTransform()) .placeholder(R.drawable.ic_person_grey600_24dp) .error(R.drawable.ic_person_grey600_24dp) - .apply(RequestOptions.circleCropTransform()) .into(avatar); } } else { diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/ItemCommentViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/ItemCommentViewHolder.java index dc9b8dbb5..221beea76 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/ItemCommentViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/comments/ItemCommentViewHolder.java @@ -44,9 +44,9 @@ public class ItemCommentViewHolder extends RecyclerView.ViewHolder { public void bind(@NonNull FullDeckComment comment, @NonNull Account account, @Nullable ThemeUtils utils, @NonNull MenuInflater inflater, @NonNull CommentDeletedListener deletedListener, @NonNull CommentSelectAsReplyListener selectAsReplyListener, @NonNull FragmentManager fragmentManager, @NonNull Consumer<CharSequence> editListener) { Glide.with(binding.avatar.getContext()) .load(account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.avatar_size), comment.getComment().getActorId())) + .apply(RequestOptions.circleCropTransform()) .placeholder(R.drawable.ic_person_grey600_24dp) .error(R.drawable.ic_person_grey600_24dp) - .apply(RequestOptions.circleCropTransform()) .into(binding.avatar); final var mentions = new HashMap<String, String>(comment.getComment().getMentions().size()); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeViewHolder.java index dd5bfa4af..989b17ef7 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/card/details/AssigneeViewHolder.java @@ -26,9 +26,9 @@ public class AssigneeViewHolder extends RecyclerView.ViewHolder { public void bind(@NonNull Account account, @NonNull User user, @Nullable Consumer<User> onClickListener) { Glide.with(binding.avatar.getContext()) .load(account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.avatar_size), user.getUid())) + .apply(RequestOptions.circleCropTransform()) .placeholder(R.drawable.ic_person_grey600_24dp) .error(R.drawable.ic_person_grey600_24dp) - .apply(RequestOptions.circleCropTransform()) .into(binding.avatar); if (onClickListener != null) { itemView.setOnClickListener((v) -> onClickListener.accept(user)); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDialogFragment.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDialogFragment.java index 08512c5c6..582303476 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDialogFragment.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterDialogFragment.java @@ -1,7 +1,6 @@ package it.niedermann.nextcloud.deck.ui.filter; import android.app.Dialog; -import android.graphics.Color; import android.graphics.drawable.Drawable; import android.os.Bundle; @@ -108,7 +107,7 @@ public class FilterDialogFragment extends ThemedDialogFragment { public void applyTheme(@ColorInt int color) { final var utils = ThemeUtils.of(color, requireContext()); - utils.deck.themeTabLayout(binding.tabLayout, Color.TRANSPARENT); + utils.deck.themeTabLayoutOnTransparent(binding.tabLayout); utils.platform.tintDrawable(requireContext(), indicator, ColorRole.PRIMARY); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java index 9c89a08a9..fec2186a1 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/filter/FilterUserAdapter.java @@ -90,9 +90,9 @@ public class FilterUserAdapter extends RecyclerView.Adapter<FilterUserAdapter.Us binding.title.setText(user.getDisplayname()); Glide.with(binding.avatar.getContext()) .load(account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.avatar.getContext(), R.dimen.avatar_size), user.getUid())) + .apply(RequestOptions.circleCropTransform()) .placeholder(R.drawable.ic_person_grey600_24dp) .error(R.drawable.ic_person_grey600_24dp) - .apply(RequestOptions.circleCropTransform()) .into(binding.avatar); itemView.setSelected(selectedUsers.contains(user)); applyTheme(color); 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 20fd6c12b..3e4eed845 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 @@ -2,19 +2,19 @@ package it.niedermann.nextcloud.deck.ui.main; import static java.util.Collections.emptyList; -import android.animation.AnimatorInflater; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.graphics.drawable.Drawable; import android.net.ConnectivityManager; import android.net.Network; import android.net.NetworkRequest; import android.net.Uri; import android.os.Bundle; +import android.text.TextUtils; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.view.inputmethod.InputMethodManager; import android.widget.PopupMenu; import androidx.activity.OnBackPressedCallback; @@ -34,10 +34,13 @@ import androidx.core.splashscreen.SplashScreen; import androidx.core.view.GravityCompat; import androidx.core.view.ViewCompat; import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; +import com.bumptech.glide.request.target.CustomTarget; +import com.bumptech.glide.request.transition.Transition; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; import com.google.android.material.tabs.TabLayoutMediator; @@ -90,6 +93,8 @@ import it.niedermann.nextcloud.deck.ui.exception.ExceptionDialogFragment; import it.niedermann.nextcloud.deck.ui.exception.ExceptionHandler; 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.settings.PreferencesViewModel; import it.niedermann.nextcloud.deck.ui.stack.DeleteStackDialogFragment; import it.niedermann.nextcloud.deck.ui.stack.DeleteStackListener; @@ -110,6 +115,21 @@ public class MainActivity extends AppCompatActivity implements DeleteStackListen private PreferencesViewModel preferencesViewModel; protected MainViewModel mainViewModel; private FilterViewModel filterViewModel; + private SearchAdapter searchAdapter; + @Nullable + private MutableLiveData<SearchResults> searchResults$ = null; + private final Observer<SearchResults> searchResultsObserver = results -> { + if (results.result.isEmpty()) { + binding.emptyContentViewSearchNoResults.setVisibility(View.VISIBLE); + binding.emptyContentViewSearchNoTerm.setVisibility(View.GONE); + binding.searchResults.setVisibility(View.GONE); + } else { + binding.emptyContentViewSearchNoResults.setVisibility(View.GONE); + binding.searchResults.setVisibility(View.VISIBLE); + } + this.searchAdapter.setItems(results); + }; + private final ReactiveLiveData<String> searchTerm$ = new ReactiveLiveData<>(); private StackAdapter stackAdapter; private DrawerMenuInflater<MainActivity> drawerMenuInflater; private Menu listMenu; @@ -124,8 +144,8 @@ public class MainActivity extends AppCompatActivity implements DeleteStackListen public void handleOnBackPressed() { if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) { binding.drawerLayout.closeDrawer(GravityCompat.START); - } else if (binding.searchToolbar.getVisibility() == View.VISIBLE) { - hideFilterTextToolbar(); + } else if (binding.searchView.isShowing()) { + binding.searchView.hide(); } else { finish(); } @@ -164,14 +184,24 @@ public class MainActivity extends AppCompatActivity implements DeleteStackListen navigationHandler = new MainActivityNavigationHandler(this, binding.drawerLayout, mainViewModel::saveCurrentBoardId); binding.navigationView.setNavigationItemSelectedListener(navigationHandler); + searchAdapter = new SearchAdapter(); + binding.searchResults.setAdapter(searchAdapter); + binding.searchView.getEditText().addTextChangedListener(new OnTextChangedWatcher(value -> { + if (TextUtils.isEmpty(value)) { + binding.emptyContentViewSearchNoTerm.setVisibility(View.VISIBLE); + binding.emptyContentViewSearchNoResults.setVisibility(View.GONE); + binding.searchResults.setVisibility(View.GONE); + searchAdapter.setItems(new SearchResults()); + } else { + binding.emptyContentViewSearchNoTerm.setVisibility(View.GONE); + binding.searchResults.setVisibility(View.VISIBLE); + searchTerm$.setValue(value); + } + })); + stackAdapter = new StackAdapter(this); binding.viewPager.setAdapter(stackAdapter); binding.viewPager.setOffscreenPageLimit(2); - binding.filterWrapper.setOnClickListener((v) -> FilterDialogFragment.newInstance().show(getSupportFragmentManager(), FilterDialogFragment.class.getCanonicalName())); - binding.filterText.addTextChangedListener(new OnTextChangedWatcher(filterViewModel::setFilterText)); - binding.enableSearch.setOnClickListener(v -> showFilterTextToolbar()); - binding.toolbar.setOnClickListener(v -> showFilterTextToolbar()); - binding.accountSwitcher.setOnClickListener(v -> AccountSwitcherDialog.newInstance().show(getSupportFragmentManager(), AccountSwitcherDialog.class.getSimpleName())); headerBinding.copyDebugLogs.setOnClickListener((v) -> { try { @@ -199,7 +229,11 @@ public class MainActivity extends AppCompatActivity implements DeleteStackListen drawerMenuInflater = new DrawerMenuInflater<>(this, binding.navigationView.getMenu()); preferencesViewModel.isDebugModeEnabled$().observe(this, enabled -> headerBinding.copyDebugLogs.setVisibility(enabled ? View.VISIBLE : View.GONE)); - filterViewModel.hasActiveFilter().observe(this, hasActiveFilter -> binding.filterWrapper.setActivated(hasActiveFilter)); + filterViewModel.hasActiveFilter().observe(this, hasActiveFilter -> { + final var menu = binding.toolbar.getMenu(); + menu.findItem(R.id.filter).setVisible(!hasActiveFilter); + menu.findItem(R.id.filter_active).setVisible(hasActiveFilter); + }); // Flag to distinguish user initiated stack changes from stack changes derived by changing the board final var boardChanged = new AtomicBoolean(true); @@ -275,12 +309,22 @@ public class MainActivity extends AppCompatActivity implements DeleteStackListen } Glide - .with(binding.accountSwitcher.getContext()) - .load(account.getAvatarUrl(binding.accountSwitcher.getWidth())) + .with(binding.toolbar.getContext()) + .load(account.getAvatarUrl(binding.toolbar.getMenu().findItem(R.id.avatar).getIcon().getIntrinsicWidth())) + .apply(RequestOptions.circleCropTransform()) .placeholder(R.drawable.ic_baseline_account_circle_24) .error(R.drawable.ic_baseline_account_circle_24) - .apply(RequestOptions.circleCropTransform()) - .into(binding.accountSwitcher); + .into(new CustomTarget<Drawable>() { + @Override + public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) { + binding.toolbar.getMenu().findItem(R.id.avatar).setIcon(resource); + } + + @Override + public void onLoadCleared(@Nullable Drawable placeholder) { + + } + }); DeckLog.verbose("Displaying maintenance mode info for", account.getName() + ":", account.isMaintenanceEnabled()); binding.infoBox.setVisibility(account.isMaintenanceEnabled() ? View.VISIBLE : View.GONE); @@ -340,13 +384,17 @@ public class MainActivity extends AppCompatActivity implements DeleteStackListen protected void applyBoard(@NonNull Account account, @NonNull Map<Integer, Long> navigationMap, @Nullable FullBoard currentBoard) { DeckLog.verbose("===== Apply Board", currentBoard); filterViewModel.clearFilterInformation(true); + binding.toolbar.getMenu().findItem(R.id.filter).setVisible(true); + binding.toolbar.getMenu().findItem(R.id.filter_active).setVisible(false); + binding.appBarLayout.setExpanded(true); + + observeSearchTerm(account, currentBoard); if (currentBoard == null) { applyBoardTheme(account.getColor()); showEditButtonsIfPermissionsGranted(false, false); - binding.toolbar.setTitle(R.string.app_name_short); - binding.filterText.setHint(R.string.app_name_short); + binding.toolbar.setHint(R.string.app_name_short); binding.fab.setText(R.string.add_board); binding.fab.setOnClickListener(v -> { binding.fab.hide(); @@ -356,8 +404,7 @@ public class MainActivity extends AppCompatActivity implements DeleteStackListen applyBoardTheme(currentBoard.getBoard().getColor()); showEditButtonsIfPermissionsGranted(true, currentBoard.board.isPermissionEdit()); - binding.toolbar.setTitle(currentBoard.getBoard().getTitle()); - binding.filterText.setHint(getString(R.string.search_in, currentBoard.getBoard().getTitle())); + binding.toolbar.setHint(getString(R.string.search_in, currentBoard.getBoard().getTitle())); binding.fab.setText(R.string.add_list); binding.fab.setOnClickListener(v -> { binding.fab.hide(); @@ -373,6 +420,21 @@ public class MainActivity extends AppCompatActivity implements DeleteStackListen .ifPresent(menuItemId -> binding.navigationView.setCheckedItem(menuItemId)); } + binding.searchView.clearText(); + } + + private void observeSearchTerm(@NonNull Account account, @Nullable FullBoard currentBoard) { + if (searchResults$ != null) { + searchResults$.removeObserver(searchResultsObserver); + } + if (currentBoard != null) { + searchResults$ = searchTerm$ + .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)); + searchResults$.observe(this, searchResultsObserver); + } } private void applyStacks(@Nullable Account account, @Nullable Long boardId, @Nullable List<Stack> stacks) { @@ -427,11 +489,11 @@ public class MainActivity extends AppCompatActivity implements DeleteStackListen private void applyBoardTheme(@ColorInt int color) { final var utils = ThemeUtils.of(color, this); - utils.deck.themeFilterIndicator(this, binding.filterWrapper.getDrawable()); - utils.deck.themeTabLayout(binding.stackTitles); + utils.deck.themeSearchBar(binding.toolbar); + utils.deck.themeSearchView(binding.searchView); + utils.deck.themeTabLayoutOnTransparent(binding.stackTitles); utils.material.themeExtendedFAB(binding.fab); utils.androidx.themeSwipeRefreshLayout(binding.swipeRefreshLayout); - utils.platform.colorEditText(binding.filterText); binding.emptyContentViewStacks.applyTheme(color); } @@ -602,9 +664,20 @@ public class MainActivity extends AppCompatActivity implements DeleteStackListen } @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.main_menu, menu); + return super.onCreateOptionsMenu(menu); + } + + @Override public boolean onOptionsItemSelected(MenuItem item) { final int itemId = item.getItemId(); - if (itemId == R.id.archive_cards) { + + if (itemId == R.id.filter || itemId == R.id.filter_active) { + FilterDialogFragment.newInstance().show(getSupportFragmentManager(), FilterDialogFragment.class.getCanonicalName()); + } else if (itemId == R.id.avatar) { + AccountSwitcherDialog.newInstance().show(getSupportFragmentManager(), AccountSwitcherDialog.class.getSimpleName()); + } else if (itemId == R.id.archive_cards) { final var stack = stackAdapter.getItem(binding.viewPager.getCurrentItem()); final var stackLocalId = stack.getLocalId(); mainViewModel.countCardsInStack(stack.getAccountId(), stackLocalId, numberOfCards -> runOnUiThread(() -> @@ -707,27 +780,6 @@ public class MainActivity extends AppCompatActivity implements DeleteStackListen binding.fab.extend(); } - private void showFilterTextToolbar() { - binding.toolbar.setVisibility(View.GONE); - binding.searchToolbar.setVisibility(View.VISIBLE); - binding.searchToolbar.setNavigationOnClickListener(v1 -> onBackPressedCallback.handleOnBackPressed()); - binding.enableSearch.setVisibility(View.GONE); - binding.filterText.requestFocus(); - final var imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(binding.filterText, InputMethodManager.SHOW_IMPLICIT); - binding.toolbarCard.setStateListAnimator(AnimatorInflater.loadStateListAnimator(this, R.animator.appbar_elevation_on)); - } - - private void hideFilterTextToolbar() { - binding.filterText.setText(null); - final var imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0); - binding.searchToolbar.setVisibility(View.GONE); - binding.enableSearch.setVisibility(View.VISIBLE); - binding.toolbar.setVisibility(View.VISIBLE); - binding.toolbarCard.setStateListAnimator(AnimatorInflater.loadStateListAnimator(this, R.animator.appbar_elevation_off)); - } - private void registerAutoSyncOnNetworkAvailable(@NonNull Account account) { final var connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); final var builder = new NetworkRequest.Builder(); @@ -912,5 +964,4 @@ public class MainActivity extends AppCompatActivity implements DeleteStackListen .newInstance(throwable, account) .show(getSupportFragmentManager(), ExceptionDialogFragment.class.getSimpleName())); } - }
\ No newline at end of file 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 25e43fe4c..e9afe5419 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 @@ -16,6 +16,7 @@ import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundExce import java.io.File; import java.time.Instant; import java.util.List; +import java.util.Map; import java.util.concurrent.CompletableFuture; import it.niedermann.android.reactivelivedata.ReactiveLiveData; @@ -58,6 +59,10 @@ public class MainViewModel extends BaseViewModel { return new IllegalStateException("SyncManager is null"); } + public LiveData<Map<Stack, List<FullCard>>> searchCards(long accountId, long boardId, @NonNull String term, int limit) { + return baseRepository.searchCards(accountId, boardId, term, limit); + } + public void synchronize(@NonNull Account account, @NonNull IResponseCallback<Boolean> callback) { if (syncRepository == null) { callback.onError(getInvalidSyncManagerException()); 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 new file mode 100644 index 000000000..cd107e629 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/search/SearchAdapter.java @@ -0,0 +1,140 @@ +package it.niedermann.nextcloud.deck.ui.main.search; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import it.niedermann.nextcloud.deck.DeckLog; +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.FullCard; +import it.niedermann.nextcloud.deck.model.interfaces.IRemoteEntity; + +public class SearchAdapter extends RecyclerView.Adapter<SearchViewHolder> { + + private static final int TYPE_STACK = 0; + private static final int TYPE_CARD = 1; + + @Nullable + private Account account; + @Nullable + private Board board; + private final List<IRemoteEntity> items = new ArrayList<>(); + @NonNull + private String term = ""; + + @NonNull + @Override + public SearchViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + final var context = parent.getContext(); + switch (viewType) { + case TYPE_STACK: { + return new SearchStackViewHolder(ItemSearchStackBinding.inflate(LayoutInflater.from(context), parent, false)); + } + case TYPE_CARD: { + return new SearchCardViewHolder(ItemSearchCardBinding.inflate(LayoutInflater.from(context), parent, false)); + } + default: { + throw new UnsupportedOperationException("Unknown view type: " + viewType); + } + } + } + + @Override + public void onBindViewHolder(@NonNull SearchViewHolder holder, int position) { + switch (getItemViewType(position)) { + case TYPE_STACK: { + final var localId = -getItemId(position); + items.stream() + .filter(item -> item.getClass() == Stack.class) + .filter(item -> item.getLocalId() == localId) + .findAny() + .map(item -> (Stack) item) + .ifPresent(stack -> { + final var searchStackViewHolder = (SearchStackViewHolder) holder; + searchStackViewHolder.bind(stack); + + if (board == null) { + DeckLog.logError(new IllegalStateException("board is null")); + return; + } + searchStackViewHolder.applyTheme(board.getColor()); + }); + break; + } + case TYPE_CARD: { + if (account == null || board == null) { + DeckLog.logError(new IllegalStateException("account or board is null")); + break; + } + final var localId = getItemId(position); + items.stream() + .filter(item -> item.getClass() == FullCard.class) + .filter(item -> item.getLocalId() == localId) + .findAny() + .map(item -> (FullCard) item) + .ifPresent(fullCard -> { + final var searchCardViewHolder = (SearchCardViewHolder) holder; + searchCardViewHolder.bind(account, board.getLocalId(), fullCard); + searchCardViewHolder.applyTheme(board.getColor(), term); + }); + break; + } + default: { + throw new UnsupportedOperationException("Unknown view type for position " + position); + } + } + } + + @Override + public int getItemViewType(int position) { + return getItemId(position) > 0 ? TYPE_CARD : TYPE_STACK; + } + + /** + * @return {@link FullCard#getLocalId()} or <strong>negated</strong> {@link Stack#getLocalId()} + */ + @Override + public long getItemId(int position) { + final var item = items.get(position); + final var clazz = item.getClass(); + if (clazz == Stack.class) { + return -item.getLocalId(); + } else if (clazz == FullCard.class) { + return item.getLocalId(); + } + throw new UnsupportedOperationException("Expected item list to only contain " + Stack.class.getSimpleName() + " or " + FullCard.class.getSimpleName() + " but found " + clazz.getSimpleName()); + } + + @Override + public int getItemCount() { + return items.size(); + } + + public void setItems(@NonNull SearchResults results) { + this.account = results.account; + this.board = results.board; + this.term = results.term; + + this.items.clear(); + results.result.entrySet() + .stream() + .sorted(Comparator.comparingLong(o -> o.getKey().getOrder())) + .forEach(entry -> { + this.items.add(entry.getKey()); + this.items.addAll(entry.getValue()); + }); + + notifyDataSetChanged(); + } +} 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 new file mode 100644 index 000000000..9e088f3da --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/search/SearchCardViewHolder.java @@ -0,0 +1,74 @@ +package it.niedermann.nextcloud.deck.ui.main.search; + +import android.text.TextUtils; +import android.view.View; + +import androidx.annotation.NonNull; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.resource.bitmap.CenterCrop; +import com.bumptech.glide.load.resource.bitmap.RoundedCorners; +import com.bumptech.glide.request.RequestOptions; +import com.nextcloud.android.common.ui.theme.utils.ColorRole; + +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.full.FullCard; +import it.niedermann.nextcloud.deck.ui.card.EditActivity; +import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; +import it.niedermann.nextcloud.deck.util.AttachmentUtil; +import it.niedermann.nextcloud.deck.util.MimeTypeUtil; +import it.niedermann.nextcloud.sso.glide.SingleSignOnUrl; + +public class SearchCardViewHolder extends SearchViewHolder { + + private final ItemSearchCardBinding binding; + + public SearchCardViewHolder(@NonNull ItemSearchCardBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bind(@NonNull Account account, long localBoardId, @NonNull FullCard fullCard) { + final var context = binding.getRoot().getContext(); + binding.getRoot().setOnClickListener(v -> context.startActivity(EditActivity.createEditCardIntent(context, account, localBoardId, fullCard.getLocalId()))); + + binding.title.setText(fullCard.getCard().getTitle()); + if (TextUtils.isEmpty(fullCard.getCard().getDescription())) { + binding.description.setVisibility(View.GONE); + } else { + binding.description.setVisibility(View.VISIBLE); + binding.description.setText(fullCard.getCard().getDescription()); + } + + + final var coverImages = fullCard.getAttachments() + .stream() + .filter(attachment -> MimeTypeUtil.isImage(attachment.getMimetype())) + .findFirst(); + + if (coverImages.isPresent()) { + binding.coverImages.setVisibility(View.VISIBLE); + binding.coverImages.post(() -> Glide.with(binding.coverImages) + .load(new SingleSignOnUrl(account.getName(), AttachmentUtil.getThumbnailUrl(account, fullCard.getId(), coverImages.get(), binding.coverImages.getWidth()))) + .apply(new RequestOptions().transform( + new CenterCrop(), + new RoundedCorners(DimensionUtil.INSTANCE.dpToPx(context, R.dimen.spacer_1x)) + )) + .placeholder(R.drawable.ic_image_grey600_24dp) + .error(R.drawable.ic_image_grey600_24dp) + .into(binding.coverImages)); + } else { + binding.coverImages.setVisibility(View.GONE); + } + } + + public void applyTheme(int color, String term) { + final var utils = ThemeUtils.of(color, binding.getRoot().getContext()); + utils.platform.colorTextView(binding.title, ColorRole.ON_SURFACE); + utils.platform.highlightText(binding.title, binding.title.getText().toString(), term); + utils.platform.highlightText(binding.description, binding.description.getText().toString(), 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 new file mode 100644 index 000000000..128fd8c63 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/search/SearchResults.java @@ -0,0 +1,39 @@ +package it.niedermann.nextcloud.deck.ui.main.search; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Collections; +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.FullCard; + +public class SearchResults { + + @Nullable + public final Account account; + + @Nullable + public final Board board; + + @NonNull + public final Map<Stack, List<FullCard>> result; + + @NonNull + public final String term; + + public SearchResults() { + this(null, null, Collections.emptyMap(), ""); + } + + public SearchResults(@Nullable Account account, @Nullable Board board, @NonNull Map<Stack, List<FullCard>> result, @NonNull String term) { + this.account = account; + this.board = board; + this.result = result; + this.term = term; + } +}
\ No newline at end of file diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/search/SearchStackViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/search/SearchStackViewHolder.java new file mode 100644 index 000000000..027952372 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/search/SearchStackViewHolder.java @@ -0,0 +1,28 @@ +package it.niedermann.nextcloud.deck.ui.main.search; + +import androidx.annotation.NonNull; + +import com.nextcloud.android.common.ui.theme.utils.ColorRole; + +import it.niedermann.nextcloud.deck.databinding.ItemSearchStackBinding; +import it.niedermann.nextcloud.deck.model.Stack; +import it.niedermann.nextcloud.deck.ui.theme.ThemeUtils; + +public class SearchStackViewHolder extends SearchViewHolder { + + private final ItemSearchStackBinding binding; + + public SearchStackViewHolder(@NonNull ItemSearchStackBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + + public void bind(@NonNull Stack stack) { + binding.title.setText(stack.getTitle()); + } + + public void applyTheme(int color) { + final var utils = ThemeUtils.of(color, binding.getRoot().getContext()); + utils.platform.colorTextView(binding.title, ColorRole.ON_SURFACE_VARIANT); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/search/SearchViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/search/SearchViewHolder.java new file mode 100644 index 000000000..f787beed4 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/main/search/SearchViewHolder.java @@ -0,0 +1,13 @@ +package it.niedermann.nextcloud.deck.ui.main.search; + +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +public abstract class SearchViewHolder extends RecyclerView.ViewHolder { + + public SearchViewHolder(@NonNull View itemView) { + super(itemView); + } +} diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountViewHolder.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountViewHolder.java index 79ba73c32..c4684fe55 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountViewHolder.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/manageaccounts/ManageAccountViewHolder.java @@ -39,9 +39,9 @@ public class ManageAccountViewHolder extends RecyclerView.ViewHolder { binding.accountHost.setText(Uri.parse(account.getUrl()).getHost()); Glide.with(itemView.getContext()) .load(account.getAvatarUrl(DimensionUtil.INSTANCE.dpToPx(binding.accountItemAvatar.getContext(), R.dimen.avatar_size))) + .apply(RequestOptions.circleCropTransform()) .placeholder(R.drawable.ic_baseline_account_circle_24) .error(R.drawable.ic_baseline_account_circle_24) - .apply(RequestOptions.circleCropTransform()) .into(binding.accountItemAvatar); binding.currentAccountIndicator.setSelected(isCurrentAccount); itemView.setOnClickListener((v) -> onAccountClick.accept(account)); diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/DeckViewThemeUtils.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/DeckViewThemeUtils.java index db0134600..b325388b2 100644 --- a/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/DeckViewThemeUtils.java +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/theme/DeckViewThemeUtils.java @@ -1,6 +1,7 @@ package it.niedermann.nextcloud.deck.ui.theme; import static com.nextcloud.android.common.ui.util.ColorStateListUtilsKt.buildColorStateList; +import static com.nextcloud.android.common.ui.util.PlatformThemeUtil.isDarkMode; import static java.time.temporal.ChronoUnit.DAYS; import android.content.Context; @@ -24,6 +25,8 @@ import androidx.core.content.res.ResourcesCompat; import androidx.core.graphics.drawable.DrawableCompat; import androidx.core.widget.TextViewCompat; +import com.google.android.material.search.SearchBar; +import com.google.android.material.search.SearchView; import com.google.android.material.tabs.TabLayout; import com.nextcloud.android.common.ui.theme.MaterialSchemes; import com.nextcloud.android.common.ui.theme.ViewThemeUtilsBase; @@ -58,21 +61,53 @@ public class DeckViewThemeUtils extends ViewThemeUtilsBase { } /** + * Themes the <code>tabLayout</code> using {@link MaterialViewThemeUtils#themeTabLayout(TabLayout)} + * and then applies <code>null</code> as {@link TabLayout#setBackground(Drawable)}. + */ + public void themeTabLayoutOnTransparent(@NonNull TabLayout tabLayout) { + this.material.themeTabLayout(tabLayout); + tabLayout.setBackground(null); + } + + /** * Convenience method for calling {@link #themeTabLayout(TabLayout, int)} with the primary color */ public void themeTabLayout(@NonNull TabLayout tabLayout) { themeTabLayout(tabLayout, ContextCompat.getColor(tabLayout.getContext(), R.color.primary)); } - /** - * Themes the <code>tabLayout</code> using {@link MaterialViewThemeUtils#themeTabLayout(TabLayout)} - * and then applies <code>backgroundColor</code>. - */ public void themeTabLayout(@NonNull TabLayout tabLayout, @ColorInt int backgroundColor) { this.material.themeTabLayout(tabLayout); tabLayout.setBackgroundColor(backgroundColor); } + public void themeSearchBar(@NonNull SearchBar searchBar) { + withScheme(searchBar.getContext(), scheme -> { + final var colorStateList = ColorStateList.valueOf( + isDarkMode(searchBar.getContext()) + ? scheme.getSurface() + : scheme.getSurfaceVariant()); + + searchBar.setBackgroundTintList(colorStateList); + + final var menu = searchBar.getMenu(); + for (int i = 0; i < menu.size(); i++) { + if (menu.getItem(i).getItemId() != R.id.avatar) { + platform.colorToolbarMenuIcon(searchBar.getContext(), menu.getItem(i)); + } + } + + return searchBar; + }); + } + + public void themeSearchView(@NonNull SearchView searchView) { + withScheme(searchView.getContext(), scheme -> { + searchView.setBackgroundTintList(ColorStateList.valueOf(scheme.getSurface())); + return searchView; + }); + } + public Drawable themeNavigationViewIcon(@NonNull Context context, @DrawableRes int icon) { return withScheme(context, scheme -> { final var colorStateList = buildColorStateList( @@ -110,14 +145,6 @@ public class DeckViewThemeUtils extends ViewThemeUtilsBase { .ifPresent(drawable -> platform.tintDrawable(context, drawable, ColorRole.PRIMARY)); } - /** - * Use <strong>only</strong> for <code>@drawable/filter</code> - */ - public void themeFilterIndicator(@NonNull Context context, @NonNull Drawable filter) { - getStateDrawable(filter, android.R.attr.state_activated, R.id.indicator) - .ifPresent(drawable -> platform.tintDrawable(context, drawable, ColorRole.PRIMARY)); - } - private Optional<Drawable> getStateDrawable(@NonNull Drawable drawable, @AttrRes int state, @IdRes int layerId) { return getStateDrawable(drawable, new int[]{state}, layerId); } diff --git a/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/FilterIndicator.java b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/FilterIndicator.java new file mode 100644 index 000000000..73b3a84a6 --- /dev/null +++ b/app/src/main/java/it/niedermann/nextcloud/deck/ui/view/FilterIndicator.java @@ -0,0 +1,27 @@ +package it.niedermann.nextcloud.deck.ui.view; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.core.content.ContextCompat; + +import com.google.android.material.button.MaterialButton; + +import it.niedermann.nextcloud.deck.R; + +public class FilterIndicator extends MaterialButton { + + public FilterIndicator(Context context) { + this(context, null, 0); + } + + public FilterIndicator(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public FilterIndicator(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, R.style.ThemeOverlay_Material3_Button_IconButton); + setIcon(ContextCompat.getDrawable(context, R.drawable.filter_active)); + } + +}
\ No newline at end of file diff --git a/app/src/main/res/drawable/filter.xml b/app/src/main/res/drawable/filter.xml deleted file mode 100644 index a3933e864..000000000 --- a/app/src/main/res/drawable/filter.xml +++ /dev/null @@ -1,14 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:state_activated="true"> - <layer-list> - <item android:drawable="@drawable/ic_filter_list_white_24dp" /> - <item android:id="@+id/indicator" android:width="8dp" android:height="8dp" android:enterFadeDuration="@android:integer/config_shortAnimTime" android:gravity="bottom|end"> - <shape android:shape="oval"> - <solid android:color="@color/defaultBrand" /> - </shape> - </item> - </layer-list> - </item> - <item android:drawable="@drawable/ic_filter_list_white_24dp" /> -</selector>
\ No newline at end of file diff --git a/app/src/main/res/drawable/filter_active.xml b/app/src/main/res/drawable/filter_active.xml new file mode 100644 index 000000000..06b74eb85 --- /dev/null +++ b/app/src/main/res/drawable/filter_active.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:drawable="@drawable/ic_filter_list_white_24dp" /> + <item + android:width="8dp" + android:height="8dp" + android:enterFadeDuration="@android:integer/config_shortAnimTime" + android:gravity="bottom|end"> + <shape android:shape="oval"> + <solid android:color="?attr/colorOnSurface" /> + </shape> + </item> +</layer-list> diff --git a/app/src/main/res/drawable/ic_filter_list_white_24dp.xml b/app/src/main/res/drawable/ic_filter_list_white_24dp.xml index 5c0bcb912..46fabab9c 100644 --- a/app/src/main/res/drawable/ic_filter_list_white_24dp.xml +++ b/app/src/main/res/drawable/ic_filter_list_white_24dp.xml @@ -1,5 +1,5 @@ <vector android:autoMirrored="true" android:height="24dp" - android:tint="#FFFFFF" android:viewportHeight="24.0" + android:tint="?attr/colorOnSurface" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> <path android:fillColor="#FF000000" android:pathData="M10,18h4v-2h-4v2zM3,6v2h18L21,6L3,6zM6,13h12v-2L6,11v2z"/> </vector> diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index d194335db..e05c9b9a1 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -13,181 +13,14 @@ android:layout_height="match_parent" tools:context=".ui.main.MainActivity"> - <androidx.swiperefreshlayout.widget.SwipeRefreshLayout - android:id="@+id/swipe_refresh_layout" + <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" + android:orientation="vertical" app:layout_behavior="@string/appbar_scrolling_view_behavior"> <LinearLayout android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="vertical"> - - <LinearLayout - android:id="@+id/info_box" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_gravity="center_horizontal|bottom" - android:background="@color/bg_info_box" - android:gravity="center" - android:padding="@dimen/spacer_1hx" - android:visibility="gone" - tools:visibility="visible"> - - <TextView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:drawablePadding="@dimen/spacer_1hx" - android:gravity="center" - android:paddingHorizontal="@dimen/spacer_1hx" - android:text="@string/info_box_maintenance_mode" - android:textColor="@color/grey600" - app:drawableStartCompat="@drawable/ic_info_outline_grey600_24dp" /> - - </LinearLayout> - - <TextView - android:id="@+id/info_box_version_not_supported" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:background="@color/danger" - android:drawablePadding="@dimen/spacer_1hx" - android:gravity="center" - android:paddingHorizontal="@dimen/spacer_2x" - android:paddingVertical="@dimen/spacer_1x" - android:text="@string/info_box_version_not_supported" - android:textColor="@android:color/white" - android:textSize="14sp" - android:visibility="gone" - app:drawableStartCompat="@drawable/ic_warning_white_24dp" - tools:visibility="visible" /> - - <it.niedermann.nextcloud.deck.ui.view.EmptyContentView - android:id="@+id/empty_content_view_boards" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:visibility="gone" - app:description="@string/add_a_new_board_using_the_button" - app:title="@string/no_boards" /> - - <it.niedermann.nextcloud.deck.ui.view.EmptyContentView - android:id="@+id/empty_content_view_stacks" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:visibility="gone" - app:description="@string/add_a_new_list_using_the_button" - app:title="@string/no_lists_yet" - tools:visibility="visible" /> - - <androidx.viewpager2.widget.ViewPager2 - android:id="@+id/viewPager" - android:layout_width="match_parent" - android:layout_height="match_parent" /> - </LinearLayout> - </androidx.swiperefreshlayout.widget.SwipeRefreshLayout> - - <com.google.android.material.appbar.AppBarLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:background="?attr/colorPrimary"> - - <com.google.android.material.card.MaterialCardView - android:id="@+id/toolbarCard" - android:layout_width="match_parent" - android:layout_height="?attr/actionBarSize" - android:layout_marginHorizontal="@dimen/spacer_2x" - android:layout_marginTop="@dimen/spacer_1x" - app:cardCornerRadius="@dimen/spacer_4x" - app:cardElevation="0dp" - app:strokeWidth="0dp"> - - <androidx.constraintlayout.widget.ConstraintLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:background="?attr/colorPrimary" - tools:title="Deck"> - - <com.google.android.material.appbar.MaterialToolbar - android:id="@+id/toolbar" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_gravity="center_vertical" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@id/enableSearch" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> - - <com.google.android.material.appbar.MaterialToolbar - android:id="@+id/searchToolbar" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_gravity="center_vertical" - android:visibility="gone" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@id/enableSearch" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - app:navigationIcon="@drawable/ic_arrow_back_white_24dp"> - - <EditText - android:id="@+id/filter_text" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:background="@android:color/transparent" - android:hint="@string/simple_filter" - android:inputType="text" - android:maxLines="1" - tools:hint="@string/app_name_short" /> - </com.google.android.material.appbar.MaterialToolbar> - - <ImageButton - android:id="@+id/enableSearch" - android:layout_width="wrap_content" - android:layout_height="0dp" - android:background="@null" - android:contentDescription="@string/simple_search" - android:paddingHorizontal="@dimen/spacer_1hx" - android:tooltipText="@string/simple_search" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@id/filterWrapper" - app:layout_constraintTop_toTopOf="parent" - app:srcCompat="@drawable/ic_baseline_search_24" - app:tint="?attr/colorAccent" - tools:targetApi="o" /> - - <ImageButton - android:id="@+id/filterWrapper" - android:layout_width="wrap_content" - android:layout_height="0dp" - android:background="?selectableItemBackgroundBorderless" - android:contentDescription="@string/simple_filter" - android:paddingHorizontal="@dimen/spacer_1hx" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@id/accountSwitcher" - app:layout_constraintTop_toTopOf="parent" - app:srcCompat="@drawable/filter" - app:tint="?attr/colorAccent" /> - - <ImageView - android:id="@+id/accountSwitcher" - android:layout_width="44dp" - android:layout_height="0dp" - android:layout_marginEnd="@dimen/spacer_1hx" - android:background="?attr/selectableItemBackgroundBorderless" - android:contentDescription="@string/choose_account" - android:padding="@dimen/spacer_1hx" - android:tooltipText="@string/choose_account" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="parent" - app:srcCompat="@drawable/ic_baseline_account_circle_24" - tools:targetApi="o" /> - </androidx.constraintlayout.widget.ConstraintLayout> - </com.google.android.material.card.MaterialCardView> - - <LinearLayout - android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> @@ -214,6 +47,93 @@ tools:ignore="UnusedAttribute" /> </LinearLayout> + <androidx.swiperefreshlayout.widget.SwipeRefreshLayout + android:id="@+id/swipe_refresh_layout" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <LinearLayout + android:id="@+id/info_box" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal|bottom" + android:background="@color/bg_info_box" + android:gravity="center" + android:padding="@dimen/spacer_1hx" + android:visibility="gone" + tools:visibility="visible"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:drawablePadding="@dimen/spacer_1hx" + android:gravity="center" + android:paddingHorizontal="@dimen/spacer_1hx" + android:text="@string/info_box_maintenance_mode" + android:textColor="@color/grey600" + app:drawableStartCompat="@drawable/ic_info_outline_grey600_24dp" /> + + </LinearLayout> + + <TextView + android:id="@+id/info_box_version_not_supported" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@color/danger" + android:drawablePadding="@dimen/spacer_1hx" + android:gravity="center" + android:paddingHorizontal="@dimen/spacer_2x" + android:paddingVertical="@dimen/spacer_1x" + android:text="@string/info_box_version_not_supported" + android:textColor="@android:color/white" + android:textSize="14sp" + android:visibility="gone" + app:drawableStartCompat="@drawable/ic_warning_white_24dp" + tools:visibility="visible" /> + + <it.niedermann.nextcloud.deck.ui.view.EmptyContentView + android:id="@+id/empty_content_view_boards" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:visibility="gone" + app:description="@string/add_a_new_board_using_the_button" + app:title="@string/no_boards" /> + + <it.niedermann.nextcloud.deck.ui.view.EmptyContentView + android:id="@+id/empty_content_view_stacks" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:visibility="gone" + app:description="@string/add_a_new_list_using_the_button" + app:title="@string/no_lists_yet" + tools:visibility="visible" /> + + <androidx.viewpager2.widget.ViewPager2 + android:id="@+id/viewPager" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + </LinearLayout> + </androidx.swiperefreshlayout.widget.SwipeRefreshLayout> + </LinearLayout> + + <com.google.android.material.appbar.AppBarLayout + android:id="@+id/appBarLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?attr/colorPrimary"> + + <com.google.android.material.search.SearchBar + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:menu="@menu/main_menu" + app:navigationIcon="@drawable/ic_arrow_back_white_24dp" /> + </com.google.android.material.appbar.AppBarLayout> <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton @@ -226,6 +146,41 @@ android:visibility="gone" app:icon="@drawable/ic_add_white_24dp" /> + <com.google.android.material.search.SearchView + android:id="@+id/search_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:hint="@string/simple_search" + app:layout_anchor="@id/toolbar" + app:useDrawerArrowDrawable="true"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/search_results" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + tools:itemCount="10" + tools:listitem="@layout/item_search_card" /> + + <it.niedermann.nextcloud.deck.ui.view.EmptyContentView + android:id="@+id/empty_content_view_search_no_term" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:visibility="gone" + app:description="@string/enter_search_term_description" + app:image="@drawable/ic_baseline_search_24" + app:title="@string/enter_search_term_title" /> + + <it.niedermann.nextcloud.deck.ui.view.EmptyContentView + android:id="@+id/empty_content_view_search_no_results" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:visibility="gone" + app:description="@string/no_search_results_description" + app:image="@drawable/ic_baseline_search_24" + app:title="@string/no_search_results_title" /> + </com.google.android.material.search.SearchView> + </androidx.coordinatorlayout.widget.CoordinatorLayout> <com.google.android.material.navigation.NavigationView diff --git a/app/src/main/res/layout/item_search_card.xml b/app/src/main/res/layout/item_search_card.xml new file mode 100644 index 000000000..9672259d1 --- /dev/null +++ b/app/src/main/res/layout/item_search_card.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout 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="wrap_content" + android:background="?attr/selectableItemBackground" + android:paddingHorizontal="@dimen/spacer_4x" + android:paddingVertical="@dimen/spacer_2x"> + + <ImageView + android:id="@+id/coverImages" + android:layout_width="@dimen/avatar_size" + android:layout_height="@dimen/avatar_size" + android:contentDescription="@null" + android:scaleType="centerCrop" + android:src="@null" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:src="@tools:sample/backgrounds/scenic" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/title" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@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_constraintStart_toEndOf="@id/coverImages" + app:layout_constraintTop_toTopOf="parent" + app:layout_goneMarginStart="0dp" + tools:text="@tools:sample/lorem" /> + + <com.google.android.material.textview.MaterialTextView + android:id="@+id/description" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@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_constraintStart_toEndOf="@id/coverImages" + app:layout_constraintTop_toBottomOf="@id/title" + app:layout_goneMarginStart="0dp" + tools:text="@tools:sample/lorem" /> +</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/item_search_stack.xml b/app/src/main/res/layout/item_search_stack.xml new file mode 100644 index 000000000..19e2b7da2 --- /dev/null +++ b/app/src/main/res/layout/item_search_stack.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<com.google.android.material.textview.MaterialTextView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingStart="@dimen/spacer_4x" + android:paddingTop="@dimen/spacer_4x" + android:paddingEnd="@dimen/spacer_4x" + android:paddingBottom="@dimen/spacer_2x" + android:textColor="?attr/colorAccent" + android:textSize="22sp" + android:textStyle="bold" + tools:text="@tools:sample/lorem" />
\ No newline at end of file diff --git a/app/src/main/res/menu/main_menu.xml b/app/src/main/res/menu/main_menu.xml new file mode 100644 index 000000000..7c0d51182 --- /dev/null +++ b/app/src/main/res/menu/main_menu.xml @@ -0,0 +1,20 @@ +<?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/filter" + android:icon="@drawable/ic_filter_list_white_24dp" + android:title="@string/simple_filter" + app:showAsAction="ifRoom" /> + <item + android:id="@+id/filter_active" + android:icon="@drawable/filter_active" + android:title="@string/simple_filter" + android:visible="false" + app:showAsAction="ifRoom" /> + <item + android:id="@+id/avatar" + android:icon="@drawable/ic_baseline_account_circle_24" + android:title="@string/choose_account" + app:showAsAction="ifRoom" /> +</menu> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e51f75722..aa4b10b15 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -355,4 +355,8 @@ <string name="show_error">Show error</string> <string name="push_notification_link_empty">Due to a known issue in the Deck web app we are unfortunately not able to display this card. For more information see: %1$s</string> <string name="push_notification_link_empty_link" translatable="false">https://github.com/nextcloud/deck/issues/3431</string> + <string name="enter_search_term_title">Enter search term</string> + <string name="enter_search_term_description">Enter search term to find cards in this board</string> + <string name="no_search_results_title">No search results</string> + <string name="no_search_results_description">We haven\'t found any result for the given search term</string> </resources> diff --git a/app/src/test/java/it/niedermann/nextcloud/deck/database/DataBaseAdapterTest.java b/app/src/test/java/it/niedermann/nextcloud/deck/database/DataBaseAdapterTest.java index 50c093ebf..8e1018b96 100644 --- a/app/src/test/java/it/niedermann/nextcloud/deck/database/DataBaseAdapterTest.java +++ b/app/src/test/java/it/niedermann/nextcloud/deck/database/DataBaseAdapterTest.java @@ -1,13 +1,17 @@ package it.niedermann.nextcloud.deck.database; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static java.lang.reflect.Modifier.isProtected; import static it.niedermann.nextcloud.deck.database.DeckDatabaseTestUtil.createAccount; import static it.niedermann.nextcloud.deck.database.DeckDatabaseTestUtil.createBoard; +import static it.niedermann.nextcloud.deck.database.DeckDatabaseTestUtil.createCard; +import static it.niedermann.nextcloud.deck.database.DeckDatabaseTestUtil.createStack; import static it.niedermann.nextcloud.deck.database.DeckDatabaseTestUtil.createUser; import android.content.Context; +import androidx.annotation.NonNull; import androidx.room.Room; import androidx.test.core.app.ApplicationProvider; @@ -25,8 +29,15 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; import java.util.concurrent.ExecutorService; +import it.niedermann.nextcloud.deck.TestUtil; +import it.niedermann.nextcloud.deck.model.Card; +import it.niedermann.nextcloud.deck.model.Stack; +import it.niedermann.nextcloud.deck.model.full.FullCard; import it.niedermann.nextcloud.deck.model.interfaces.IRemoteEntity; @RunWith(RobolectricTestRunner.class) @@ -111,4 +122,50 @@ public class DataBaseAdapterTest { assertEquals(leet + 1, args.get(1)); } + @Test + public void testSearchCards() throws InterruptedException { + final var account = createAccount(db.getAccountDao()); + final var user = createUser(db.getUserDao(), account); + final var board = createBoard(db.getBoardDao(), account, user); + + final var stack1 = createStack(db.getStackDao(), account, board); + final var stack2 = createStack(db.getStackDao(), account, board); + final var card1_1 = createCard(db.getCardDao(), account, stack1, "Foo", "Hello world"); + final var card1_2 = createCard(db.getCardDao(), account, stack1, "Bar", "Hello Bar"); + final var card1_3 = createCard(db.getCardDao(), account, stack2, "Baz", ""); + final var card2_1 = createCard(db.getCardDao(), account, stack2, "Qux", "Hello Foo"); + final var card2_2 = createCard(db.getCardDao(), account, stack2, "Lorem", "Ipsum"); + + var result = TestUtil.getOrAwaitValue(adapter.searchCards(account.getId(), board.getLocalId(), "Hello", 3)); + assertEquals(2, result.size()); + assertEquals(2, countCardsOf(result, stack1)); + assertEquals(1, countCardsOf(result, stack2)); + assertTrue(containsCard(result, stack1, card1_1)); + assertTrue(containsCard(result, stack1, card1_2)); + assertTrue(containsCard(result, stack2, card2_1)); + } + + private int countCardsOf(@NonNull Map<Stack, List<FullCard>> map, @NonNull Stack stackToFind) { + for (final var stack : map.keySet()) { + if (Objects.equals(stack.getLocalId(), stackToFind.getLocalId())) { + //noinspection ConstantConditions + return map.get(stack).size(); + } + } + throw new NoSuchElementException(); + } + + private boolean containsCard(@NonNull Map<Stack, List<FullCard>> map, @NonNull Stack stackToFind, @NonNull Card cardToFind) { + for (final var stack : map.keySet()) { + if (Objects.equals(stack.getLocalId(), stackToFind.getLocalId())) { + //noinspection ConstantConditions + for (final var fullCard : map.get(stack)) { + if (Objects.equals(fullCard.getLocalId(), cardToFind.getLocalId())) { + return true; + } + } + } + } + return false; + } } diff --git a/app/src/test/java/it/niedermann/nextcloud/deck/database/DeckDatabaseTestUtil.java b/app/src/test/java/it/niedermann/nextcloud/deck/database/DeckDatabaseTestUtil.java index 8da72d4c6..42e6f4d9d 100644 --- a/app/src/test/java/it/niedermann/nextcloud/deck/database/DeckDatabaseTestUtil.java +++ b/app/src/test/java/it/niedermann/nextcloud/deck/database/DeckDatabaseTestUtil.java @@ -62,10 +62,14 @@ public class DeckDatabaseTestUtil { } public static Card createCard(@NonNull CardDao dao, @NonNull Account account, @NonNull Stack stack) { + return createCard(dao, account, stack, randomString(15), randomString(50)); + } + + public static Card createCard(@NonNull CardDao dao, @NonNull Account account, @NonNull Stack stack, @NonNull String title, @NonNull String description) { final var cardToCreate = new Card(); cardToCreate.setAccountId(account.getId()); - cardToCreate.setTitle(randomString(15)); - cardToCreate.setDescription(randomString(50)); + cardToCreate.setTitle(title); + cardToCreate.setDescription(description); cardToCreate.setStackId(stack.getLocalId()); cardToCreate.setId(currentLong++); diff --git a/fastlane/metadata/android/en-US/changelogs/1021009.txt b/fastlane/metadata/android/en-US/changelogs/1021009.txt index 90074559e..4b758f405 100644 --- a/fastlane/metadata/android/en-US/changelogs/1021009.txt +++ b/fastlane/metadata/android/en-US/changelogs/1021009.txt @@ -1,3 +1,4 @@ +- 🔍 Implement new SearchBar - 🎨 Unify material theming with Nextcloud files app - ↔️ Improve 'Move Card' Dialog (#972) - 🐞 Fix collapsed input fields of dialogs (#1385) |