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 /app/src/main/java | |
parent | 41ae988679aa6f84dfb56479eabd9e1c1209405c (diff) |
feat: Implement new SearchBar
Signed-off-by: Stefan Niedermann <info@niedermann.it>
Diffstat (limited to 'app/src/main/java')
28 files changed, 572 insertions, 103 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 |