diff --git a/app/build.gradle b/app/build.gradle index 5668544c..bcf4c844 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -115,7 +115,7 @@ dependencies { implementation 'com.github.chrisvest:stormpot:2.4.2' implementation 'androidx.browser:browser:1.8.0' implementation 'com.google.android.flexbox:flexbox:3.0.0' - implementation('org.codeberg.gitnex:tea4j-autodeploy:a91978be6a') { + implementation('org.codeberg.gitnex:tea4j-autodeploy:268d5b9c96') { exclude module: 'org.apache.oltu.oauth2.common' } implementation 'io.github.amrdeveloper:codeview:1.3.9' diff --git a/app/src/main/java/org/mian/gitnex/adapters/DependencyAdapter.java b/app/src/main/java/org/mian/gitnex/adapters/DependencyAdapter.java new file mode 100644 index 00000000..7c350ca6 --- /dev/null +++ b/app/src/main/java/org/mian/gitnex/adapters/DependencyAdapter.java @@ -0,0 +1,81 @@ +package org.mian.gitnex.adapters; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import java.util.List; +import org.gitnex.tea4j.v2.models.Issue; +import org.mian.gitnex.databinding.ListIssueDependencyBinding; + +/** + * @author mmarif + */ +public class DependencyAdapter + extends RecyclerView.Adapter { + + private final List dependenciesList; + private final boolean showDeleteIcon; + private OnItemClickListener itemClickListener; + + public DependencyAdapter(List dependenciesList, boolean showDeleteIcon) { + this.dependenciesList = dependenciesList; + this.showDeleteIcon = showDeleteIcon; + } + + public void setOnItemClickListener(OnItemClickListener listener) { + this.itemClickListener = listener; + } + + @NonNull @Override + public DependencyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ListIssueDependencyBinding binding = + ListIssueDependencyBinding.inflate( + LayoutInflater.from(parent.getContext()), parent, false); + return new DependencyViewHolder(binding); + } + + @Override + public void onBindViewHolder(@NonNull DependencyViewHolder holder, int position) { + + Issue issue = dependenciesList.get(position); + holder.binding.dependencyTitle.setText(issue.getTitle()); + holder.binding.deleteDependency.setVisibility(showDeleteIcon ? View.VISIBLE : View.GONE); + + if (showDeleteIcon) { + holder.binding.deleteDependency.setOnClickListener( + v -> { + if (itemClickListener != null) { + itemClickListener.onItemClick(issue, position); + } + }); + holder.binding.cardView.setOnClickListener(null); + } else { + holder.binding.cardView.setOnClickListener( + v -> { + if (itemClickListener != null) { + itemClickListener.onItemClick(issue, position); + } + }); + } + } + + @Override + public int getItemCount() { + return dependenciesList.size(); + } + + public static class DependencyViewHolder extends RecyclerView.ViewHolder { + private final ListIssueDependencyBinding binding; + + DependencyViewHolder(ListIssueDependencyBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + } + + public interface OnItemClickListener { + void onItemClick(Issue issue, int position); + } +} diff --git a/app/src/main/java/org/mian/gitnex/fragments/BottomSheetIssueDependenciesFragment.java b/app/src/main/java/org/mian/gitnex/fragments/BottomSheetIssueDependenciesFragment.java new file mode 100644 index 00000000..c5c0f6b5 --- /dev/null +++ b/app/src/main/java/org/mian/gitnex/fragments/BottomSheetIssueDependenciesFragment.java @@ -0,0 +1,339 @@ +package org.mian.gitnex.fragments; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.LinearLayoutManager; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import org.gitnex.tea4j.v2.models.Issue; +import org.gitnex.tea4j.v2.models.IssueMeta; +import org.mian.gitnex.R; +import org.mian.gitnex.adapters.DependencyAdapter; +import org.mian.gitnex.clients.RetrofitClient; +import org.mian.gitnex.databinding.BottomSheetIssueDependenciesBinding; +import org.mian.gitnex.helpers.Toasty; +import org.mian.gitnex.helpers.contexts.IssueContext; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +/** + * @author mmarif + */ +public class BottomSheetIssueDependenciesFragment extends BottomSheetDialogFragment { + + private BottomSheetIssueDependenciesBinding binding; + private IssueContext issue; + private DependencyAdapter dependenciesAdapter; + private DependencyAdapter searchResultsAdapter; + private List dependenciesList; + private List searchResultsList; + + public static BottomSheetIssueDependenciesFragment newInstance(IssueContext issue) { + + BottomSheetIssueDependenciesFragment fragment = new BottomSheetIssueDependenciesFragment(); + Bundle args = new Bundle(); + args.putSerializable(IssueContext.INTENT_EXTRA, issue); + fragment.setArguments(args); + return fragment; + } + + @Nullable @Override + public View onCreateView( + @NonNull LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + + binding = BottomSheetIssueDependenciesBinding.inflate(inflater, container, false); + + if (getArguments() != null) { + issue = (IssueContext) getArguments().getSerializable(IssueContext.INTENT_EXTRA); + } + if (issue == null) { + throw new IllegalStateException("IssueContext is required"); + } + + dependenciesList = new ArrayList<>(); + dependenciesAdapter = new DependencyAdapter(dependenciesList, true); + binding.dependenciesRecyclerView.setLayoutManager( + new LinearLayoutManager(requireContext())); + binding.dependenciesRecyclerView.setAdapter(dependenciesAdapter); + dependenciesAdapter.setOnItemClickListener(this::deleteDependency); + + searchResultsList = new ArrayList<>(); + searchResultsAdapter = new DependencyAdapter(searchResultsList, false); + binding.searchResultsRecyclerView.setLayoutManager( + new LinearLayoutManager(requireContext())); + binding.searchResultsRecyclerView.setAdapter(searchResultsAdapter); + searchResultsAdapter.setOnItemClickListener(this::addDependency); + + binding.searchInputLayout.setEndIconOnClickListener( + v -> { + String query = + Objects.requireNonNull(binding.searchInput.getText()).toString().trim(); + if (!query.isEmpty()) { + searchIssues(query); + } else { + clearSearchResults(); + } + }); + + loadDependencies(); + + return binding.getRoot(); + } + + private void loadDependencies() { + Call> call = + RetrofitClient.getApiInterface(requireContext()) + .issueListIssueDependencies( + issue.getRepository().getOwner(), + issue.getRepository().getName(), + String.valueOf(issue.getIssueIndex()), + 1, + 10); + + call.enqueue( + new Callback<>() { + @Override + public void onResponse( + @NonNull Call> call, + @NonNull Response> response) { + if (response.isSuccessful() && response.body() != null) { + List newDependencies = response.body(); + int oldSize = dependenciesList.size(); + + dependenciesList.clear(); + if (oldSize > 0) { + dependenciesAdapter.notifyItemRangeRemoved(0, oldSize); + } + + dependenciesList.addAll(newDependencies); + if (!newDependencies.isEmpty()) { + dependenciesAdapter.notifyItemRangeInserted( + 0, newDependencies.size()); + } + + if (dependenciesList.isEmpty()) { + binding.dependenciesRecyclerView.setVisibility(View.GONE); + binding.noDependenciesText.setVisibility(View.VISIBLE); + } else { + binding.dependenciesRecyclerView.setVisibility(View.VISIBLE); + binding.noDependenciesText.setVisibility(View.GONE); + } + } else { + int oldSize = dependenciesList.size(); + dependenciesList.clear(); + if (oldSize > 0) { + dependenciesAdapter.notifyItemRangeRemoved(0, oldSize); + } + binding.dependenciesRecyclerView.setVisibility(View.GONE); + binding.noDependenciesText.setVisibility(View.VISIBLE); + } + } + + @Override + public void onFailure(@NonNull Call> call, @NonNull Throwable t) { + int oldSize = dependenciesList.size(); + dependenciesList.clear(); + if (oldSize > 0) { + dependenciesAdapter.notifyItemRangeRemoved(0, oldSize); + } + binding.dependenciesRecyclerView.setVisibility(View.GONE); + binding.noDependenciesText.setVisibility(View.VISIBLE); + } + }); + } + + private void deleteDependency(Issue dependency, int position) { + IssueMeta meta = new IssueMeta(); + meta.setOwner(issue.getRepository().getOwner()); + meta.setRepo(issue.getRepository().getName()); + meta.setIndex(dependency.getId()); + + Call call = + RetrofitClient.getApiInterface(requireContext()) + .customIssueRemoveIssueDependencies( + issue.getRepository().getOwner(), + issue.getRepository().getName(), + String.valueOf(issue.getIssue().getId()), + meta); + + call.enqueue( + new Callback<>() { + @Override + public void onResponse( + @NonNull Call call, @NonNull Response response) { + if (response.isSuccessful()) { + dependenciesList.remove(position); + dependenciesAdapter.notifyItemRemoved(position); + dependenciesAdapter.notifyItemRangeChanged( + position, dependenciesList.size()); + + if (dependenciesList.isEmpty()) { + binding.dependenciesRecyclerView.setVisibility(View.GONE); + binding.noDependenciesText.setVisibility(View.VISIBLE); + } + Toasty.success( + requireContext(), getString(R.string.dependency_removed)); + } else { + Toasty.error( + requireContext(), + getString(R.string.dependency_removal_failed)); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + Toasty.error( + requireContext(), getString(R.string.genericServerResponseError)); + } + }); + } + + private void searchIssues(String query) { + Call> call = + RetrofitClient.getApiInterface(requireContext()) + .issueListIssues( + issue.getRepository().getOwner(), + issue.getRepository().getName(), + "open", + null, + query, + null, + null, + null, + null, + null, + null, + null, + 1, + 3); + + call.enqueue( + new Callback<>() { + @Override + public void onResponse( + @NonNull Call> call, + @NonNull Response> response) { + if (response.isSuccessful() && response.body() != null) { + int oldSize = searchResultsList.size(); + searchResultsList.clear(); + if (oldSize > 0) { + searchResultsAdapter.notifyItemRangeRemoved(0, oldSize); + } + + List results = response.body(); + long currentIssueId = issue.getIssue().getId(); + List dependencyIds = new ArrayList<>(); + for (Issue dep : dependenciesList) { + dependencyIds.add(dep.getId()); + } + + for (Issue result : results) { + if (result.getId() != currentIssueId + && !dependencyIds.contains(result.getId())) { + searchResultsList.add(result); + } + } + + if (searchResultsList.isEmpty()) { + binding.searchResultsRecyclerView.setVisibility(View.GONE); + Toasty.info( + requireContext(), + getString(R.string.no_dependency_search_results)); + } else { + searchResultsAdapter.notifyItemRangeInserted( + 0, searchResultsList.size()); + binding.searchResultsRecyclerView.setVisibility(View.VISIBLE); + } + } else { + int oldSize = searchResultsList.size(); + if (oldSize > 0) { + searchResultsList.clear(); + searchResultsAdapter.notifyItemRangeRemoved(0, oldSize); + } + binding.searchResultsRecyclerView.setVisibility(View.GONE); + Toasty.error(requireContext(), getString(R.string.search_failed)); + } + } + + @Override + public void onFailure(@NonNull Call> call, @NonNull Throwable t) { + int oldSize = searchResultsList.size(); + if (oldSize > 0) { + searchResultsList.clear(); + searchResultsAdapter.notifyItemRangeRemoved(0, oldSize); + } + binding.searchResultsRecyclerView.setVisibility(View.GONE); + Toasty.error( + requireContext(), getString(R.string.genericServerResponseError)); + } + }); + } + + private void addDependency(Issue dependency, int position) { + IssueMeta meta = new IssueMeta(); + meta.setOwner(issue.getRepository().getOwner()); + meta.setRepo(issue.getRepository().getName()); + meta.setIndex(dependency.getId()); + + Call call = + RetrofitClient.getApiInterface(requireContext()) + .issueCreateIssueDependencies( + issue.getRepository().getOwner(), + issue.getRepository().getName(), + String.valueOf(issue.getIssue().getId()), + meta); + + call.enqueue( + new Callback<>() { + @Override + public void onResponse( + @NonNull Call call, @NonNull Response response) { + if (response.isSuccessful()) { + binding.searchInput.setText(""); + int oldSize = searchResultsList.size(); + if (oldSize > 0) { + searchResultsList.clear(); + searchResultsAdapter.notifyItemRangeRemoved(0, oldSize); + } + binding.searchResultsRecyclerView.setVisibility(View.GONE); + + loadDependencies(); + Toasty.success(requireContext(), getString(R.string.dependency_added)); + } else { + Toasty.error( + requireContext(), getString(R.string.dependency_add_failed)); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + Toasty.error( + requireContext(), getString(R.string.genericServerResponseError)); + } + }); + } + + private void clearSearchResults() { + int oldSize = searchResultsList.size(); + if (oldSize > 0) { + searchResultsList.clear(); + searchResultsAdapter.notifyItemRangeRemoved(0, oldSize); + } + binding.searchResultsRecyclerView.setVisibility(View.GONE); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } +} diff --git a/app/src/main/java/org/mian/gitnex/fragments/BottomSheetSingleIssueFragment.java b/app/src/main/java/org/mian/gitnex/fragments/BottomSheetSingleIssueFragment.java index c02e3db3..2ae2f6b0 100644 --- a/app/src/main/java/org/mian/gitnex/fragments/BottomSheetSingleIssueFragment.java +++ b/app/src/main/java/org/mian/gitnex/fragments/BottomSheetSingleIssueFragment.java @@ -34,7 +34,6 @@ public class BottomSheetSingleIssueFragment extends BottomSheetDialogFragment { private final IssueContext issue; private final String issueCreator; private BottomSheetListener bmListener; - private boolean issuePinStatus = false; public BottomSheetSingleIssueFragment(IssueContext issue, String username) { this.issue = issue; @@ -244,6 +243,15 @@ public class BottomSheetSingleIssueFragment extends BottomSheetDialogFragment { }); } + binding.manageDependencies.setOnClickListener( + v -> { + BottomSheetIssueDependenciesFragment dependenciesSheet = + BottomSheetIssueDependenciesFragment.newInstance(issue); + dependenciesSheet.show( + getParentFragmentManager(), "issueDependenciesBottomSheet"); + dismiss(); + }); + binding.subscribeIssue.setOnClickListener( subscribeToIssue -> { IssueActions.subscribe(ctx, issue); @@ -277,6 +285,7 @@ public class BottomSheetSingleIssueFragment extends BottomSheetDialogFragment { binding.bottomSheetHeaderFrame.setVisibility(View.GONE); binding.mergePullRequest.setVisibility(View.GONE); binding.updatePullRequest.setVisibility(View.GONE); + binding.manageDependencies.setVisibility(View.GONE); if (issue.getIssueType().equalsIgnoreCase("issue")) { binding.issuePrDivider.setVisibility(View.GONE); } @@ -287,6 +296,7 @@ public class BottomSheetSingleIssueFragment extends BottomSheetDialogFragment { } else { binding.addRemoveAssignees.setVisibility(View.GONE); binding.editLabels.setVisibility(View.GONE); + binding.manageDependencies.setVisibility(View.GONE); } binding.pinIssue.setOnClickListener( diff --git a/app/src/main/res/drawable/ic_dependencies.xml b/app/src/main/res/drawable/ic_dependencies.xml new file mode 100644 index 00000000..8b3a43c5 --- /dev/null +++ b/app/src/main/res/drawable/ic_dependencies.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_issue_detail.xml b/app/src/main/res/layout/activity_issue_detail.xml index 0a1f5da3..b9323fa6 100644 --- a/app/src/main/res/layout/activity_issue_detail.xml +++ b/app/src/main/res/layout/activity_issue_detail.xml @@ -471,6 +471,7 @@ android:id="@+id/divider_info" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:background="?attr/materialCardBackgroundColor" android:text="@string/timeline" android:paddingTop="@dimen/dimen4dp" android:paddingBottom="@dimen/dimen4dp" diff --git a/app/src/main/res/layout/bottom_sheet_issue_dependencies.xml b/app/src/main/res/layout/bottom_sheet_issue_dependencies.xml new file mode 100644 index 00000000..cb29ae34 --- /dev/null +++ b/app/src/main/res/layout/bottom_sheet_issue_dependencies.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/bottom_sheet_single_issue.xml b/app/src/main/res/layout/bottom_sheet_single_issue.xml index f1e8fbe4..eefa779d 100644 --- a/app/src/main/res/layout/bottom_sheet_single_issue.xml +++ b/app/src/main/res/layout/bottom_sheet_single_issue.xml @@ -163,6 +163,19 @@ app:drawableTopCompat="@drawable/ic_tag" app:layout_alignSelf="flex_start" /> + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ae4c06b3..8edb7af0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -947,4 +947,13 @@ No matches found %1$d contributions on %2$s I\'m mentioned + + Dependencies + No dependencies set + Dependency removed + Failed to remove dependency + Dependency added + Failed to add dependency + Search failed + No matching results found