diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index da640a82..d7a57f36 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -85,6 +85,7 @@ + diff --git a/app/src/main/java/org/mian/gitnex/activities/CreatePullRequestActivity.java b/app/src/main/java/org/mian/gitnex/activities/CreatePullRequestActivity.java new file mode 100644 index 00000000..e066deb5 --- /dev/null +++ b/app/src/main/java/org/mian/gitnex/activities/CreatePullRequestActivity.java @@ -0,0 +1,431 @@ +package org.mian.gitnex.activities; + +import android.app.DatePickerDialog; +import android.app.Dialog; +import android.content.Context; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import androidx.annotation.NonNull; +import org.mian.gitnex.R; +import org.mian.gitnex.adapters.LabelsListAdapter; +import org.mian.gitnex.clients.RetrofitClient; +import org.mian.gitnex.databinding.ActivityCreatePrBinding; +import org.mian.gitnex.databinding.CustomLabelsSelectionDialogBinding; +import org.mian.gitnex.helpers.AppUtil; +import org.mian.gitnex.helpers.Authorization; +import org.mian.gitnex.helpers.StaticGlobalVariables; +import org.mian.gitnex.helpers.TinyDB; +import org.mian.gitnex.helpers.Toasty; +import org.mian.gitnex.helpers.Version; +import org.mian.gitnex.models.Branches; +import org.mian.gitnex.models.CreatePullRequest; +import org.mian.gitnex.models.Labels; +import org.mian.gitnex.models.Milestones; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.Callback; + +/** + * Author M M Arif + */ + +public class CreatePullRequestActivity extends BaseActivity implements LabelsListAdapter.LabelsListAdapterListener { + + private View.OnClickListener onClickListener; + private Context ctx = this; + private Context appCtx; + private TinyDB tinyDb; + private ActivityCreatePrBinding viewBinding; + private CustomLabelsSelectionDialogBinding labelsBinding; + private int resultLimit = StaticGlobalVariables.resultLimitOldGiteaInstances; + private Dialog dialogLabels; + private String labelsSetter; + private ArrayList labelsIds = new ArrayList<>(); + private ArrayList assignees = new ArrayList<>(); + private int milestoneId; + + private String instanceUrl; + private String loginUid; + private String instanceToken; + private String repoOwner; + private String repoName; + + private LabelsListAdapter labelsAdapter; + + List milestonesList = new ArrayList<>(); + List branchesList = new ArrayList<>(); + List labelsList = new ArrayList<>(); + + public CreatePullRequestActivity() { + } + + @Override + protected int getLayoutResourceId(){ + return R.layout.activity_create_pr; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + + super.onCreate(savedInstanceState); + appCtx = getApplicationContext(); + tinyDb = new TinyDB(appCtx); + + viewBinding = ActivityCreatePrBinding.inflate(getLayoutInflater()); + View view = viewBinding.getRoot(); + setContentView(view); + + instanceUrl = tinyDb.getString("instanceUrl"); + loginUid = tinyDb.getString("loginUid"); + String repoFullName = tinyDb.getString("repoFullName"); + String[] parts = repoFullName.split("/"); + repoOwner = parts[0]; + repoName = parts[1]; + instanceToken = "token " + tinyDb.getString(loginUid + "-token"); + + // require gitea 1.12 or higher + if(new Version(tinyDb.getString("giteaVersion")).higherOrEqual("1.12.0")) { + resultLimit = StaticGlobalVariables.resultLimitNewGiteaInstances; + } + + labelsAdapter = new LabelsListAdapter(labelsList, CreatePullRequestActivity.this); + + ImageView closeActivity = findViewById(R.id.close); + + initCloseListener(); + closeActivity.setOnClickListener(onClickListener); + + viewBinding.prDueDate.setOnClickListener(dueDate -> + setDueDate() + ); + + disableProcessButton(); + + getMilestones(instanceUrl, instanceToken, repoOwner, repoName, loginUid, resultLimit); + getBranches(instanceUrl, instanceToken, repoOwner, repoName, loginUid); + + viewBinding.prLabels.setOnClickListener(prLabels -> + showLabels() + ); + + viewBinding.createPr.setOnClickListener(createPr -> + processPullRequest() + ); + } + + private void processPullRequest() { + + String prTitle = String.valueOf(viewBinding.prTitle.getText()); + String prDescription = String.valueOf(viewBinding.prBody.getText()); + String mergeInto = viewBinding.mergeIntoBranchSpinner.getText().toString(); + String pullFrom = viewBinding.pullFromBranchSpinner.getText().toString(); + String dueDate = String.valueOf(viewBinding.prDueDate.getText()); + + assignees.add(""); + + if (labelsIds.size() == 0) { + labelsIds.add(0); + } + + if (dueDate.matches("")) { + dueDate = null; + } + else { + dueDate = AppUtil.customDateCombine(AppUtil.customDateFormat(dueDate)); + } + + if(prTitle.matches("")) { + + Toasty.error(ctx, getString(R.string.titleError)); + } + else if(mergeInto.matches("")) { + + Toasty.error(ctx, getString(R.string.mergeIntoError)); + } + else if(pullFrom.matches("")) { + + Toasty.error(ctx, getString(R.string.pullFromError)); + } + else if(pullFrom.equals(mergeInto)) { + + Toasty.error(ctx, getString(R.string.sameBranchesError)); + } + else { + createPullRequest(prTitle, prDescription, mergeInto, pullFrom, milestoneId, dueDate, assignees); + } + //Log.e("processPullRequest", String.valueOf(milestoneId)); + } + + private void createPullRequest(String prTitle, String prDescription, String mergeInto, String pullFrom, int milestoneId, String dueDate, ArrayList assignees) { + + CreatePullRequest createPullRequest = new CreatePullRequest(prTitle, prDescription, loginUid, mergeInto, pullFrom, milestoneId, dueDate, assignees, labelsIds); + + Call transferCall = RetrofitClient + .getInstance(instanceUrl, ctx) + .getApiInterface() + .createPullRequest(instanceToken, repoOwner, repoName, createPullRequest); + + transferCall.enqueue(new Callback() { + + @Override + public void onResponse(@NonNull Call call, @NonNull retrofit2.Response response) { + + disableProcessButton(); + + if (response.code() == 201) { + + Toasty.success(ctx, getString(R.string.prCreateSuccess)); + finish(); + } + else if (response.code() == 409 && response.message().equals("Conflict")) { + + enableProcessButton(); + Toasty.error(ctx, getString(R.string.prAlreadyExists)); + } + else if (response.code() == 404) { + + enableProcessButton(); + Toasty.error(ctx, getString(R.string.apiNotFound)); + } + else { + + enableProcessButton(); + Toasty.error(ctx, getString(R.string.genericError)); + } + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + enableProcessButton(); + Toasty.error(ctx, getString(R.string.genericServerResponseError)); + } + }); + } + + @Override + public void labelsStringData(ArrayList data) { + + labelsSetter = String.valueOf(data); + viewBinding.prLabels.setText(labelsSetter.replace("]", "").replace("[", "")); + } + + @Override + public void labelsIdsData(ArrayList data) { + + labelsIds = data; + } + + private void showLabels() { + + dialogLabels = new Dialog(ctx, R.style.ThemeOverlay_MaterialComponents_Dialog_Alert); + + if (dialogLabels.getWindow() != null) { + dialogLabels.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + } + + labelsBinding = CustomLabelsSelectionDialogBinding.inflate(LayoutInflater.from(ctx)); + + View view = labelsBinding.getRoot(); + dialogLabels.setContentView(view); + + labelsBinding.cancel.setOnClickListener(editProperties -> + dialogLabels.dismiss() + ); + + Call> call = RetrofitClient + .getInstance(instanceUrl, ctx) + .getApiInterface() + .getlabels(instanceToken, repoOwner, repoName); + + call.enqueue(new Callback>() { + + @Override + public void onResponse(@NonNull Call> call, @NonNull retrofit2.Response> response) { + + labelsList.clear(); + List labelsList_ = response.body(); + + labelsBinding.progressBar.setVisibility(View.GONE); + labelsBinding.dialogFrame.setVisibility(View.VISIBLE); + + if (response.code() == 200) { + + assert labelsList_ != null; + if(labelsList_.size() > 0) { + for (int i = 0; i < labelsList_.size(); i++) { + + labelsList.add(new Labels(labelsList_.get(i).getId(), labelsList_.get(i).getName())); + + } + } + else { + + dialogLabels.dismiss(); + Toasty.warning(ctx, getString(R.string.noLabelsFound)); + } + + labelsBinding.labelsRecyclerView.setAdapter(labelsAdapter); + + } + else { + + Toasty.error(ctx, getString(R.string.genericError)); + } + + } + + @Override + public void onFailure(@NonNull Call> call, @NonNull Throwable t) { + + Toasty.error(ctx, getString(R.string.genericServerResponseError)); + } + }); + + dialogLabels.show(); + + } + + private void getBranches(String instanceUrl, String instanceToken, String repoOwner, String repoName, String loginUid) { + + Call> call = RetrofitClient + .getInstance(instanceUrl, ctx) + .getApiInterface() + .getBranches(Authorization.returnAuthentication(ctx, loginUid, instanceToken), repoOwner, repoName); + + call.enqueue(new Callback>() { + + @Override + public void onResponse(@NonNull Call> call, @NonNull retrofit2.Response> response) { + + if(response.isSuccessful()) { + if(response.code() == 200) { + + List branchesList_ = response.body(); + + assert branchesList_ != null; + if(branchesList_.size() > 0) { + for (int i = 0; i < branchesList_.size(); i++) { + + Branches data = new Branches( + branchesList_.get(i).getName() + ); + branchesList.add(data); + + } + } + + ArrayAdapter adapter = new ArrayAdapter<>(CreatePullRequestActivity.this, + R.layout.list_spinner_items, branchesList); + + viewBinding.mergeIntoBranchSpinner.setAdapter(adapter); + viewBinding.pullFromBranchSpinner.setAdapter(adapter); + enableProcessButton(); + + } + } + + } + + @Override + public void onFailure(@NonNull Call> call, @NonNull Throwable t) { + + Toasty.error(ctx, getString(R.string.genericServerResponseError)); + } + }); + + } + + private void getMilestones(String instanceUrl, String instanceToken, String repoOwner, String repoName, String loginUid, int resultLimit) { + + String msState = "open"; + Call> call = RetrofitClient + .getInstance(instanceUrl, ctx) + .getApiInterface() + .getMilestones(Authorization.returnAuthentication(ctx, loginUid, instanceToken), repoOwner, repoName, 1, resultLimit, msState); + + call.enqueue(new Callback>() { + + @Override + public void onResponse(@NonNull Call> call, @NonNull retrofit2.Response> response) { + + if(response.code() == 200) { + + List milestonesList_ = response.body(); + + milestonesList.add(new Milestones(0,getString(R.string.issueCreatedNoMilestone))); + assert milestonesList_ != null; + if(milestonesList_.size() > 0) { + for (int i = 0; i < milestonesList_.size(); i++) { + + //Don't translate "open" is a enum + if(milestonesList_.get(i).getState().equals("open")) { + Milestones data = new Milestones( + milestonesList_.get(i).getId(), + milestonesList_.get(i).getTitle() + ); + milestonesList.add(data); + } + + } + } + + ArrayAdapter adapter = new ArrayAdapter<>(CreatePullRequestActivity.this, + R.layout.list_spinner_items, milestonesList); + + viewBinding.milestonesSpinner.setAdapter(adapter); + enableProcessButton(); + + viewBinding.milestonesSpinner.setOnItemClickListener ((parent, view, position, id) -> + + milestoneId = milestonesList.get(position).getId() + ); + + } + } + + @Override + public void onFailure(@NonNull Call> call, @NonNull Throwable t) { + + Toasty.error(ctx, getString(R.string.genericServerResponseError)); + } + }); + + } + + private void setDueDate() { + + final Calendar c = Calendar.getInstance(); + int mYear = c.get(Calendar.YEAR); + final int mMonth = c.get(Calendar.MONTH); + final int mDay = c.get(Calendar.DAY_OF_MONTH); + + DatePickerDialog datePickerDialog = new DatePickerDialog(this, + (view, year, monthOfYear, dayOfMonth) -> viewBinding.prDueDate.setText(getString(R.string.setDueDate, year, (monthOfYear + 1), dayOfMonth)), mYear, mMonth, mDay); + datePickerDialog.show(); + } + + private void initCloseListener() { + + onClickListener = view -> finish(); + } + + private void disableProcessButton() { + + viewBinding.createPr.setEnabled(false); + } + + private void enableProcessButton() { + + viewBinding.createPr.setEnabled(true); + } +} diff --git a/app/src/main/java/org/mian/gitnex/activities/RepoDetailActivity.java b/app/src/main/java/org/mian/gitnex/activities/RepoDetailActivity.java index e3743446..80b00689 100644 --- a/app/src/main/java/org/mian/gitnex/activities/RepoDetailActivity.java +++ b/app/src/main/java/org/mian/gitnex/activities/RepoDetailActivity.java @@ -406,6 +406,10 @@ public class RepoDetailActivity extends BaseActivity implements BottomSheetRepoF startActivity(new Intent(RepoDetailActivity.this, RepositorySettingsActivity.class)); break; + case "newPullRequest": + startActivity(new Intent(RepoDetailActivity.this, CreatePullRequestActivity.class)); + break; + } } diff --git a/app/src/main/java/org/mian/gitnex/activities/RepositorySettingsActivity.java b/app/src/main/java/org/mian/gitnex/activities/RepositorySettingsActivity.java index d6dff0b5..e4c0ff96 100644 --- a/app/src/main/java/org/mian/gitnex/activities/RepositorySettingsActivity.java +++ b/app/src/main/java/org/mian/gitnex/activities/RepositorySettingsActivity.java @@ -394,6 +394,9 @@ public class RepositorySettingsActivity extends BaseActivity { if (response.code() == 200) { + tinyDb.putBoolean("hasIssues", repoEnableIssues); + tinyDb.putBoolean("hasPullRequests", repoEnablePr); + dialogProp.dismiss(); Toasty.success(ctx, getString(R.string.repoPropertiesSaveSuccess)); diff --git a/app/src/main/java/org/mian/gitnex/adapters/LabelsListAdapter.java b/app/src/main/java/org/mian/gitnex/adapters/LabelsListAdapter.java new file mode 100644 index 00000000..92b8ba54 --- /dev/null +++ b/app/src/main/java/org/mian/gitnex/adapters/LabelsListAdapter.java @@ -0,0 +1,95 @@ +package org.mian.gitnex.adapters; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import org.mian.gitnex.R; +import org.mian.gitnex.models.Labels; +import java.util.ArrayList; +import java.util.List; + +/** + * Author M M Arif + */ + +public class LabelsListAdapter extends RecyclerView.Adapter { + + private List labels; + private ArrayList labelsStrings = new ArrayList<>(); + private ArrayList labelsIds = new ArrayList<>(); + + private LabelsListAdapterListener labelsListener; + + public interface LabelsListAdapterListener { + + void labelsStringData(ArrayList data); + void labelsIdsData(ArrayList data); + } + + public LabelsListAdapter(List labelsMain, LabelsListAdapterListener labelsListener) { + + this.labels = labelsMain; + this.labelsListener = labelsListener; + } + + static class LabelsViewHolder extends RecyclerView.ViewHolder { + + private CheckBox labelSelection; + + private LabelsViewHolder(View itemView) { + super(itemView); + + labelSelection = itemView.findViewById(R.id.labelSelection); + + } + } + + @NonNull + @Override + public LabelsListAdapter.LabelsViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.custom_labels_list, parent, false); + return new LabelsListAdapter.LabelsViewHolder(v); + } + + @Override + public void onBindViewHolder(@NonNull LabelsListAdapter.LabelsViewHolder holder, int position) { + + Labels currentItem = labels.get(position); + + holder.labelSelection.setText(currentItem.getName()); + + for(int i = 0; i < labelsIds.size(); i++) { + + if(labelsStrings.contains(currentItem.getName())) { + + holder.labelSelection.setChecked(true); + } + } + + holder.labelSelection.setOnCheckedChangeListener((buttonView, isChecked) -> { + + if(isChecked) { + + labelsStrings.add(currentItem.getName()); + labelsIds.add(currentItem.getId()); + } + else { + + labelsStrings.remove(currentItem.getName()); + labelsIds.remove(Integer.valueOf(currentItem.getId())); + } + + labelsListener.labelsStringData(labelsStrings); + labelsListener.labelsIdsData(labelsIds); + }); + } + + @Override + public int getItemCount() { + return labels.size(); + } +} diff --git a/app/src/main/java/org/mian/gitnex/fragments/BottomSheetRepoFragment.java b/app/src/main/java/org/mian/gitnex/fragments/BottomSheetRepoFragment.java index dd097632..f99ea3a1 100644 --- a/app/src/main/java/org/mian/gitnex/fragments/BottomSheetRepoFragment.java +++ b/app/src/main/java/org/mian/gitnex/fragments/BottomSheetRepoFragment.java @@ -43,6 +43,7 @@ public class BottomSheetRepoFragment extends BottomSheetDialogFragment { TextView copyRepoUrl = v.findViewById(R.id.copyRepoUrl); View repoSettingsDivider = v.findViewById(R.id.repoSettingsDivider); TextView repoSettings = v.findViewById(R.id.repoSettings); + TextView createPullRequest = v.findViewById(R.id.createPullRequest); createLabel.setOnClickListener(v112 -> { @@ -64,6 +65,20 @@ public class BottomSheetRepoFragment extends BottomSheetDialogFragment { createIssue.setVisibility(View.GONE); } + if(tinyDb.getBoolean("hasPullRequests")) { + + createPullRequest.setVisibility(View.VISIBLE); + createPullRequest.setOnClickListener(vPr -> { + + bmListener.onButtonClicked("newPullRequest"); + dismiss(); + }); + } + else { + + createPullRequest.setVisibility(View.GONE); + } + createMilestone.setOnClickListener(v13 -> { bmListener.onButtonClicked("newMilestone"); diff --git a/app/src/main/java/org/mian/gitnex/fragments/IssuesFragment.java b/app/src/main/java/org/mian/gitnex/fragments/IssuesFragment.java index 77a68ab9..4ab393d3 100644 --- a/app/src/main/java/org/mian/gitnex/fragments/IssuesFragment.java +++ b/app/src/main/java/org/mian/gitnex/fragments/IssuesFragment.java @@ -32,7 +32,6 @@ import org.mian.gitnex.interfaces.ApiInterface; import org.mian.gitnex.models.Issues; import java.util.ArrayList; import java.util.List; -import java.util.Objects; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; @@ -112,7 +111,7 @@ public class IssuesFragment extends Fragment { recyclerView.setLayoutManager(new LinearLayoutManager(context)); recyclerView.setAdapter(adapter); - ((RepoDetailActivity) Objects.requireNonNull(getActivity())).setFragmentRefreshListener(issueState -> { + ((RepoDetailActivity) requireActivity()).setFragmentRefreshListener(issueState -> { if(issueState.equals("closed")) { menu.getItem(1).setIcon(R.drawable.ic_filter_closed); diff --git a/app/src/main/java/org/mian/gitnex/fragments/RepoInfoFragment.java b/app/src/main/java/org/mian/gitnex/fragments/RepoInfoFragment.java index bdf79e30..58e96a16 100644 --- a/app/src/main/java/org/mian/gitnex/fragments/RepoInfoFragment.java +++ b/app/src/main/java/org/mian/gitnex/fragments/RepoInfoFragment.java @@ -351,6 +351,13 @@ public class RepoInfoFragment extends Fragment { tinyDb.putBoolean("hasIssues", true); } + if(repoInfo.isHas_pull_requests()) { + tinyDb.putBoolean("hasPullRequests", repoInfo.isHas_pull_requests()); + } + else { + tinyDb.putBoolean("hasPullRequests", false); + } + tinyDb.putString("repoHtmlUrl", repoInfo.getHtml_url()); mProgressBar.setVisibility(View.GONE); diff --git a/app/src/main/java/org/mian/gitnex/interfaces/ApiInterface.java b/app/src/main/java/org/mian/gitnex/interfaces/ApiInterface.java index 6c7a78fb..460b2cea 100644 --- a/app/src/main/java/org/mian/gitnex/interfaces/ApiInterface.java +++ b/app/src/main/java/org/mian/gitnex/interfaces/ApiInterface.java @@ -7,6 +7,7 @@ import org.mian.gitnex.models.Collaborators; import org.mian.gitnex.models.Commits; import org.mian.gitnex.models.CreateIssue; import org.mian.gitnex.models.CreateLabel; +import org.mian.gitnex.models.CreatePullRequest; import org.mian.gitnex.models.DeleteFile; import org.mian.gitnex.models.EditFile; import org.mian.gitnex.models.Emails; @@ -321,6 +322,9 @@ public interface ApiInterface { @POST("repos/{owner}/{repo}/pulls/{index}/merge") // merge a pull request Call mergePullRequest(@Header("Authorization") String token, @Path("owner") String ownerName, @Path("repo") String repoName, @Path("index") int index, @Body MergePullRequest jsonStr); + @POST("repos/{owner}/{repo}/pulls") // create a pull request + Call createPullRequest(@Header("Authorization") String token, @Path("owner") String ownerName, @Path("repo") String repoName, @Body CreatePullRequest jsonStr); + @GET("repos/{owner}/{repo}/commits") // get all commits Call> getRepositoryCommits(@Header("Authorization") String token, @Path("owner") String owner, @Path("repo") String repo, @Query("page") int page, @Query("sha") String branchName, @Query("limit") int limit); diff --git a/app/src/main/java/org/mian/gitnex/models/CreatePullRequest.java b/app/src/main/java/org/mian/gitnex/models/CreatePullRequest.java new file mode 100644 index 00000000..91c4b074 --- /dev/null +++ b/app/src/main/java/org/mian/gitnex/models/CreatePullRequest.java @@ -0,0 +1,36 @@ +package org.mian.gitnex.models; + +import java.util.ArrayList; + +/** + * Author M M Arif + */ + +public class CreatePullRequest { + + private String title; + private String body; + private String assignee; + private String base; + private String head; + private int milestone; + private String due_date; + private String message; + + private ArrayList assignees; + private ArrayList labels; + + public CreatePullRequest(String title, String body, String assignee, String base, String head, int milestone, String due_date, ArrayList assignees, ArrayList labels) { + + this.title = title; + this.body = body; + this.assignee = assignee; + this.base = base; // merge into branch + this.head = head; // pull from branch + this.milestone = milestone; + this.due_date = due_date; + this.assignees = assignees; + this.labels = labels; + } + +} diff --git a/app/src/main/java/org/mian/gitnex/models/Labels.java b/app/src/main/java/org/mian/gitnex/models/Labels.java index 0569a8f9..87ca1aaa 100644 --- a/app/src/main/java/org/mian/gitnex/models/Labels.java +++ b/app/src/main/java/org/mian/gitnex/models/Labels.java @@ -21,6 +21,11 @@ public class Labels { this.labels = labels; } + public Labels(int id, String name) { + this.id = id; + this.name = name; + } + public int getId() { return id; } diff --git a/app/src/main/res/layout/activity_create_pr.xml b/app/src/main/res/layout/activity_create_pr.xml new file mode 100644 index 00000000..72dae40a --- /dev/null +++ b/app/src/main/res/layout/activity_create_pr.xml @@ -0,0 +1,246 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +