Dependencies for issues and pr (view/add/remove) (#1420)

Closes #878

Reviewed-on: https://codeberg.org/gitnex/GitNex/pulls/1420
Co-authored-by: M M Arif <mmarif@swatian.com>
Co-committed-by: M M Arif <mmarif@swatian.com>
This commit is contained in:
M M Arif
2025-03-17 15:29:08 +00:00
committed by M M Arif
parent c69699b9a8
commit 52717c649b
10 changed files with 677 additions and 2 deletions

View File

@@ -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'

View File

@@ -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<DependencyAdapter.DependencyViewHolder> {
private final List<Issue> dependenciesList;
private final boolean showDeleteIcon;
private OnItemClickListener itemClickListener;
public DependencyAdapter(List<Issue> 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);
}
}

View File

@@ -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<Issue> dependenciesList;
private List<Issue> 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<List<Issue>> 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<List<Issue>> call,
@NonNull Response<List<Issue>> response) {
if (response.isSuccessful() && response.body() != null) {
List<Issue> 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<List<Issue>> 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<Void> 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<Void> call, @NonNull Response<Void> 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<Void> call, @NonNull Throwable t) {
Toasty.error(
requireContext(), getString(R.string.genericServerResponseError));
}
});
}
private void searchIssues(String query) {
Call<List<Issue>> 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<List<Issue>> call,
@NonNull Response<List<Issue>> response) {
if (response.isSuccessful() && response.body() != null) {
int oldSize = searchResultsList.size();
searchResultsList.clear();
if (oldSize > 0) {
searchResultsAdapter.notifyItemRangeRemoved(0, oldSize);
}
List<Issue> results = response.body();
long currentIssueId = issue.getIssue().getId();
List<Long> 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<List<Issue>> 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<Issue> 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<Issue> call, @NonNull Response<Issue> 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<Issue> 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;
}
}

View File

@@ -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(

View File

@@ -0,0 +1,97 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M10,8v8h2a2,2 0,0 0,2 -2v-4a2,2 0,0 0,-2 -2z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="?attr/iconsColor"
android:strokeLineCap="round"/>
<path
android:pathData="M7.5,4.21v0.01"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="?attr/iconsColor"
android:strokeLineCap="round"/>
<path
android:pathData="M4.21,7.5v0.01"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="?attr/iconsColor"
android:strokeLineCap="round"/>
<path
android:pathData="M3,12v0.01"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="?attr/iconsColor"
android:strokeLineCap="round"/>
<path
android:pathData="M4.21,16.5v0.01"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="?attr/iconsColor"
android:strokeLineCap="round"/>
<path
android:pathData="M7.5,19.79v0.01"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="?attr/iconsColor"
android:strokeLineCap="round"/>
<path
android:pathData="M12,21v0.01"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="?attr/iconsColor"
android:strokeLineCap="round"/>
<path
android:pathData="M16.5,19.79v0.01"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="?attr/iconsColor"
android:strokeLineCap="round"/>
<path
android:pathData="M19.79,16.5v0.01"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="?attr/iconsColor"
android:strokeLineCap="round"/>
<path
android:pathData="M21,12v0.01"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="?attr/iconsColor"
android:strokeLineCap="round"/>
<path
android:pathData="M19.79,7.5v0.01"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="?attr/iconsColor"
android:strokeLineCap="round"/>
<path
android:pathData="M16.5,4.21v0.01"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="?attr/iconsColor"
android:strokeLineCap="round"/>
<path
android:pathData="M12,3v0.01"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="?attr/iconsColor"
android:strokeLineCap="round"/>
</vector>

View File

@@ -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"

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:padding="@dimen/dimen16dp">
<TextView
android:id="@+id/bottomSheetHeader"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/dependencies"
android:textAppearance="?attr/textAppearanceTitleLarge"
android:textColor="?attr/colorOnSurface"
android:paddingBottom="@dimen/dimen16dp" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/searchInputLayout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/search"
app:endIconMode="custom"
app:endIconDrawable="@drawable/ic_search"
app:endIconContentDescription="@string/search">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/searchInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:maxLines="1"
android:textSize="@dimen/dimen16sp"
android:textAppearance="?attr/textAppearanceBodyMedium" />
</com.google.android.material.textfield.TextInputLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/searchResultsRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_marginTop="@dimen/dimen16dp"
android:overScrollMode="never"
tools:listitem="@layout/list_issue_dependency"
tools:itemCount="3" />
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="@dimen/dimen16dp"
app:dividerColor="?attr/colorOutlineVariant" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/dependenciesRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen8dp"
android:overScrollMode="never"
tools:listitem="@layout/list_issue_dependency"
tools:itemCount="0" />
<TextView
android:id="@+id/noDependenciesText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/no_dependencies_set"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurfaceVariant"
android:gravity="center"
android:padding="@dimen/dimen16dp"
android:visibility="visible" />
</LinearLayout>

View File

@@ -163,6 +163,19 @@
app:drawableTopCompat="@drawable/ic_tag"
app:layout_alignSelf="flex_start" />
<TextView
android:id="@+id/manageDependencies"
android:layout_width="@dimen/dimen132dp"
android:layout_height="@dimen/dimen100dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:gravity="center"
android:padding="@dimen/dimen4dp"
android:text="@string/dependencies"
android:textColor="?attr/primaryTextColor"
android:textSize="@dimen/dimen14sp"
app:drawableTopCompat="@drawable/ic_dependencies"
app:layout_alignSelf="flex_start" />
<TextView
android:id="@+id/pin_issue"
android:layout_width="@dimen/dimen132dp"

View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/dimen4dp"
android:orientation="horizontal">
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="?attr/materialCardViewElevatedStyle"
app:cardElevation="@dimen/dimen0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?android:attr/selectableItemBackground"
android:background="?attr/materialCardBackgroundColor"
android:padding="@dimen/dimen12dp"
android:orientation="horizontal">
<TextView
android:id="@+id/dependencyTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?attr/colorOnSurface"
android:gravity="center_vertical"
android:ellipsize="end" />
<ImageView
android:id="@+id/deleteDependency"
android:layout_width="@dimen/dimen22dp"
android:layout_height="@dimen/dimen22dp"
android:src="@drawable/ic_delete"
android:layout_marginStart="@dimen/dimen12dp"
android:contentDescription="@string/menuDeleteText"
android:layout_gravity="center_vertical"
android:visibility="gone"
app:tint="?attr/colorOnSurfaceVariant" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>

View File

@@ -947,4 +947,13 @@
<string name="search_no_matches">No matches found</string>
<string name="heatmap_contribution">%1$d contributions on %2$s</string>
<string name="im_mentioned">I\'m mentioned</string>
<string name="dependencies">Dependencies</string>
<string name="no_dependencies_set">No dependencies set</string>
<string name="dependency_removed">Dependency removed</string>
<string name="dependency_removal_failed">Failed to remove dependency</string>
<string name="dependency_added">Dependency added</string>
<string name="dependency_add_failed">Failed to add dependency</string>
<string name="search_failed">Search failed</string>
<string name="no_dependency_search_results">No matching results found</string>
</resources>