From 64600ceb046a73da4800dd7f5df7ea70eaeb1caf Mon Sep 17 00:00:00 2001 From: Ammar Githam Date: Wed, 7 Jul 2021 16:29:44 +0900 Subject: [PATCH] Convert UserSearchFragment to kotlin and fix UserSearchMode --- .../fragments/UserSearchFragment.java | 312 ------------------ .../fragments/UserSearchFragment.kt | 252 ++++++++++++++ .../instagrabber/fragments/UserSearchMode.kt | 2 +- .../viewmodels/UserSearchViewModel.java | 2 +- 4 files changed, 254 insertions(+), 314 deletions(-) delete mode 100644 app/src/main/java/awais/instagrabber/fragments/UserSearchFragment.java create mode 100644 app/src/main/java/awais/instagrabber/fragments/UserSearchFragment.kt diff --git a/app/src/main/java/awais/instagrabber/fragments/UserSearchFragment.java b/app/src/main/java/awais/instagrabber/fragments/UserSearchFragment.java deleted file mode 100644 index 417b7237..00000000 --- a/app/src/main/java/awais/instagrabber/fragments/UserSearchFragment.java +++ /dev/null @@ -1,312 +0,0 @@ -package awais.instagrabber.fragments; - -import android.content.Context; -import android.os.Bundle; -import android.view.KeyEvent; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBar; -import androidx.core.util.Pair; -import androidx.fragment.app.Fragment; -import androidx.lifecycle.SavedStateHandle; -import androidx.lifecycle.ViewModelProvider; -import androidx.navigation.NavBackStackEntry; -import androidx.navigation.NavController; -import androidx.navigation.fragment.NavHostFragment; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.transition.TransitionManager; - -import com.google.android.material.chip.Chip; -import com.google.android.material.snackbar.Snackbar; - -import java.util.Objects; -import java.util.Set; - -import awais.instagrabber.activities.MainActivity; -import awais.instagrabber.adapters.UserSearchResultsAdapter; -import awais.instagrabber.customviews.helpers.TextWatcherAdapter; -import awais.instagrabber.databinding.FragmentUserSearchBinding; -import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; -import awais.instagrabber.utils.TextUtils; -import awais.instagrabber.utils.Utils; -import awais.instagrabber.utils.ViewUtils; -import awais.instagrabber.viewmodels.UserSearchViewModel; - -public class UserSearchFragment extends Fragment { - private static final String TAG = UserSearchFragment.class.getSimpleName(); - - private FragmentUserSearchBinding binding; - private UserSearchViewModel viewModel; - private UserSearchResultsAdapter resultsAdapter; - private int paddingOffset; - - private final int windowWidth = Utils.displayMetrics.widthPixels; - private final int minInputWidth = Utils.convertDpToPx(50); - private String actionLabel; - private String title; - private boolean multiple; - - @Override - public void onCreate(@Nullable final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @Nullable - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { - binding = FragmentUserSearchBinding.inflate(inflater, container, false); - viewModel = new ViewModelProvider(this).get(UserSearchViewModel.class); - return binding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { - paddingOffset = binding.search.getPaddingStart() + binding.search.getPaddingEnd() + binding.group - .getPaddingStart() + binding.group.getPaddingEnd() + binding.group.getChipSpacingHorizontal(); - init(); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - viewModel.cleanup(); - } - - private void init() { - final Bundle arguments = getArguments(); - if (arguments != null) { - final UserSearchFragmentArgs fragmentArgs = UserSearchFragmentArgs.fromBundle(arguments); - actionLabel = fragmentArgs.getActionLabel(); - title = fragmentArgs.getTitle(); - multiple = fragmentArgs.getMultiple(); - viewModel.setHideThreadIds(fragmentArgs.getHideThreadIds()); - viewModel.setHideUserIds(fragmentArgs.getHideUserIds()); - viewModel.setSearchMode(fragmentArgs.getSearchMode()); - viewModel.setShowGroups(fragmentArgs.getShowGroups()); - } - setupTitles(); - setupInput(); - setupResults(); - setupObservers(); - // show cached results - viewModel.showCachedResults(); - } - - private void setupTitles() { - if (!TextUtils.isEmpty(actionLabel)) { - binding.done.setText(actionLabel); - } - if (!TextUtils.isEmpty(title)) { - final MainActivity activity = (MainActivity) getActivity(); - if (activity != null) { - final ActionBar actionBar = activity.getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(title); - } - } - } - } - - private void setupResults() { - final Context context = getContext(); - if (context == null) return; - binding.results.setLayoutManager(new LinearLayoutManager(context)); - resultsAdapter = new UserSearchResultsAdapter(multiple, (position, recipient, selected) -> { - if (!multiple) { - final NavController navController = NavHostFragment.findNavController(this); - if (!setResult(navController, recipient)) return; - navController.navigateUp(); - return; - } - viewModel.setSelectedRecipient(recipient, !selected); - resultsAdapter.setSelectedRecipient(recipient, !selected); - if (!selected) { - createChip(recipient); - return; - } - final View chip = findChip(recipient); - if (chip == null) return; - removeChipFromGroup(chip); - }); - binding.results.setAdapter(resultsAdapter); - binding.done.setOnClickListener(v -> { - final NavController navController = NavHostFragment.findNavController(this); - if (!setResult(navController, viewModel.getSelectedRecipients())) return; - navController.navigateUp(); - }); - } - - private boolean setResult(@NonNull final NavController navController, final RankedRecipient rankedRecipient) { - final NavBackStackEntry navBackStackEntry = navController.getPreviousBackStackEntry(); - if (navBackStackEntry == null) return false; - final SavedStateHandle savedStateHandle = navBackStackEntry.getSavedStateHandle(); - savedStateHandle.set("result", rankedRecipient); - return true; - } - - private boolean setResult(@NonNull final NavController navController, final Set rankedRecipients) { - final NavBackStackEntry navBackStackEntry = navController.getPreviousBackStackEntry(); - if (navBackStackEntry == null) return false; - final SavedStateHandle savedStateHandle = navBackStackEntry.getSavedStateHandle(); - savedStateHandle.set("result", rankedRecipients); - return true; - } - - private void setupInput() { - binding.search.addTextChangedListener(new TextWatcherAdapter() { - @Override - public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { - // if (TextUtils.isEmpty(s)) { - // viewModel.cancelSearch(); - // viewModel.clearResults(); - // return; - // } - viewModel.search(s == null ? null : s.toString().trim()); - } - }); - binding.search.setOnKeyListener((v, keyCode, event) -> { - if (event != null && event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_DEL) { - final View chip = getLastChip(); - if (chip == null) return false; - removeChip(chip); - } - return false; - }); - binding.group.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() { - @Override - public void onChildViewAdded(final View parent, final View child) {} - - @Override - public void onChildViewRemoved(final View parent, final View child) { - binding.group.post(() -> { - TransitionManager.beginDelayedTransition(binding.getRoot()); - calculateInputWidth(0); - }); - } - }); - - } - - private void setupObservers() { - viewModel.getRecipients().observe(getViewLifecycleOwner(), results -> { - if (results == null) return; - switch (results.status) { - case SUCCESS: - if (results.data != null) { - resultsAdapter.submitList(results.data); - } - break; - case ERROR: - if (results.message != null) { - Snackbar.make(binding.getRoot(), results.message, Snackbar.LENGTH_LONG).show(); - } - if (results.resId != 0) { - Snackbar.make(binding.getRoot(), results.resId, Snackbar.LENGTH_LONG).show(); - } - if (results.data != null) { - resultsAdapter.submitList(results.data); - } - break; - case LOADING: - //noinspection DuplicateBranchesInSwitch - if (results.data != null) { - resultsAdapter.submitList(results.data); - } - break; - } - }); - viewModel.showAction().observe(getViewLifecycleOwner(), showAction -> binding.done.setVisibility(showAction ? View.VISIBLE : View.GONE)); - } - - private void createChip(final RankedRecipient recipient) { - final Context context = getContext(); - if (context == null) return; - final Chip chip = new Chip(context); - chip.setTag(recipient); - chip.setText(getRecipientText(recipient)); - chip.setCloseIconVisible(true); - chip.setOnCloseIconClickListener(v -> removeChip(chip)); - binding.group.post(() -> { - final Pair measure = ViewUtils.measure(chip, binding.group); - TransitionManager.beginDelayedTransition(binding.getRoot()); - calculateInputWidth(measure.second != null ? measure.second : 0); - binding.group.addView(chip, binding.group.getChildCount() - 1); - }); - } - - private String getRecipientText(final RankedRecipient recipient) { - if (recipient == null) return null; - if (recipient.getUser() != null) { - return recipient.getUser().getFullName(); - } - if (recipient.getThread() != null) { - return recipient.getThread().getThreadTitle(); - } - return null; - } - - private void removeChip(@NonNull final View chip) { - final RankedRecipient recipient = (RankedRecipient) chip.getTag(); - if (recipient == null) return; - viewModel.setSelectedRecipient(recipient, false); - resultsAdapter.setSelectedRecipient(recipient, false); - removeChipFromGroup(chip); - } - - private View findChip(final RankedRecipient recipient) { - if (recipient == null || recipient.getUser() == null && recipient.getThread() == null) return null; - boolean isUser = recipient.getUser() != null; - final int childCount = binding.group.getChildCount(); - if (childCount == 0) return null; - for (int i = childCount - 1; i >= 0; i--) { - final View child = binding.group.getChildAt(i); - if (child == null) continue; - final RankedRecipient tag = (RankedRecipient) child.getTag(); - if (tag == null || isUser && tag.getUser() == null || !isUser && tag.getThread() == null) continue; - if ((isUser && tag.getUser().getPk() == recipient.getUser().getPk()) - || (!isUser && Objects.equals(tag.getThread().getThreadId(), recipient.getThread().getThreadId()))) { - return child; - } - } - return null; - } - - private void removeChipFromGroup(final View chip) { - binding.group.post(() -> { - TransitionManager.beginDelayedTransition(binding.getRoot()); - binding.group.removeView(chip); - }); - } - - private void calculateInputWidth(final int newChipWidth) { - final View lastChip = getLastChip(); - int lastRight = lastChip != null ? lastChip.getRight() : 0; - final int remainingSpaceInRow = windowWidth - lastRight; - if (remainingSpaceInRow < newChipWidth) { - // next chip will go to the next row, so assume no chips present - lastRight = 0; - } - final int newRight = lastRight + newChipWidth; - final int newInputWidth = windowWidth - newRight - paddingOffset; - binding.search.getLayoutParams().width = newInputWidth < minInputWidth ? windowWidth : newInputWidth; - binding.search.requestLayout(); - } - - private View getLastChip() { - final int childCount = binding.group.getChildCount(); - if (childCount == 0) { - return null; - } - for (int i = childCount - 1; i >= 0; i--) { - final View child = binding.group.getChildAt(i); - if (child instanceof Chip) { - return child; - } - } - return null; - } -} diff --git a/app/src/main/java/awais/instagrabber/fragments/UserSearchFragment.kt b/app/src/main/java/awais/instagrabber/fragments/UserSearchFragment.kt new file mode 100644 index 00000000..a72be70a --- /dev/null +++ b/app/src/main/java/awais/instagrabber/fragments/UserSearchFragment.kt @@ -0,0 +1,252 @@ +package awais.instagrabber.fragments + +import android.os.Bundle +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.OnHierarchyChangeListener +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.navigation.NavController +import androidx.navigation.fragment.NavHostFragment +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.transition.TransitionManager +import awais.instagrabber.activities.MainActivity +import awais.instagrabber.adapters.UserSearchResultsAdapter +import awais.instagrabber.customviews.helpers.TextWatcherAdapter +import awais.instagrabber.databinding.FragmentUserSearchBinding +import awais.instagrabber.models.Resource +import awais.instagrabber.repositories.responses.directmessages.RankedRecipient +import awais.instagrabber.utils.Utils +import awais.instagrabber.utils.extensions.trimAll +import awais.instagrabber.utils.measure +import awais.instagrabber.viewmodels.UserSearchViewModel +import com.google.android.material.chip.Chip +import com.google.android.material.snackbar.Snackbar + +class UserSearchFragment : Fragment() { + + private lateinit var binding: FragmentUserSearchBinding + + private var resultsAdapter: UserSearchResultsAdapter? = null + private var paddingOffset = 0 + private var actionLabel: String? = null + private var title: String? = null + private var multiple = false + + private val viewModel: UserSearchViewModel by viewModels() + private val windowWidth = Utils.displayMetrics.widthPixels + private val minInputWidth = Utils.convertDpToPx(50f) + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + binding = FragmentUserSearchBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + paddingOffset = with(binding) { + search.paddingStart + search.paddingEnd + group.paddingStart + group.paddingEnd + group.chipSpacingHorizontal + } + init() + } + + override fun onDestroyView() { + super.onDestroyView() + viewModel.cleanup() + } + + private fun init() { + val arguments = arguments + if (arguments != null) { + val fragmentArgs = UserSearchFragmentArgs.fromBundle(arguments) + actionLabel = fragmentArgs.actionLabel + title = fragmentArgs.title + multiple = fragmentArgs.multiple + viewModel.setHideThreadIds(fragmentArgs.hideThreadIds) + viewModel.setHideUserIds(fragmentArgs.hideUserIds) + viewModel.setSearchMode(fragmentArgs.searchMode) + viewModel.setShowGroups(fragmentArgs.showGroups) + } + setupTitles() + setupInput() + setupResults() + setupObservers() + // show cached results + viewModel.showCachedResults() + } + + private fun setupTitles() { + if (!actionLabel.isNullOrBlank()) { + binding.done.text = actionLabel + } + if (title.isNullOrBlank()) return + (activity as MainActivity?)?.supportActionBar?.title = title + } + + private fun setupResults() { + val context = context ?: return + binding.results.layoutManager = LinearLayoutManager(context) + resultsAdapter = UserSearchResultsAdapter(multiple) { _: Int, recipient: RankedRecipient, selected: Boolean -> + if (!multiple) { + val navController = NavHostFragment.findNavController(this) + if (!setResult(navController, recipient)) return@UserSearchResultsAdapter + navController.navigateUp() + return@UserSearchResultsAdapter + } + viewModel.setSelectedRecipient(recipient, !selected) + resultsAdapter?.setSelectedRecipient(recipient, !selected) + if (!selected) { + createChip(recipient) + return@UserSearchResultsAdapter + } + val chip = findChip(recipient) ?: return@UserSearchResultsAdapter + removeChipFromGroup(chip) + } + binding.results.adapter = resultsAdapter + binding.done.setOnClickListener { + val navController = NavHostFragment.findNavController(this) + if (!setResult(navController, viewModel.selectedRecipients)) return@setOnClickListener + navController.navigateUp() + } + } + + private fun setResult(navController: NavController, rankedRecipient: RankedRecipient): Boolean { + navController.previousBackStackEntry?.savedStateHandle?.set("result", rankedRecipient) ?: return false + return true + } + + private fun setResult(navController: NavController, rankedRecipients: Set): Boolean { + navController.previousBackStackEntry?.savedStateHandle?.set("result", rankedRecipients) ?: return false + return true + } + + private fun setupInput() { + binding.search.addTextChangedListener(object : TextWatcherAdapter() { + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + viewModel.search(s.toString().trimAll()) + } + }) + binding.search.setOnKeyListener { _: View?, _: Int, event: KeyEvent? -> + if (event != null && event.action == KeyEvent.ACTION_DOWN && event.keyCode == KeyEvent.KEYCODE_DEL) { + val chip = lastChip ?: return@setOnKeyListener false + removeChip(chip) + } + false + } + binding.group.setOnHierarchyChangeListener(object : OnHierarchyChangeListener { + override fun onChildViewAdded(parent: View, child: View) {} + override fun onChildViewRemoved(parent: View, child: View) { + binding.group.post { + TransitionManager.beginDelayedTransition(binding.root) + calculateInputWidth(0) + } + } + }) + } + + private fun setupObservers() { + viewModel.recipients.observe(viewLifecycleOwner) { + if (it == null) return@observe + when (it.status) { + Resource.Status.SUCCESS -> if (it.data != null) { + resultsAdapter?.submitList(it.data) + } + Resource.Status.ERROR -> { + if (it.message != null) { + Snackbar.make(binding.root, it.message, Snackbar.LENGTH_LONG).show() + } + if (it.resId != 0) { + Snackbar.make(binding.root, it.resId, Snackbar.LENGTH_LONG).show() + } + if (it.data != null) { + resultsAdapter?.submitList(it.data) + } + } + Resource.Status.LOADING -> if (it.data != null) { + resultsAdapter?.submitList(it.data) + } + } + } + viewModel.showAction().observe(viewLifecycleOwner) { binding.done.visibility = if (it) View.VISIBLE else View.GONE } + } + + private fun createChip(recipient: RankedRecipient) { + val context = context ?: return + val chip = Chip(context).apply { + tag = recipient + text = getRecipientText(recipient) + isCloseIconVisible = true + setOnCloseIconClickListener { removeChip(this) } + } + binding.group.post { + val measure = measure(chip, binding.group) + TransitionManager.beginDelayedTransition(binding.root) + calculateInputWidth(if (measure.second != null) measure.second else 0) + binding.group.addView(chip, binding.group.childCount - 1) + } + } + + private fun getRecipientText(recipient: RankedRecipient?): String? = when { + recipient == null -> null + recipient.user != null -> recipient.user.fullName + recipient.thread != null -> recipient.thread.threadTitle + else -> null + } + + private fun removeChip(chip: View) { + val recipient = chip.tag as RankedRecipient + viewModel.setSelectedRecipient(recipient, false) + resultsAdapter?.setSelectedRecipient(recipient, false) + removeChipFromGroup(chip) + } + + private fun findChip(recipient: RankedRecipient?): View? { + if (recipient == null || recipient.user == null && recipient.thread == null) return null + val isUser = recipient.user != null + val childCount = binding.group.childCount + if (childCount == 0) return null + for (i in childCount - 1 downTo 0) { + val child = binding.group.getChildAt(i) ?: continue + val tag = child.tag as RankedRecipient + if (isUser && tag.user == null || !isUser && tag.thread == null) continue + if (isUser && tag.user?.pk == recipient.user?.pk || !isUser && tag.thread?.threadId == recipient.thread?.threadId) { + return child + } + } + return null + } + + private fun removeChipFromGroup(chip: View) { + binding.group.post { + TransitionManager.beginDelayedTransition(binding.root) + binding.group.removeView(chip) + } + } + + private fun calculateInputWidth(newChipWidth: Int) { + var lastRight = lastChip?.right ?: 0 + val remainingSpaceInRow = windowWidth - lastRight + if (remainingSpaceInRow < newChipWidth) { + // next chip will go to the next row, so assume no chips present + lastRight = 0 + } + val newRight = lastRight + newChipWidth + val newInputWidth = windowWidth - newRight - paddingOffset + binding.search.layoutParams.width = if (newInputWidth < minInputWidth) windowWidth else newInputWidth + binding.search.requestLayout() + } + + private val lastChip: View? + get() { + val childCount = binding.group.childCount + if (childCount == 0) return null + for (i in childCount - 1 downTo 0) { + val child = binding.group.getChildAt(i) + if (child is Chip) { + return child + } + } + return null + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/fragments/UserSearchMode.kt b/app/src/main/java/awais/instagrabber/fragments/UserSearchMode.kt index 8d0da314..fc0e9191 100644 --- a/app/src/main/java/awais/instagrabber/fragments/UserSearchMode.kt +++ b/app/src/main/java/awais/instagrabber/fragments/UserSearchMode.kt @@ -1,6 +1,6 @@ package awais.instagrabber.fragments -enum class UserSearchMode(name: String) { +enum class UserSearchMode(val mode: String) { USER_SEARCH("user_name"), RAVEN("raven"), RESHARE("reshare"); diff --git a/app/src/main/java/awais/instagrabber/viewmodels/UserSearchViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/UserSearchViewModel.java index 63161b94..c9bac545 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/UserSearchViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/UserSearchViewModel.java @@ -192,7 +192,7 @@ public class UserSearchViewModel extends ViewModel { private void rankedRecipientSearch() { directMessagesRepository.rankedRecipients( - searchMode.name(), + searchMode.getMode(), showGroups, currentQuery, CoroutineUtilsKt.getContinuation((response, throwable) -> {