New Pr creation and edit UI

This commit is contained in:
M M Arif
2026-04-15 23:26:00 +05:00
parent b2e6f3d8ba
commit 0545f4480b
27 changed files with 1409 additions and 2257 deletions

View File

@@ -39,10 +39,6 @@
<activity
android:name=".activities.AdminGetUsersActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|keyboard|keyboardHidden|navigation"/>
<activity
android:name=".activities.EditIssueActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|keyboard|keyboardHidden|navigation"
android:windowSoftInputMode="adjustResize"/>
<activity
android:name=".activities.OrganizationTeamDetailsActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|keyboard|keyboardHidden|navigation"/>
@@ -98,10 +94,6 @@
<activity
android:name=".activities.RepositorySettingsActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|keyboard|keyboardHidden|navigation"/>
<activity
android:name=".activities.CreatePullRequestActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|keyboard|keyboardHidden|navigation"
android:windowSoftInputMode="adjustResize"/>
<activity
android:name=".activities.CodeEditorActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|keyboard|keyboardHidden|navigation"

View File

@@ -1,788 +0,0 @@
package org.mian.gitnex.activities;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Dialog;
import android.content.Intent;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.datepicker.MaterialDatePicker;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.vdurmont.emoji.EmojiParser;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.TimeZone;
import okhttp3.MediaType;
import okhttp3.RequestBody;
import org.gitnex.tea4j.v2.models.Attachment;
import org.gitnex.tea4j.v2.models.Branch;
import org.gitnex.tea4j.v2.models.CreatePullRequestOption;
import org.gitnex.tea4j.v2.models.Label;
import org.gitnex.tea4j.v2.models.Milestone;
import org.gitnex.tea4j.v2.models.PullRequest;
import org.mian.gitnex.R;
import org.mian.gitnex.actions.LabelsActions;
import org.mian.gitnex.adapters.AttachmentsAdapter;
import org.mian.gitnex.adapters.BranchAdapter;
import org.mian.gitnex.adapters.LabelsListAdapter;
import org.mian.gitnex.adapters.NotesAdapter;
import org.mian.gitnex.clients.RetrofitClient;
import org.mian.gitnex.database.api.BaseApi;
import org.mian.gitnex.database.api.NotesApi;
import org.mian.gitnex.database.models.Notes;
import org.mian.gitnex.databinding.ActivityCreatePrBinding;
import org.mian.gitnex.databinding.BottomSheetAttachmentsBinding;
import org.mian.gitnex.databinding.CustomInsertNoteBinding;
import org.mian.gitnex.databinding.CustomLabelsSelectionDialogBinding;
import org.mian.gitnex.helpers.AlertDialogs;
import org.mian.gitnex.helpers.AppDatabaseSettings;
import org.mian.gitnex.helpers.Constants;
import org.mian.gitnex.helpers.Markdown;
import org.mian.gitnex.helpers.MentionHelper;
import org.mian.gitnex.helpers.Toasty;
import org.mian.gitnex.helpers.attachments.AttachmentUtils;
import org.mian.gitnex.helpers.attachments.AttachmentsModel;
import org.mian.gitnex.helpers.contexts.RepositoryContext;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* @author mmarif
*/
public class CreatePullRequestActivity extends BaseActivity
implements LabelsListAdapter.LabelsListAdapterListener,
AttachmentsAdapter.AttachmentsReceiverListener {
private final List<String> assignees = new ArrayList<>();
LinkedHashMap<String, Milestone> milestonesList = new LinkedHashMap<>();
List<Label> labelsList = new ArrayList<>();
private ActivityCreatePrBinding viewBinding;
private List<Integer> labelsIds = new ArrayList<>();
private int milestoneId;
private RepositoryContext repository;
private LabelsListAdapter labelsAdapter;
private MaterialAlertDialogBuilder materialAlertDialogBuilder;
private MaterialAlertDialogBuilder materialAlertDialogBuilderNotes;
private boolean renderMd = false;
private RepositoryContext repositoryContext;
private static List<AttachmentsModel> attachmentsList;
private AttachmentsAdapter attachmentsAdapter;
private static final List<Uri> contentUri = new ArrayList<>();
private CustomInsertNoteBinding customInsertNoteBinding;
private NotesAdapter adapter;
private NotesApi notesApi;
public AlertDialog dialogNotes;
private MentionHelper mentionHelper;
@SuppressLint("ClickableViewAccessibility")
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewBinding = ActivityCreatePrBinding.inflate(getLayoutInflater());
setContentView(viewBinding.getRoot());
repositoryContext = RepositoryContext.fromIntent(getIntent());
materialAlertDialogBuilder =
new MaterialAlertDialogBuilder(ctx, R.style.ThemeOverlay_Material3_Dialog_Alert);
materialAlertDialogBuilderNotes =
new MaterialAlertDialogBuilder(ctx, R.style.ThemeOverlay_Material3_Dialog_Alert);
repository = RepositoryContext.fromIntent(getIntent());
attachmentsList = new ArrayList<>();
attachmentsAdapter = new AttachmentsAdapter(attachmentsList, ctx);
AttachmentsAdapter.setAttachmentsReceiveListener(this);
int resultLimit = Constants.getCurrentResultLimit(ctx);
mentionHelper = new MentionHelper(this, viewBinding.prBody);
mentionHelper.setup();
viewBinding.prBody.setOnTouchListener(
(touchView, motionEvent) -> {
touchView.getParent().requestDisallowInterceptTouchEvent(true);
if ((motionEvent.getAction() & MotionEvent.ACTION_UP) != 0
&& (motionEvent.getActionMasked() & MotionEvent.ACTION_UP) != 0) {
touchView.getParent().requestDisallowInterceptTouchEvent(false);
}
return false;
});
labelsAdapter =
new LabelsListAdapter(labelsList, CreatePullRequestActivity.this, labelsIds);
showDatePickerDialog();
viewBinding.topAppBar.setNavigationOnClickListener(
v -> {
finish();
contentUri.clear();
});
viewBinding.topAppBar.setOnMenuItemClickListener(
menuItem -> {
int id = menuItem.getItemId();
if (id == R.id.markdown) {
if (!renderMd) {
Markdown.render(
ctx,
EmojiParser.parseToUnicode(
Objects.requireNonNull(viewBinding.prBody.getText())
.toString()),
viewBinding.markdownPreview,
repositoryContext);
viewBinding.markdownPreview.setVisibility(View.VISIBLE);
viewBinding.prBodyLayout.setVisibility(View.GONE);
renderMd = true;
} else {
viewBinding.markdownPreview.setVisibility(View.GONE);
viewBinding.prBodyLayout.setVisibility(View.VISIBLE);
renderMd = false;
}
return true;
} else if (id == R.id.create) {
processPullRequest();
return true;
} else if (id == R.id.attachment) {
checkForAttachments();
return true;
} else {
return super.onOptionsItemSelected(menuItem);
}
});
viewBinding.insertNote.setOnClickListener(insertNote -> showAllNotes());
getMilestones(repository.getOwner(), repository.getName(), resultLimit);
viewBinding.mergeIntoBranchSpinner.setKeyListener(null);
viewBinding.mergeIntoBranchSpinner.setCursorVisible(false);
viewBinding.mergeIntoBranchSpinner.setOnFocusChangeListener(
(v, hasFocus) -> {
if (hasFocus) {
getBranches("merge");
viewBinding.mergeIntoBranchSpinner.clearFocus();
}
});
viewBinding.pullFromBranchSpinner.setKeyListener(null);
viewBinding.pullFromBranchSpinner.setCursorVisible(false);
viewBinding.pullFromBranchSpinner.setOnFocusChangeListener(
(v, hasFocus) -> {
if (hasFocus) {
getBranches("pull");
viewBinding.pullFromBranchSpinner.clearFocus();
}
});
viewBinding.prLabels.setOnClickListener(prLabels -> showLabels());
if (!repository.getPermissions().isPush()) {
viewBinding.prDueDateLayout.setVisibility(View.GONE);
viewBinding.prLabelsLayout.setVisibility(View.GONE);
viewBinding.milestonesSpinnerLayout.setVisibility(View.GONE);
}
}
ActivityResultLauncher<Intent> startActivityForResult =
registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK) {
Intent data = result.getData();
assert data != null;
contentUri.add(data.getData());
attachmentsList.add(
new AttachmentsModel(
AttachmentUtils.queryName(ctx, data.getData()),
data.getData()));
attachmentsAdapter.updateList(attachmentsList);
}
});
private void showAllNotes() {
List<Notes> notesList = new ArrayList<>();
notesApi = BaseApi.getInstance(ctx, NotesApi.class);
customInsertNoteBinding = CustomInsertNoteBinding.inflate(LayoutInflater.from(ctx));
materialAlertDialogBuilderNotes.setView(customInsertNoteBinding.getRoot());
customInsertNoteBinding.recyclerView.setLayoutManager(new LinearLayoutManager(ctx));
adapter = new NotesAdapter(ctx, notesList, "insert", "pr");
customInsertNoteBinding.recyclerView.setAdapter(adapter);
if (notesApi.getCount() > 0) {
fetchNotes();
dialogNotes = materialAlertDialogBuilderNotes.show();
} else {
Toasty.show(ctx, getString(R.string.noNotes));
}
}
private void fetchNotes() {
customInsertNoteBinding.expressiveLoader.setVisibility(View.VISIBLE);
notesApi.fetchAllNotes()
.observe(
this,
allNotes -> {
customInsertNoteBinding.expressiveLoader.setVisibility(View.GONE);
if (allNotes != null && !allNotes.isEmpty()) {
adapter.updateList(allNotes);
customInsertNoteBinding
.layoutEmpty
.getRoot()
.setVisibility(View.GONE);
} else {
adapter.updateList(new ArrayList<>());
customInsertNoteBinding
.layoutEmpty
.getRoot()
.setVisibility(View.VISIBLE);
}
});
}
public void onDestroy() {
super.onDestroy();
AttachmentsAdapter.setAttachmentsReceiveListener(null);
mentionHelper.dismissPopup();
}
@Override
public void setAttachmentsData(Uri filename) {
contentUri.remove(filename);
}
private void checkForAttachments() {
if (!contentUri.isEmpty()) {
BottomSheetAttachmentsBinding bottomSheetAttachmentsBinding =
BottomSheetAttachmentsBinding.inflate(getLayoutInflater());
BottomSheetDialog bottomSheetDialog = new BottomSheetDialog(ctx);
bottomSheetAttachmentsBinding.addAttachment.setOnClickListener(
v1 -> openFileAttachmentActivity());
bottomSheetAttachmentsBinding.recyclerViewAttachments.setHasFixedSize(true);
bottomSheetAttachmentsBinding.recyclerViewAttachments.setLayoutManager(
new LinearLayoutManager(ctx));
bottomSheetAttachmentsBinding.recyclerViewAttachments.setAdapter(attachmentsAdapter);
bottomSheetDialog.setContentView(bottomSheetAttachmentsBinding.getRoot());
bottomSheetDialog.show();
} else {
openFileAttachmentActivity();
}
}
private void openFileAttachmentActivity() {
Intent data = new Intent(Intent.ACTION_GET_CONTENT);
data.addCategory(Intent.CATEGORY_OPENABLE);
data.setType("*/*");
Intent intent = Intent.createChooser(data, "Choose a file");
startActivityForResult.launch(intent);
}
private void processAttachments(long issueIndex) {
for (int i = 0; i < contentUri.size(); i++) {
File file = AttachmentUtils.getFile(ctx, contentUri.get(i));
RequestBody requestFile =
RequestBody.create(
file,
MediaType.parse(
Objects.requireNonNull(
getContentResolver().getType(contentUri.get(i)))));
uploadAttachments(requestFile, issueIndex, file.getName());
}
}
private void uploadAttachments(RequestBody requestFile, long issueIndex, String filename1) {
Call<Attachment> call3 =
RetrofitClient.getApiInterface(ctx)
.issueCreateIssueAttachment(
requestFile,
repository.getOwner(),
repository.getName(),
issueIndex,
filename1);
call3.enqueue(
new Callback<>() {
@Override
public void onResponse(
@NonNull Call<Attachment> call,
@NonNull retrofit2.Response<Attachment> response2) {
if (response2.code() == 201) {
new Handler().postDelayed(() -> finish(), 3000);
} else if (response2.code() == 401) {
AlertDialogs.authorizationTokenRevokedDialog(ctx);
} else {
Toasty.show(ctx, getString(R.string.attachmentsSaveError));
}
}
@Override
public void onFailure(@NonNull Call<Attachment> call, @NonNull Throwable t) {
Toasty.show(ctx, getString(R.string.genericServerResponseError));
}
});
}
private void processPullRequest() {
String prTitle = String.valueOf(viewBinding.prTitle.getText());
String prDescription = String.valueOf(viewBinding.prBody.getText());
String mergeInto =
Objects.requireNonNull(viewBinding.mergeIntoBranchSpinner.getText()).toString();
String pullFrom =
Objects.requireNonNull(viewBinding.pullFromBranchSpinner.getText()).toString();
String prDueDate = Objects.requireNonNull(viewBinding.prDueDate.getText()).toString();
assignees.add("");
if (labelsIds.isEmpty()) {
labelsIds.add(0);
}
if (prTitle.matches("")) {
Toasty.show(ctx, getString(R.string.titleError));
} else if (mergeInto.matches("")) {
Toasty.show(ctx, getString(R.string.mergeIntoError));
} else if (pullFrom.matches("")) {
Toasty.show(ctx, getString(R.string.pullFromError));
} else if (pullFrom.equals(mergeInto)) {
Toasty.show(ctx, getString(R.string.sameBranchesError));
} else {
createPullRequest(
prTitle, prDescription, mergeInto, pullFrom, milestoneId, assignees, prDueDate);
}
}
private void createPullRequest(
String prTitle,
String prDescription,
String mergeInto,
String pullFrom,
int milestoneId,
List<String> assignees,
String prDueDate) {
viewBinding.topAppBar.getMenu().getItem(2).setVisible(false);
ArrayList<Long> labelIds = new ArrayList<>();
for (Integer i : labelsIds) {
labelIds.add((long) i);
}
CreatePullRequestOption createPullRequest = new CreatePullRequestOption();
createPullRequest.setTitle(prTitle);
createPullRequest.setMilestone((long) milestoneId);
createPullRequest.setAssignees(assignees);
createPullRequest.setBody(prDescription);
createPullRequest.setBase(mergeInto);
createPullRequest.setHead(pullFrom);
createPullRequest.setLabels(labelIds);
String[] date = prDueDate.split("-");
if (!prDueDate.equalsIgnoreCase("")) {
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.YEAR, Integer.parseInt(date[0]));
calendar.set(Calendar.MONTH, Integer.parseInt(date[1]));
calendar.set(Calendar.DATE, Integer.parseInt(date[2]));
Date dueDate = calendar.getTime();
createPullRequest.setDueDate(dueDate);
}
Call<PullRequest> transferCall =
RetrofitClient.getApiInterface(ctx)
.repoCreatePullRequest(
repository.getOwner(), repository.getName(), createPullRequest);
transferCall.enqueue(
new Callback<>() {
@Override
public void onResponse(
@NonNull Call<PullRequest> call,
@NonNull retrofit2.Response<PullRequest> response) {
if (response.code() == 201) {
Toasty.show(ctx, getString(R.string.prCreateSuccess));
// RepoDetailActivity.updateRepo = true;
// PullRequestsFragment.resumePullRequests = true;
MainActivity.reloadRepos = true;
if (!contentUri.isEmpty()) {
assert response.body() != null;
processAttachments(response.body().getNumber());
contentUri.clear();
} else {
new Handler().postDelayed(() -> finish(), 3000);
}
} else if (response.code() == 409
|| response.message().equals("Conflict")) {
viewBinding.topAppBar.getMenu().getItem(2).setVisible(false);
Toasty.show(ctx, getString(R.string.prAlreadyExists));
} else if (response.code() == 404) {
viewBinding.topAppBar.getMenu().getItem(2).setVisible(false);
Toasty.show(ctx, getString(R.string.apiNotFound));
} else {
viewBinding.topAppBar.getMenu().getItem(2).setVisible(false);
Toasty.show(ctx, getString(R.string.genericError));
}
}
@Override
public void onFailure(@NonNull Call<PullRequest> call, @NonNull Throwable t) {
viewBinding.topAppBar.getMenu().getItem(2).setVisible(false);
Toasty.show(ctx, getString(R.string.genericServerResponseError));
}
});
}
private void showDatePickerDialog() {
MaterialDatePicker.Builder<Long> builder = MaterialDatePicker.Builder.datePicker();
builder.setSelection(Calendar.getInstance().getTimeInMillis());
builder.setTitleText(R.string.newIssueDueDateTitle);
MaterialDatePicker<Long> materialDatePicker = builder.build();
String[] locale_ =
AppDatabaseSettings.getSettingsValue(ctx, AppDatabaseSettings.APP_LOCALE_KEY)
.split("\\|");
viewBinding.prDueDate.setOnClickListener(
v -> materialDatePicker.show(getSupportFragmentManager(), "DATE_PICKER"));
materialDatePicker.addOnPositiveButtonClickListener(
selection -> {
Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
calendar.setTimeInMillis(selection);
SimpleDateFormat format =
new SimpleDateFormat("yyyy-MM-dd", new Locale(locale_[1]));
String formattedDate = format.format(calendar.getTime());
viewBinding.prDueDate.setText(formattedDate);
});
}
@Override
public void labelsInterface(List<String> data) {
String labelsSetter = String.valueOf(data);
viewBinding.prLabels.setText(labelsSetter.replace("]", "").replace("[", ""));
}
@Override
public void labelsIdsInterface(List<Integer> data) {
labelsIds = data;
}
private void showLabels() {
viewBinding.progressBar.setVisibility(View.VISIBLE);
CustomLabelsSelectionDialogBinding labelsBinding =
CustomLabelsSelectionDialogBinding.inflate(LayoutInflater.from(ctx));
View view = labelsBinding.getRoot();
materialAlertDialogBuilder.setView(view);
materialAlertDialogBuilder.setNeutralButton(R.string.close, null);
LabelsActions.getRepositoryLabels(
ctx,
repository.getOwner(),
repository.getName(),
labelsList,
materialAlertDialogBuilder,
labelsAdapter,
labelsBinding,
viewBinding.progressBar);
}
private void getBranches(String type) {
Dialog progressDialog = new Dialog(ctx);
progressDialog.setCancelable(false);
progressDialog.setContentView(R.layout.custom_progress_loader);
progressDialog.show();
MaterialAlertDialogBuilder dialogBuilder = new MaterialAlertDialogBuilder(ctx);
View dialogView = getLayoutInflater().inflate(R.layout.custom_branches_dialog, null);
dialogBuilder.setView(dialogView);
RecyclerView recyclerView = dialogView.findViewById(R.id.recyclerView);
recyclerView.setLayoutManager(new LinearLayoutManager(ctx));
recyclerView.addItemDecoration(
new RecyclerView.ItemDecoration() {
@Override
public void getItemOffsets(
@NonNull Rect outRect,
@NonNull View view,
@NonNull RecyclerView parent,
@NonNull RecyclerView.State state) {
int position = parent.getChildAdapterPosition(view);
int spacingSides = (int) ctx.getResources().getDimension(R.dimen.dimen16dp);
int spacingTop = (int) ctx.getResources().getDimension(R.dimen.dimen12dp);
outRect.right = spacingSides;
outRect.left = spacingSides;
if (position > 0) {
outRect.top = spacingTop;
}
}
});
dialogBuilder.setNeutralButton(R.string.close, (dialog, which) -> dialog.dismiss());
AlertDialog dialog = dialogBuilder.create();
dialog.setCancelable(false);
dialog.setCanceledOnTouchOutside(false);
final int[] page = {1};
final int resultLimit = Constants.getCurrentResultLimit(ctx);
final boolean[] isLoading = {false};
final boolean[] isLastPage = {false};
BranchAdapter adapter =
new BranchAdapter(
branchName -> {
if (type.equalsIgnoreCase("merge")) {
viewBinding.mergeIntoBranchSpinner.setText(branchName);
}
if (type.equalsIgnoreCase("pull")) {
viewBinding.pullFromBranchSpinner.setText(branchName);
}
dialog.dismiss();
});
recyclerView.setAdapter(adapter);
Runnable fetchBranches =
() -> {
if (isLoading[0] || isLastPage[0]) return;
isLoading[0] = true;
Call<List<Branch>> call =
RetrofitClient.getApiInterface(ctx)
.repoListBranches(
repository.getOwner(),
repository.getName(),
page[0],
resultLimit);
call.enqueue(
new Callback<>() {
@Override
public void onResponse(
@NonNull Call<List<Branch>> call,
@NonNull Response<List<Branch>> response) {
isLoading[0] = false;
if (response.code() == 200 && response.body() != null) {
List<Branch> newBranches = response.body();
adapter.addBranches(newBranches);
String totalCountStr =
response.headers().get("X-Total-Count");
if (totalCountStr != null) {
int totalItems = Integer.parseInt(totalCountStr);
int totalPages =
(int)
Math.ceil(
(double) totalItems
/ resultLimit);
isLastPage[0] = page[0] >= totalPages;
} else {
isLastPage[0] = newBranches.size() < resultLimit;
}
page[0]++;
if (page[0] == 2 && !dialog.isShowing()) {
progressDialog.dismiss();
dialog.show();
}
} else {
progressDialog.dismiss();
}
}
@Override
public void onFailure(
@NonNull Call<List<Branch>> call, @NonNull Throwable t) {
isLoading[0] = false;
progressDialog.dismiss();
}
});
};
recyclerView.addOnScrollListener(
new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
LinearLayoutManager layoutManager =
(LinearLayoutManager) recyclerView.getLayoutManager();
if (layoutManager != null) {
int visibleItemCount = layoutManager.getChildCount();
int totalItemCount = layoutManager.getItemCount();
int firstVisibleItemPosition =
layoutManager.findFirstVisibleItemPosition();
if (!isLoading[0]
&& !isLastPage[0]
&& (visibleItemCount + firstVisibleItemPosition)
>= totalItemCount - 5) {
fetchBranches.run();
}
}
}
});
// adapter.clear();
fetchBranches.run();
}
private void getMilestones(String repoOwner, String repoName, int resultLimit) {
String msState = "open";
Call<List<Milestone>> call =
RetrofitClient.getApiInterface(ctx)
.issueGetMilestonesList(repoOwner, repoName, msState, null, 1, resultLimit);
call.enqueue(
new Callback<>() {
@Override
public void onResponse(
@NonNull Call<List<Milestone>> call,
@NonNull retrofit2.Response<List<Milestone>> response) {
if (response.code() == 200) {
List<Milestone> milestonesList_ = response.body();
milestonesList.put(
getString(R.string.issueCreatedNoMilestone),
new Milestone()
.id(0L)
.title(getString(R.string.issueCreatedNoMilestone)));
assert milestonesList_ != null;
if (!milestonesList_.isEmpty()) {
for (Milestone milestone : milestonesList_) {
// Don't translate "open" is a enum
if (milestone.getState().equals("open")) {
milestonesList.put(milestone.getTitle(), milestone);
}
}
}
ArrayAdapter<String> adapter =
new ArrayAdapter<>(
CreatePullRequestActivity.this,
R.layout.list_spinner_items,
new ArrayList<>(milestonesList.keySet()));
viewBinding.milestonesSpinner.setAdapter(adapter);
viewBinding.milestonesSpinner.setOnItemClickListener(
(parent, view, position, id) -> {
if (position == 0) {
milestoneId = 0;
} else if (view instanceof TextView) {
milestoneId =
Math.toIntExact(
Objects.requireNonNull(
milestonesList.get(
((TextView)
view)
.getText()
.toString()))
.getId());
}
});
}
}
@Override
public void onFailure(
@NonNull Call<List<Milestone>> call, @NonNull Throwable t) {
Toasty.show(ctx, getString(R.string.genericServerResponseError));
}
});
}
@Override
public void onResume() {
super.onResume();
repository.checkAccountSwitch(this);
}
}

View File

@@ -1,894 +0,0 @@
package org.mian.gitnex.activities;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.card.MaterialCardView;
import com.google.android.material.datepicker.MaterialDatePicker;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.vdurmont.emoji.EmojiParser;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.TimeZone;
import okhttp3.MediaType;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import org.apache.commons.io.FilenameUtils;
import org.gitnex.tea4j.v2.models.Attachment;
import org.gitnex.tea4j.v2.models.EditIssueOption;
import org.gitnex.tea4j.v2.models.Issue;
import org.gitnex.tea4j.v2.models.Milestone;
import org.mian.gitnex.R;
import org.mian.gitnex.adapters.AttachmentsAdapter;
import org.mian.gitnex.clients.RetrofitClient;
import org.mian.gitnex.databinding.ActivityEditIssueBinding;
import org.mian.gitnex.databinding.BottomSheetAttachmentsBinding;
import org.mian.gitnex.databinding.CustomImageViewDialogBinding;
import org.mian.gitnex.helpers.AlertDialogs;
import org.mian.gitnex.helpers.AppDatabaseSettings;
import org.mian.gitnex.helpers.AppUtil;
import org.mian.gitnex.helpers.Constants;
import org.mian.gitnex.helpers.Markdown;
import org.mian.gitnex.helpers.MentionHelper;
import org.mian.gitnex.helpers.Toasty;
import org.mian.gitnex.helpers.attachments.AttachmentUtils;
import org.mian.gitnex.helpers.attachments.AttachmentsModel;
import org.mian.gitnex.helpers.contexts.IssueContext;
import org.mian.gitnex.notifications.Notifications;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* @author mmarif
*/
public class EditIssueActivity extends BaseActivity
implements AttachmentsAdapter.AttachmentsReceiverListener {
private ActivityEditIssueBinding binding;
private final String msState = "open";
private final LinkedHashMap<String, Milestone> milestonesList = new LinkedHashMap<>();
private int milestoneId = 0;
private IssueContext issue;
private boolean renderMd = false;
private MaterialAlertDialogBuilder materialAlertDialogBuilder;
private String token;
private String filename;
private Long filesize;
private String filehash;
private String instanceUrlOnly;
private AttachmentsAdapter attachmentsAdapter;
private static List<AttachmentsModel> attachmentsList;
private static final List<Uri> contentUri = new ArrayList<>();
private MenuItem create;
private MentionHelper mentionHelper;
public ActivityResultLauncher<Intent> downloadAttachmentLauncher =
registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK) {
assert result.getData() != null;
try {
OutputStream outputStream =
getContentResolver()
.openOutputStream(
Objects.requireNonNull(
result.getData().getData()));
NotificationCompat.Builder builder =
new NotificationCompat.Builder(ctx, ctx.getPackageName())
.setContentTitle(
getString(
R.string
.fileViewerNotificationTitleStarted))
.setContentText(
getString(
R.string
.fileViewerNotificationDescriptionStarted,
filename))
.setSmallIcon(R.drawable.gitnex_transparent)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setChannelId(
Constants.downloadNotificationChannelId)
.setProgress(100, 0, false)
.setOngoing(true);
int notificationId = Notifications.uniqueNotificationId(ctx);
NotificationManager notificationManager =
(NotificationManager)
getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(notificationId, builder.build());
Thread thread =
new Thread(
() -> {
try {
Call<ResponseBody> call =
RetrofitClient.getWebInterface(
ctx,
instanceUrlOnly)
.getAttachment(filehash);
Response<ResponseBody> response =
call.execute();
assert response.body() != null;
builder.setOngoing(false)
.setContentTitle(
getString(
R.string
.fileViewerNotificationTitleFinished))
.setContentText(
getString(
R.string
.fileViewerNotificationDescriptionFinished,
filename));
AppUtil.copyProgress(
response.body().byteStream(),
outputStream,
filesize,
progress -> {
builder.setProgress(
100, progress, false);
notificationManager.notify(
notificationId,
builder.build());
});
} catch (IOException ignored) {
builder.setOngoing(false)
.setContentTitle(
getString(
R.string
.fileViewerNotificationTitleFailed))
.setContentText(
getString(
R.string
.fileViewerNotificationDescriptionFailed,
filename));
} finally {
builder.setProgress(0, 0, false)
.setOngoing(false);
notificationManager.notify(
notificationId, builder.build());
}
});
thread.start();
} catch (IOException ignored) {
}
}
});
ActivityResultLauncher<Intent> startActivityForResult =
registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK) {
Intent data = result.getData();
assert data != null;
contentUri.add(data.getData());
attachmentsList.add(
new AttachmentsModel(
AttachmentUtils.queryName(ctx, data.getData()),
data.getData()));
attachmentsAdapter.updateList(attachmentsList);
}
});
public void onDestroy() {
super.onDestroy();
AttachmentsAdapter.setAttachmentsReceiveListener(null);
mentionHelper.dismissPopup();
}
@SuppressLint("ClickableViewAccessibility")
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityEditIssueBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
int resultLimit = Constants.getCurrentResultLimit(ctx);
issue = IssueContext.fromIntent(getIntent());
binding.topAppBar.setNavigationOnClickListener(
v -> {
finish();
contentUri.clear();
});
materialAlertDialogBuilder =
new MaterialAlertDialogBuilder(ctx, R.style.ThemeOverlay_Material3_Dialog_Alert);
token = ((BaseActivity) ctx).getAccount().getAccount().getToken();
String instanceUrl = ((BaseActivity) ctx).getAccount().getAccount().getInstanceUrl();
instanceUrlOnly = instanceUrl.substring(0, instanceUrl.lastIndexOf("api/v1/"));
attachmentsList = new ArrayList<>();
attachmentsAdapter = new AttachmentsAdapter(attachmentsList, ctx);
AttachmentsAdapter.setAttachmentsReceiveListener(this);
mentionHelper = new MentionHelper(this, binding.editIssueDescription);
mentionHelper.setup();
create = binding.topAppBar.getMenu().getItem(2);
create.setTitle(getString(R.string.saveButton));
binding.editIssueDescription.setOnTouchListener(
(touchView, motionEvent) -> {
touchView.getParent().requestDisallowInterceptTouchEvent(true);
if ((motionEvent.getAction() & MotionEvent.ACTION_UP) != 0
&& (motionEvent.getActionMasked() & MotionEvent.ACTION_UP) != 0) {
touchView.getParent().requestDisallowInterceptTouchEvent(false);
}
return false;
});
if (issue.getIssueType().equalsIgnoreCase("Pull")) {
binding.topAppBar.setTitle(
getString(R.string.editPrNavHeader, String.valueOf(issue.getIssueIndex())));
} else {
binding.topAppBar.setTitle(
getString(R.string.editIssueNavHeader, String.valueOf(issue.getIssueIndex())));
}
showDatePickerDialog();
binding.topAppBar.setOnMenuItemClickListener(
menuItem -> {
int id = menuItem.getItemId();
if (id == R.id.markdown) {
if (!renderMd) {
Markdown.render(
ctx,
EmojiParser.parseToUnicode(
Objects.requireNonNull(
binding.editIssueDescription.getText())
.toString()),
binding.markdownPreview,
issue.getRepository());
binding.markdownPreview.setVisibility(View.VISIBLE);
binding.editIssueDescriptionLayout.setVisibility(View.GONE);
renderMd = true;
} else {
binding.markdownPreview.setVisibility(View.GONE);
binding.editIssueDescriptionLayout.setVisibility(View.VISIBLE);
renderMd = false;
}
return true;
} else if (id == R.id.create) {
create.setVisible(false);
processEditIssue();
if (!contentUri.isEmpty()) {
processAttachments();
contentUri.clear();
}
return true;
} else if (id == R.id.attachment) {
checkForAttachments();
return true;
} else {
return super.onOptionsItemSelected(menuItem);
}
});
getIssue(
issue.getRepository().getOwner(),
issue.getRepository().getName(),
issue.getIssueIndex(),
resultLimit);
getAttachments();
if (!issue.getRepository().getPermissions().isPush()) {
findViewById(R.id.editIssueMilestoneSpinnerLayout).setVisibility(View.GONE);
findViewById(R.id.editIssueDueDateLayout).setVisibility(View.GONE);
}
}
@Override
public void setAttachmentsData(Uri filename) {
contentUri.remove(filename);
}
private void checkForAttachments() {
if (!contentUri.isEmpty()) {
BottomSheetAttachmentsBinding bottomSheetAttachmentsBinding =
BottomSheetAttachmentsBinding.inflate(getLayoutInflater());
BottomSheetDialog bottomSheetDialog = new BottomSheetDialog(ctx);
bottomSheetAttachmentsBinding.addAttachment.setOnClickListener(
v1 -> openFileAttachmentActivity());
bottomSheetAttachmentsBinding.recyclerViewAttachments.setHasFixedSize(true);
bottomSheetAttachmentsBinding.recyclerViewAttachments.setLayoutManager(
new LinearLayoutManager(ctx));
bottomSheetAttachmentsBinding.recyclerViewAttachments.setAdapter(attachmentsAdapter);
bottomSheetDialog.setContentView(bottomSheetAttachmentsBinding.getRoot());
bottomSheetDialog.show();
} else {
attachmentsAdapter.clearAdapter();
openFileAttachmentActivity();
}
}
private void openFileAttachmentActivity() {
Intent data = new Intent(Intent.ACTION_GET_CONTENT);
data.addCategory(Intent.CATEGORY_OPENABLE);
data.setType("*/*");
Intent intent = Intent.createChooser(data, "Choose a file");
startActivityForResult.launch(intent);
}
private void processAttachments() {
for (int i = 0; i < contentUri.size(); i++) {
File file = AttachmentUtils.getFile(ctx, contentUri.get(i));
RequestBody requestFile =
RequestBody.create(
file,
MediaType.parse(
Objects.requireNonNull(
getContentResolver().getType(contentUri.get(i)))));
uploadAttachments(requestFile, file.getName());
}
}
private void uploadAttachments(RequestBody requestFile, String filename1) {
Call<Attachment> call3 =
RetrofitClient.getApiInterface(ctx)
.issueCreateIssueAttachment(
requestFile,
issue.getRepository().getOwner(),
issue.getRepository().getName(),
(long) issue.getIssueIndex(),
filename1);
call3.enqueue(
new Callback<>() {
@Override
public void onResponse(
@NonNull Call<Attachment> call,
@NonNull retrofit2.Response<Attachment> response2) {
if (response2.code() == 201) {
new Handler().postDelayed(() -> finish(), 3000);
} else if (response2.code() == 401) {
AlertDialogs.authorizationTokenRevokedDialog(ctx);
} else {
create.setVisible(true);
Toasty.show(ctx, getString(R.string.attachmentsSaveError));
}
}
@Override
public void onFailure(@NonNull Call<Attachment> call, @NonNull Throwable t) {
create.setVisible(true);
Toasty.show(ctx, getString(R.string.genericServerResponseError));
}
});
}
private void processEditIssue() {
String editIssueTitleForm =
Objects.requireNonNull(binding.editIssueTitle.getText()).toString();
String editIssueDescriptionForm =
Objects.requireNonNull(binding.editIssueDescription.getText()).toString();
String dueDate = Objects.requireNonNull(binding.editIssueDueDate.getText()).toString();
if (editIssueTitleForm.isEmpty()) {
Toasty.show(ctx, getString(R.string.issueTitleEmpty));
return;
}
editIssue(
issue.getRepository().getOwner(),
issue.getRepository().getName(),
issue.getIssueIndex(),
editIssueTitleForm,
editIssueDescriptionForm,
milestoneId,
dueDate);
}
private void editIssue(
String repoOwner,
String repoName,
int issueIndex,
String title,
String description,
int milestoneId,
String dueDate) {
EditIssueOption issueData = new EditIssueOption();
issueData.setTitle(title);
issueData.setBody(description);
String[] date = dueDate.split("-");
if (!dueDate.equalsIgnoreCase("")) {
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.YEAR, Integer.parseInt(date[0]));
calendar.set(Calendar.MONTH, Integer.parseInt(date[1]));
calendar.set(Calendar.DATE, Integer.parseInt(date[2]));
Date dueDate_ = calendar.getTime();
issueData.setDueDate(dueDate_);
}
issueData.setMilestone((long) milestoneId);
Call<Issue> call =
RetrofitClient.getApiInterface(ctx)
.issueEditIssue(repoOwner, repoName, (long) issueIndex, issueData);
call.enqueue(
new Callback<>() {
@Override
public void onResponse(
@NonNull Call<Issue> call,
@NonNull retrofit2.Response<Issue> response) {
if (response.code() == 201) {
if (issue.getIssueType().equalsIgnoreCase("Pull")) {
Toasty.show(ctx, getString(R.string.editPrSuccessMessage));
} else {
Toasty.show(ctx, getString(R.string.editIssueSuccessMessage));
}
Intent result = new Intent();
result.putExtra("issueEdited", true);
// PullRequestsFragment.resumePullRequests =
// issue.getIssue().getPullRequest() != null;
setResult(200, result);
new Handler().postDelayed(() -> finish(), 3000);
} else if (response.code() == 401) {
AlertDialogs.authorizationTokenRevokedDialog(ctx);
} else {
create.setVisible(true);
Toasty.show(ctx, getString(R.string.genericError));
}
}
@Override
public void onFailure(@NonNull Call<Issue> call, @NonNull Throwable t) {
create.setVisible(true);
}
});
}
private void showDatePickerDialog() {
MaterialDatePicker.Builder<Long> builder = MaterialDatePicker.Builder.datePicker();
builder.setSelection(Calendar.getInstance().getTimeInMillis());
builder.setTitleText(R.string.newIssueDueDateTitle);
MaterialDatePicker<Long> materialDatePicker = builder.build();
String[] locale_ =
AppDatabaseSettings.getSettingsValue(ctx, AppDatabaseSettings.APP_LOCALE_KEY)
.split("\\|");
binding.editIssueDueDate.setOnClickListener(
v -> materialDatePicker.show(getSupportFragmentManager(), "DATE_PICKER"));
materialDatePicker.addOnPositiveButtonClickListener(
selection -> {
Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
calendar.setTimeInMillis(selection);
SimpleDateFormat format =
new SimpleDateFormat("yyyy-MM-dd", new Locale(locale_[1]));
String formattedDate = format.format(calendar.getTime());
binding.editIssueDueDate.setText(formattedDate);
});
}
private void getIssue(
final String repoOwner, final String repoName, int issueIndex, int resultLimit) {
Call<Issue> call =
RetrofitClient.getApiInterface(ctx)
.issueGetIssue(repoOwner, repoName, (long) issueIndex);
call.enqueue(
new Callback<>() {
@Override
public void onResponse(
@NonNull Call<Issue> call,
@NonNull retrofit2.Response<Issue> response) {
if (response.code() == 200) {
assert response.body() != null;
binding.editIssueTitle.setText(response.body().getTitle());
binding.editIssueDescription.setText(response.body().getBody());
Milestone currentMilestone = response.body().getMilestone();
// get milestones list
if (response.body().getId() > 0) {
Call<List<Milestone>> call_ =
RetrofitClient.getApiInterface(ctx)
.issueGetMilestonesList(
repoOwner,
repoName,
msState,
null,
1,
resultLimit);
call_.enqueue(
new Callback<>() {
@Override
public void onResponse(
@NonNull Call<List<Milestone>> call,
@NonNull retrofit2.Response<List<Milestone>>
response_) {
if (response_.code() == 200) {
List<Milestone> milestonesList_ =
response_.body();
assert milestonesList_ != null;
Milestone ms = new Milestone();
ms.setId(0L);
ms.setTitle(
getString(
R.string
.issueCreatedNoMilestone));
milestonesList.put(ms.getTitle(), ms);
if (!milestonesList_.isEmpty()) {
for (Milestone milestone :
milestonesList_) {
// Don't translate "open" is a enum
if (milestone
.getState()
.equals("open")) {
milestonesList.put(
milestone.getTitle(),
milestone);
}
}
}
ArrayAdapter<String> adapter =
new ArrayAdapter<>(
EditIssueActivity.this,
R.layout.list_spinner_items,
new ArrayList<>(
milestonesList
.keySet()));
binding.editIssueMilestoneSpinner.setAdapter(
adapter);
binding.editIssueMilestoneSpinner
.setOnItemClickListener(
(parent,
view,
position,
id) -> {
if (position == 0) {
milestoneId = 0;
} else if (view
instanceof
TextView) {
milestoneId =
Math.toIntExact(
Objects
.requireNonNull(
milestonesList
.get(
((TextView)
view)
.getText()
.toString()))
.getId());
}
});
new Handler(Looper.getMainLooper())
.postDelayed(
() -> {
if (currentMilestone
!= null) {
milestoneId =
Math.toIntExact(
currentMilestone
.getId());
binding
.editIssueMilestoneSpinner
.setText(
currentMilestone
.getTitle(),
false);
} else {
milestoneId = 0;
binding
.editIssueMilestoneSpinner
.setText(
getString(
R
.string
.issueCreatedNoMilestone),
false);
}
},
500);
}
}
@Override
public void onFailure(
@NonNull Call<List<Milestone>> call,
@NonNull Throwable t) {
Log.e("onFailure", t.toString());
}
});
}
// get milestones list
if (response.body().getDueDate() != null) {
@SuppressLint("SimpleDateFormat")
DateFormat formatter = new SimpleDateFormat("yyyy-M-dd");
String dueDate = formatter.format(response.body().getDueDate());
binding.editIssueDueDate.setText(dueDate);
}
} else if (response.code() == 401) {
AlertDialogs.authorizationTokenRevokedDialog(ctx);
} else {
Toasty.show(ctx, getString(R.string.genericError));
}
}
@Override
public void onFailure(@NonNull Call<Issue> call, @NonNull Throwable t) {
// Log.e("onFailure", t.toString());
}
});
}
private void getAttachments() {
Call<List<Attachment>> call =
RetrofitClient.getApiInterface(ctx)
.issueListIssueAttachments(
issue.getRepository().getOwner(),
issue.getRepository().getName(),
(long) issue.getIssueIndex());
call.enqueue(
new Callback<>() {
@Override
public void onResponse(
@NonNull Call<List<Attachment>> call,
@NonNull retrofit2.Response<List<Attachment>> response) {
List<Attachment> attachment = response.body();
if (response.code() == 200) {
assert attachment != null;
if (!attachment.isEmpty()) {
binding.attachmentFrame.setVisibility(View.VISIBLE);
LinearLayout.LayoutParams paramsAttachment =
new LinearLayout.LayoutParams(96, 96);
paramsAttachment.setMargins(0, 0, 48, 0);
for (int i = 0; i < attachment.size(); i++) {
ImageView attachmentView = new ImageView(ctx);
MaterialCardView materialCardView = new MaterialCardView(ctx);
materialCardView.setLayoutParams(paramsAttachment);
materialCardView.setStrokeWidth(0);
materialCardView.setRadius(28);
materialCardView.setCardBackgroundColor(Color.TRANSPARENT);
if (Arrays.asList(
"bmp", "gif", "jpg", "jpeg", "png", "webp",
"heic", "heif")
.contains(
FilenameUtils.getExtension(
attachment.get(i).getName())
.toLowerCase())) {
Glide.with(ctx)
.load(
attachment.get(i).getBrowserDownloadUrl()
+ "?token="
+ token)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.placeholder(R.drawable.loader_animated)
.centerCrop()
.error(R.drawable.ic_close)
.into(attachmentView);
binding.attachmentsView.addView(materialCardView);
attachmentView.setLayoutParams(paramsAttachment);
materialCardView.addView(attachmentView);
int finalI1 = i;
materialCardView.setOnClickListener(
v1 ->
imageViewDialog(
attachment
.get(finalI1)
.getBrowserDownloadUrl()));
} else {
attachmentView.setImageResource(
R.drawable.ic_file_download);
attachmentView.setPadding(4, 4, 4, 4);
binding.attachmentsView.addView(materialCardView);
attachmentView.setLayoutParams(paramsAttachment);
materialCardView.addView(attachmentView);
int finalI = i;
materialCardView.setOnClickListener(
v1 -> {
filesize = attachment.get(finalI).getSize();
filename = attachment.get(finalI).getName();
filehash = attachment.get(finalI).getUuid();
requestFileDownload();
});
}
}
} else {
binding.attachmentFrame.setVisibility(View.GONE);
}
}
}
@Override
public void onFailure(
@NonNull Call<List<Attachment>> call, @NonNull Throwable t) {}
});
}
private void requestFileDownload() {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.putExtra(Intent.EXTRA_TITLE, filename);
intent.setType("*/*");
downloadAttachmentLauncher.launch(intent);
}
private void imageViewDialog(String url) {
CustomImageViewDialogBinding imageViewDialogBinding =
CustomImageViewDialogBinding.inflate(LayoutInflater.from(ctx));
View view = imageViewDialogBinding.getRoot();
materialAlertDialogBuilder.setView(view);
materialAlertDialogBuilder.setNeutralButton(getString(R.string.close), null);
Glide.with(ctx)
.asBitmap()
.load(url + "?token=" + token)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.placeholder(R.drawable.loader_animated)
.centerCrop()
.error(R.drawable.ic_close)
.into(
new CustomTarget<Bitmap>() {
@Override
public void onResourceReady(
@NonNull Bitmap resource,
Transition<? super Bitmap> transition) {
imageViewDialogBinding.imageView.setImageBitmap(resource);
imageViewDialogBinding.imageView.buildDrawingCache();
}
@Override
public void onLoadCleared(Drawable placeholder) {}
});
materialAlertDialogBuilder.create().show();
}
@Override
public void onResume() {
super.onResume();
issue.getRepository().checkAccountSwitch(this);
}
}

View File

@@ -21,6 +21,7 @@ import org.mian.gitnex.R;
import org.mian.gitnex.databinding.ActivityRepoDetailBinding;
import org.mian.gitnex.fragments.BottomSheetCreateIssue;
import org.mian.gitnex.fragments.BottomSheetCreateMilestone;
import org.mian.gitnex.fragments.BottomSheetCreatePullRequest;
import org.mian.gitnex.fragments.BottomSheetCreateRelease;
import org.mian.gitnex.fragments.BottomSheetCreateWiki;
import org.mian.gitnex.fragments.BottomsheetRepoMenu;
@@ -618,7 +619,8 @@ public class RepoDetailActivity extends BaseActivity
case "pullNew":
switchTab("prs", R.id.btn_nav_prs);
startActivity(repository.getIntent(this, CreatePullRequestActivity.class));
BottomSheetCreatePullRequest.newInstance(repository, null)
.show(getSupportFragmentManager(), "CREATE_PULL_REQUEST");
break;
case "releases":

View File

@@ -5,10 +5,12 @@ import android.content.Context;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.RequestOptions;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.gitnex.tea4j.v2.models.User;
@@ -25,16 +27,30 @@ public class AssigneeSelectionAdapter
private final List<User> assignees;
private final Set<String> selectedAssignees;
private final RequestOptions avatarOptions;
private final String excludeUser;
private final List<User> filteredAssignees = new ArrayList<>();
public AssigneeSelectionAdapter(List<User> assignees, Set<String> selectedAssignees) {
public AssigneeSelectionAdapter(
List<User> assignees, Set<String> selectedAssignees, @Nullable String excludeUser) {
this.assignees = assignees;
this.selectedAssignees = selectedAssignees;
this.excludeUser = excludeUser;
this.avatarOptions =
new RequestOptions()
.diskCacheStrategy(DiskCacheStrategy.ALL)
.placeholder(R.drawable.loader_animated)
.error(R.drawable.ic_person)
.centerCrop();
filterAssignees();
}
private void filterAssignees() {
filteredAssignees.clear();
for (User user : assignees) {
if (excludeUser == null || !excludeUser.equals(user.getLogin())) {
filteredAssignees.add(user);
}
}
}
@NonNull @Override
@@ -48,7 +64,7 @@ public class AssigneeSelectionAdapter
@SuppressLint("SetTextI18n")
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
User user = assignees.get(position);
User user = filteredAssignees.get(position);
Context context = holder.itemView.getContext();
String fullName = user.getFullName();
@@ -87,13 +103,18 @@ public class AssigneeSelectionAdapter
@Override
public int getItemCount() {
return assignees != null ? assignees.size() : 0;
return filteredAssignees.size();
}
public boolean isEmpty() {
return filteredAssignees.isEmpty();
}
@SuppressLint("NotifyDataSetChanged")
public void updateList(List<User> newList) {
this.assignees.clear();
this.assignees.addAll(newList);
filterAssignees();
notifyDataSetChanged();
}

View File

@@ -6,9 +6,7 @@ import android.content.Intent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.vdurmont.emoji.EmojiParser;
@@ -19,9 +17,7 @@ import java.util.Locale;
import java.util.Objects;
import org.apache.commons.lang3.StringUtils;
import org.mian.gitnex.R;
import org.mian.gitnex.activities.BaseActivity;
import org.mian.gitnex.activities.CreateNoteActivity;
import org.mian.gitnex.activities.CreatePullRequestActivity;
import org.mian.gitnex.database.api.BaseApi;
import org.mian.gitnex.database.api.NotesApi;
import org.mian.gitnex.database.models.Notes;
@@ -71,8 +67,6 @@ public class NotesAdapter extends RecyclerView.Adapter<NotesAdapter.NotesViewHol
if ("insert".equalsIgnoreCase(insert)) {
if (itemClickListener != null) {
itemClickListener.onItemClick(note);
} else {
performInsert();
}
} else {
Intent intent = new Intent(ctx, CreateNoteActivity.class);
@@ -99,27 +93,6 @@ public class NotesAdapter extends RecyclerView.Adapter<NotesAdapter.NotesViewHol
});
}
private void performInsert() {
if (!(ctx instanceof BaseActivity activity)) return;
EditText targetField = null;
AlertDialog dialogToDismiss = null;
if (activity instanceof CreatePullRequestActivity prAct) {
targetField = prAct.findViewById(R.id.prBody);
dialogToDismiss = prAct.dialogNotes;
}
if (targetField != null) {
targetField.append(note.getContent());
if (dialogToDismiss != null && dialogToDismiss.isShowing()) {
dialogToDismiss.dismiss();
}
}
}
public void bind(Notes note) {
this.note = note;

View File

@@ -24,6 +24,7 @@ import org.mian.gitnex.activities.IssueDetailActivity;
import org.mian.gitnex.activities.ProfileActivity;
import org.mian.gitnex.activities.RepoDetailActivity;
import org.mian.gitnex.databinding.ListPrBinding;
import org.mian.gitnex.fragments.BottomSheetCreatePullRequest;
import org.mian.gitnex.helpers.AppDatabaseSettings;
import org.mian.gitnex.helpers.AppUtil;
import org.mian.gitnex.helpers.AvatarGenerator;
@@ -31,6 +32,7 @@ import org.mian.gitnex.helpers.Markdown;
import org.mian.gitnex.helpers.TimeHelper;
import org.mian.gitnex.helpers.Toasty;
import org.mian.gitnex.helpers.contexts.IssueContext;
import org.mian.gitnex.helpers.contexts.RepositoryContext;
/**
* @author mmarif
@@ -146,6 +148,20 @@ public class PullRequestsAdapter
binding.mergedBadge.setVisibility(View.GONE);
}
// TEMPORARY: remove later once PR edit is implemented
binding.prNumber.setOnClickListener(
v -> {
if (context instanceof RepoDetailActivity) {
RepositoryContext repository =
((RepoDetailActivity) context).repository;
BottomSheetCreatePullRequest.newInstance(repository, pr)
.show(
((RepoDetailActivity) context)
.getSupportFragmentManager(),
"EDIT_PULL_REQUEST");
}
});
binding.userName.setText(pr.getUser().getLogin());
binding.repoFullName.setText(pr.getBase().getRepo().getFullName());
binding.prNumber.setText(context.getString(R.string.hash_with_text, pr.getNumber()));

View File

@@ -44,7 +44,6 @@ public class BottomSheetAddCollaborator extends BottomSheetDialogFragment
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(STYLE_NORMAL, R.style.Custom_BottomSheet);
if (getArguments() != null) {
repository = RepositoryContext.fromBundle(getArguments());
}

View File

@@ -43,7 +43,6 @@ public class BottomSheetAddTeamMember extends BottomSheetDialogFragment {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(STYLE_NORMAL, R.style.Custom_BottomSheet);
}
@Nullable @Override
@@ -74,6 +73,8 @@ public class BottomSheetAddTeamMember extends BottomSheetDialogFragment {
viewModel.loadCurrentMembers(requireContext(), teamId);
}
binding.sheetTitle.setText(R.string.add_team_member);
setupRecyclerView();
setupListeners();
observeViewModel();

View File

@@ -12,7 +12,6 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import java.util.ArrayList;
import org.mian.gitnex.R;
import org.mian.gitnex.adapters.OrganizationTeamRepositoriesAdapter;
import org.mian.gitnex.databinding.BottomsheetAddTeamRepoBinding;
import org.mian.gitnex.helpers.AppUtil;
@@ -42,7 +41,6 @@ public class BottomSheetAddTeamRepo extends BottomSheetDialogFragment {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(STYLE_NORMAL, R.style.Custom_BottomSheet);
if (getArguments() != null) {
teamId = getArguments().getLong("teamId");
orgName = getArguments().getString("orgName");

View File

@@ -42,16 +42,25 @@ public class BottomSheetAssigneesPicker extends BottomSheetDialogFragment {
private AssigneesViewModel assigneesViewModel;
private AssigneeSelectionAdapter adapter;
private int resultLimit;
private String excludeUser = null;
public static BottomSheetAssigneesPicker newInstance(
RepositoryContext repo, List<String> current) {
RepositoryContext repo, List<String> current, @Nullable String excludeUser) {
BottomSheetAssigneesPicker f = new BottomSheetAssigneesPicker();
Bundle b = repo.getBundle();
b.putStringArrayList("current_assignees", new ArrayList<>(current));
if (excludeUser != null) {
b.putString("exclude_user", excludeUser);
}
f.setArguments(b);
return f;
}
public static BottomSheetAssigneesPicker newInstance(
RepositoryContext repo, List<String> current) {
return newInstance(repo, current, null);
}
public void setOnAssigneesSelectedListener(OnAssigneesSelectedListener l) {
this.listener = l;
}
@@ -68,6 +77,7 @@ public class BottomSheetAssigneesPicker extends BottomSheetDialogFragment {
repository = RepositoryContext.fromBundle(args);
selectedAssignees =
new HashSet<>(Objects.requireNonNull(args.getStringArrayList("current_assignees")));
excludeUser = args.getString("exclude_user");
resultLimit = Constants.getCurrentResultLimit(requireContext());
setupRecyclerView();
@@ -101,7 +111,7 @@ public class BottomSheetAssigneesPicker extends BottomSheetDialogFragment {
private void setupRecyclerView() {
LinearLayoutManager layoutManager = new LinearLayoutManager(requireContext());
binding.rvAssignees.setLayoutManager(layoutManager);
adapter = new AssigneeSelectionAdapter(new ArrayList<>(), selectedAssignees);
adapter = new AssigneeSelectionAdapter(new ArrayList<>(), selectedAssignees, excludeUser);
binding.rvAssignees.setAdapter(adapter);
EndlessRecyclerViewScrollListener scrollListener =
@@ -122,9 +132,12 @@ public class BottomSheetAssigneesPicker extends BottomSheetDialogFragment {
list -> {
List<User> data = (list != null) ? list : new ArrayList<>();
adapter.updateList(data);
if (!data.isEmpty()) {
if (!adapter.isEmpty()) {
binding.rvAssignees.setVisibility(View.VISIBLE);
binding.layoutEmpty.getRoot().setVisibility(View.GONE);
} else {
binding.rvAssignees.setVisibility(View.GONE);
binding.layoutEmpty.getRoot().setVisibility(View.VISIBLE);
}
});

View File

@@ -0,0 +1,960 @@
package org.mian.gitnex.fragments;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Dialog;
import android.content.ClipData;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.google.android.material.datepicker.MaterialDatePicker;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.TimeZone;
import org.gitnex.tea4j.v2.models.CreatePullRequestOption;
import org.gitnex.tea4j.v2.models.EditPullRequestOption;
import org.gitnex.tea4j.v2.models.Label;
import org.gitnex.tea4j.v2.models.PullRequest;
import org.gitnex.tea4j.v2.models.User;
import org.mian.gitnex.R;
import org.mian.gitnex.adapters.CreateAttachmentsAdapter;
import org.mian.gitnex.database.api.BaseApi;
import org.mian.gitnex.database.api.UserAccountsApi;
import org.mian.gitnex.database.models.UserAccount;
import org.mian.gitnex.databinding.BottomsheetCreatePullRequestBinding;
import org.mian.gitnex.helpers.AlertDialogs;
import org.mian.gitnex.helpers.AppDatabaseSettings;
import org.mian.gitnex.helpers.AppUtil;
import org.mian.gitnex.helpers.TinyDB;
import org.mian.gitnex.helpers.Toasty;
import org.mian.gitnex.helpers.attachments.AttachmentManager;
import org.mian.gitnex.helpers.contexts.RepositoryContext;
import org.mian.gitnex.viewmodels.AttachmentsViewModel;
import org.mian.gitnex.viewmodels.PullRequestsViewModel;
/**
* @author mmarif
*/
public class BottomSheetCreatePullRequest extends BottomSheetDialogFragment {
private BottomsheetCreatePullRequestBinding binding;
private PullRequestsViewModel viewModel;
private RepositoryContext repoContext;
private PullRequest prToEdit;
private String selectedMergeInto = null;
private String selectedPullFrom = null;
private Set<String> selectedLabels = new HashSet<>();
private final List<Long> selectedLabelIds = new ArrayList<>();
private String selectedMilestone = null;
private Long selectedMilestoneId = null;
private Set<String> selectedAssignees = new HashSet<>();
private Set<String> selectedReviewers = new HashSet<>();
private String selectedDueDate = null;
private int maxAttachmentSize = -1;
private int maxNumberOfAttachments = -1;
private AttachmentManager attachmentManager;
private AttachmentsViewModel attachmentsViewModel;
protected TinyDB tinyDB;
public static BottomSheetCreatePullRequest newInstance(
RepositoryContext repository, @Nullable PullRequest pr) {
BottomSheetCreatePullRequest fragment = new BottomSheetCreatePullRequest();
Bundle args = new Bundle();
args.putSerializable("repo_context", repository);
if (pr != null) {
args.putSerializable("pr_item", pr);
}
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
repoContext = (RepositoryContext) getArguments().getSerializable("repo_context");
prToEdit = (PullRequest) getArguments().getSerializable("pr_item");
}
}
@Nullable @Override
public View onCreateView(
@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
binding = BottomsheetCreatePullRequestBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
viewModel = new ViewModelProvider(requireActivity()).get(PullRequestsViewModel.class);
attachmentsViewModel =
new ViewModelProvider(requireActivity()).get(AttachmentsViewModel.class);
this.tinyDB = TinyDB.getInstance(requireContext());
viewModel.clearCreatedPr();
viewModel.clearUpdatedPr();
attachmentsViewModel.reset();
setupUI();
setupListeners();
setupAttachments();
observeViewModel();
observeAttachmentsViewModel();
}
private void setupUI() {
boolean hasWriteAccess =
repoContext.getPermissions() != null && repoContext.getPermissions().isPush();
binding.cardMergeInto.cardIcon.setImageResource(R.drawable.ic_branch);
binding.cardMergeInto.tvCardLabel.setText(R.string.mergeIntoBranch);
binding.cardMergeInto.getRoot().setVisibility(hasWriteAccess ? View.VISIBLE : View.GONE);
binding.cardPullFrom.cardIcon.setImageResource(R.drawable.ic_branch);
binding.cardPullFrom.tvCardLabel.setText(R.string.pullFromBranch);
binding.cardPullFrom.getRoot().setVisibility(hasWriteAccess ? View.VISIBLE : View.GONE);
binding.cardMilestone.cardIcon.setImageResource(R.drawable.ic_milestone);
binding.cardMilestone.tvCardLabel.setText(R.string.milestone);
binding.cardMilestone.getRoot().setVisibility(hasWriteAccess ? View.VISIBLE : View.GONE);
binding.cardLabels.cardIcon.setImageResource(R.drawable.ic_label);
binding.cardLabels.tvCardLabel.setText(R.string.newIssueLabelsTitle);
binding.cardLabels.getRoot().setVisibility(hasWriteAccess ? View.VISIBLE : View.GONE);
binding.cardAssignees.cardIcon.setImageResource(R.drawable.ic_person);
binding.cardAssignees.tvCardLabel.setText(R.string.newIssueAssigneesListTitle);
binding.cardAssignees.getRoot().setVisibility(hasWriteAccess ? View.VISIBLE : View.GONE);
binding.cardReviewers.cardIcon.setImageResource(R.drawable.ic_followers);
binding.cardReviewers.tvCardLabel.setText(R.string.reviewers);
binding.cardReviewers.getRoot().setVisibility(hasWriteAccess ? View.VISIBLE : View.GONE);
binding.cardDueDate.cardIcon.setImageResource(R.drawable.ic_calendar);
binding.cardDueDate.tvCardLabel.setText(R.string.newIssueDueDateTitle);
binding.cardDueDate.getRoot().setVisibility(hasWriteAccess ? View.VISIBLE : View.GONE);
if (prToEdit != null) {
binding.sheetTitle.setText(R.string.edit_pr);
binding.prTitle.setText(prToEdit.getTitle());
binding.prBody.setText(prToEdit.getBody());
binding.btnSubmit.setText(R.string.update);
binding.cardMergeInto.getRoot().setVisibility(View.GONE);
binding.cardPullFrom.getRoot().setVisibility(View.GONE);
binding.cardReviewers.getRoot().setVisibility(View.GONE);
binding.switchAllowMaintainerEdit.setVisibility(View.VISIBLE);
if (hasWriteAccess) {
if (prToEdit.getLabels() != null && !prToEdit.getLabels().isEmpty()) {
for (Label label : prToEdit.getLabels()) {
selectedLabels.add(label.getName());
if (label.getId() != null) {
selectedLabelIds.add(label.getId());
}
}
updateLabelsDisplay();
}
if (prToEdit.getMilestone() != null) {
selectedMilestone = prToEdit.getMilestone().getTitle();
selectedMilestoneId = prToEdit.getMilestone().getId();
updateMilestoneDisplay();
}
if (prToEdit.getAssignees() != null && !prToEdit.getAssignees().isEmpty()) {
for (User assignee : prToEdit.getAssignees()) {
selectedAssignees.add(assignee.getLogin());
}
updateAssigneesDisplay();
}
if (prToEdit.getDueDate() != null) {
selectedDueDate = formatDateForDisplay(prToEdit.getDueDate());
updateDueDateDisplay();
}
if (prToEdit.getBase() != null && prToEdit.getBase().getRef() != null) {
selectedMergeInto = prToEdit.getBase().getRef();
updateMergeIntoDisplay();
}
if (prToEdit.getHead() != null && prToEdit.getHead().getRef() != null) {
selectedPullFrom = prToEdit.getHead().getRef();
updatePullFromDisplay();
}
}
} else {
binding.sheetTitle.setText(R.string.create_pr);
binding.btnSubmit.setText(R.string.create_pr);
binding.switchAllowMaintainerEdit.setVisibility(View.GONE);
if (hasWriteAccess) {
updateLabelsDisplay();
updateMilestoneDisplay();
updateAssigneesDisplay();
updateReviewersDisplay();
updateDueDateDisplay();
updateMergeIntoDisplay();
updatePullFromDisplay();
}
}
if (hasWriteAccess) {
updateClearButtonVisibility();
updateMilestoneClearButtonVisibility();
updateAssigneesClearButtonVisibility();
updateReviewersClearButtonVisibility();
updateDueDateClearButtonVisibility();
updateMergeIntoClearButtonVisibility();
updatePullFromClearButtonVisibility();
}
}
private void setupListeners() {
binding.btnClose.setOnClickListener(v -> dismiss());
binding.btnExpand.setOnClickListener(v -> openFullScreenEditor());
binding.btnSubmit.setOnClickListener(v -> submitAction());
boolean hasWriteAccess =
repoContext.getPermissions() != null && repoContext.getPermissions().isPush();
binding.cardMergeInto.getRoot().setOnClickListener(v -> openBranchPicker("merge"));
binding.cardMergeInto.btnClear.setOnClickListener(
v -> {
selectedMergeInto = null;
updateMergeIntoDisplay();
updateMergeIntoClearButtonVisibility();
});
binding.cardPullFrom.getRoot().setOnClickListener(v -> openBranchPicker("pull"));
binding.cardPullFrom.btnClear.setOnClickListener(
v -> {
selectedPullFrom = null;
updatePullFromDisplay();
updatePullFromClearButtonVisibility();
});
if (hasWriteAccess) {
binding.cardMilestone.getRoot().setOnClickListener(v -> openMilestonePicker());
binding.cardMilestone.btnClear.setOnClickListener(
v -> {
selectedMilestone = null;
selectedMilestoneId = null;
updateMilestoneDisplay();
updateMilestoneClearButtonVisibility();
});
binding.cardLabels.getRoot().setOnClickListener(v -> openLabelPicker());
binding.cardLabels.btnClear.setOnClickListener(
v -> {
selectedLabels.clear();
selectedLabelIds.clear();
updateLabelsDisplay();
updateClearButtonVisibility();
});
binding.cardAssignees.getRoot().setOnClickListener(v -> openAssigneesPicker());
binding.cardAssignees.btnClear.setOnClickListener(
v -> {
selectedAssignees.clear();
updateAssigneesDisplay();
updateAssigneesClearButtonVisibility();
});
binding.cardReviewers.getRoot().setOnClickListener(v -> openReviewersPicker());
binding.cardReviewers.btnClear.setOnClickListener(
v -> {
selectedReviewers.clear();
updateReviewersDisplay();
updateReviewersClearButtonVisibility();
});
binding.cardDueDate.getRoot().setOnClickListener(v -> openDatePicker());
binding.cardDueDate.btnClear.setOnClickListener(
v -> {
selectedDueDate = null;
updateDueDateDisplay();
updateDueDateClearButtonVisibility();
});
}
binding.cardAttachments.btnAddAttachment.setOnClickListener(
v -> {
if (attachmentManager != null) {
attachmentManager.openFilePicker();
}
});
}
private void openFullScreenEditor() {
BottomSheetFullScreenEditor editorBottomSheet =
BottomSheetFullScreenEditor.newInstance(
Objects.requireNonNull(binding.prBody.getText()).toString(),
repoContext,
true,
true);
editorBottomSheet.setEditorListener(
newContent -> {
binding.prBody.setText(newContent);
binding.prBody.setSelection(newContent != null ? newContent.length() : 0);
});
editorBottomSheet.show(getParentFragmentManager(), "FULLSCREEN_EDITOR");
}
private void submitAction() {
String title =
binding.prTitle.getText() != null
? binding.prTitle.getText().toString().trim()
: "";
String body =
binding.prBody.getText() != null ? binding.prBody.getText().toString().trim() : "";
if (title.isEmpty()) {
Toasty.show(requireContext(), R.string.titleError);
return;
}
if (selectedMergeInto == null || selectedMergeInto.isEmpty()) {
Toasty.show(requireContext(), R.string.mergeIntoError);
return;
}
if (selectedPullFrom == null || selectedPullFrom.isEmpty()) {
Toasty.show(requireContext(), R.string.pullFromError);
return;
}
if (selectedMergeInto.equals(selectedPullFrom)) {
Toasty.show(requireContext(), R.string.sameBranchesError);
return;
}
if (isCurrentUserInReviewers()) {
Toasty.show(requireContext(), R.string.cannotAddSelfAsReviewer);
return;
}
if (prToEdit != null) {
submitUpdatePr(title, body);
} else {
submitCreatePr(title, body);
}
}
private boolean isCurrentUserInReviewers() {
if (selectedReviewers.isEmpty()) {
return false;
}
UserAccountsApi userAccountsApi =
BaseApi.getInstance(requireContext(), UserAccountsApi.class);
if (userAccountsApi != null) {
int currentAccountId = tinyDB.getInt("currentActiveAccountId", -1);
UserAccount currentAccount = userAccountsApi.getAccountById(currentAccountId);
if (currentAccount != null && currentAccount.getUserName() != null) {
return selectedReviewers.contains(currentAccount.getUserName());
}
}
return false;
}
private void submitCreatePr(String title, String body) {
CreatePullRequestOption prData = new CreatePullRequestOption();
prData.setTitle(title);
prData.setBody(body);
prData.setBase(selectedMergeInto);
prData.setHead(selectedPullFrom);
if (selectedMilestoneId != null) {
prData.setMilestone(selectedMilestoneId);
}
if (!selectedLabelIds.isEmpty()) {
prData.setLabels(new ArrayList<>(selectedLabelIds));
}
if (!selectedAssignees.isEmpty()) {
prData.setAssignees(new ArrayList<>(selectedAssignees));
}
if (!selectedReviewers.isEmpty()) {
prData.setReviewers(new ArrayList<>(selectedReviewers));
}
Date dueDate = getDueDateForApi();
if (dueDate != null) {
prData.setDueDate(dueDate);
}
viewModel.createPullRequest(
requireContext(), repoContext.getOwner(), repoContext.getName(), prData);
}
private void submitUpdatePr(String title, String body) {
EditPullRequestOption prData = new EditPullRequestOption();
prData.setTitle(title);
prData.setBody(body);
prData.setAllowMaintainerEdit(binding.switchAllowMaintainerEdit.isChecked());
if (selectedMilestoneId != null) {
prData.setMilestone(selectedMilestoneId);
}
if (!selectedLabelIds.isEmpty()) {
prData.setLabels(new ArrayList<>(selectedLabelIds));
}
if (!selectedAssignees.isEmpty()) {
prData.setAssignees(new ArrayList<>(selectedAssignees));
}
Date dueDate = getDueDateForApi();
if (dueDate != null) {
prData.setDueDate(dueDate);
}
viewModel.updatePullRequest(
requireContext(),
repoContext.getOwner(),
repoContext.getName(),
prToEdit.getNumber(),
prData);
}
private void observeViewModel() {
viewModel
.getIsCreating()
.observe(
getViewLifecycleOwner(),
isCreating -> {
if (prToEdit == null) {
binding.loadingIndicator.setVisibility(
isCreating ? View.VISIBLE : View.GONE);
binding.btnSubmit.setEnabled(!isCreating);
binding.btnSubmit.setText(
isCreating ? "" : getString(R.string.create_pr));
}
});
viewModel
.getCreatedPr()
.observe(
getViewLifecycleOwner(),
pr -> {
if (pr != null) {
handlePrSuccess(pr.getNumber());
}
});
viewModel
.getCreateError()
.observe(
getViewLifecycleOwner(),
error -> {
if (error != null && !error.isEmpty()) {
handleError(error);
viewModel.clearCreateError();
}
});
viewModel
.getIsUpdating()
.observe(
getViewLifecycleOwner(),
isUpdating -> {
if (prToEdit != null) {
binding.loadingIndicator.setVisibility(
isUpdating ? View.VISIBLE : View.GONE);
binding.btnSubmit.setEnabled(!isUpdating);
binding.btnSubmit.setText(
isUpdating ? "" : getString(R.string.update));
}
});
viewModel
.getUpdatedPr()
.observe(
getViewLifecycleOwner(),
pr -> {
if (pr != null) {
handlePrSuccess(pr.getNumber());
}
});
viewModel
.getUpdateError()
.observe(
getViewLifecycleOwner(),
error -> {
if (error != null && !error.isEmpty()) {
handleError(error);
viewModel.clearUpdateError();
}
});
}
private void handleError(String error) {
if (error.equals("UNAUTHORIZED")) {
AlertDialogs.authorizationTokenRevokedDialog(requireContext());
} else {
Toasty.show(requireContext(), error);
}
}
private void openBranchPicker(String type) {
BottomsheetBranchPicker branchPicker =
BottomsheetBranchPicker.newInstance(
repoContext.getOwner(),
repoContext.getName(),
type.equals("merge") ? selectedMergeInto : selectedPullFrom);
branchPicker.setOnBranchSelectedListener(
branchName -> {
if (type.equals("merge")) {
selectedMergeInto = branchName;
updateMergeIntoDisplay();
updateMergeIntoClearButtonVisibility();
} else {
selectedPullFrom = branchName;
updatePullFromDisplay();
updatePullFromClearButtonVisibility();
}
});
branchPicker.show(getParentFragmentManager(), "BRANCH_PICKER_" + type.toUpperCase());
}
private void updateMergeIntoDisplay() {
if (selectedMergeInto == null || selectedMergeInto.isEmpty()) {
binding.cardMergeInto.tvSelectedText.setText(R.string.add_release_branch);
} else {
binding.cardMergeInto.tvSelectedText.setText(selectedMergeInto);
}
}
private void updateMergeIntoClearButtonVisibility() {
binding.cardMergeInto.btnClear.setVisibility(
selectedMergeInto == null || selectedMergeInto.isEmpty()
? View.GONE
: View.VISIBLE);
}
private void updatePullFromDisplay() {
if (selectedPullFrom == null || selectedPullFrom.isEmpty()) {
binding.cardPullFrom.tvSelectedText.setText(R.string.add_release_branch);
} else {
binding.cardPullFrom.tvSelectedText.setText(selectedPullFrom);
}
}
private void updatePullFromClearButtonVisibility() {
binding.cardPullFrom.btnClear.setVisibility(
selectedPullFrom == null || selectedPullFrom.isEmpty() ? View.GONE : View.VISIBLE);
}
private void openLabelPicker() {
BottomSheetLabelPicker labelPicker =
BottomSheetLabelPicker.newInstance(repoContext, new ArrayList<>(selectedLabels));
labelPicker.setOnLabelsSelectedWithIdsListener(
(selected, labelIds) -> {
selectedLabels = selected;
selectedLabelIds.clear();
selectedLabelIds.addAll(labelIds.values());
updateLabelsDisplay();
updateClearButtonVisibility();
});
labelPicker.show(getParentFragmentManager(), "LABEL_PICKER");
}
private void updateLabelsDisplay() {
if (selectedLabels.isEmpty()) {
binding.cardLabels.tvSelectedText.setText(R.string.add_labels);
} else {
binding.cardLabels.tvSelectedText.setText(String.join(", ", selectedLabels));
}
}
private void updateClearButtonVisibility() {
binding.cardLabels.btnClear.setVisibility(
selectedLabels.isEmpty() ? View.GONE : View.VISIBLE);
}
private void openMilestonePicker() {
List<String> current =
selectedMilestone != null
? Collections.singletonList(selectedMilestone)
: new ArrayList<>();
BottomSheetMilestonePicker milestonePicker =
BottomSheetMilestonePicker.newInstance(repoContext, current);
milestonePicker.setOnMilestonesSelectedWithIdsListener(
(selected, milestoneIds) -> {
if (selected.isEmpty()) {
selectedMilestone = null;
selectedMilestoneId = null;
} else {
selectedMilestone = selected.iterator().next();
selectedMilestoneId = milestoneIds.get(selectedMilestone);
}
updateMilestoneDisplay();
updateMilestoneClearButtonVisibility();
});
milestonePicker.show(getParentFragmentManager(), "MILESTONE_PICKER");
}
private void updateMilestoneDisplay() {
if (selectedMilestone == null || selectedMilestone.isEmpty()) {
binding.cardMilestone.tvSelectedText.setText(R.string.add_milestone);
} else {
binding.cardMilestone.tvSelectedText.setText(selectedMilestone);
}
}
private void updateMilestoneClearButtonVisibility() {
binding.cardMilestone.btnClear.setVisibility(
selectedMilestone == null || selectedMilestone.isEmpty()
? View.GONE
: View.VISIBLE);
}
private void openAssigneesPicker() {
BottomSheetAssigneesPicker assigneesPicker =
BottomSheetAssigneesPicker.newInstance(
repoContext, new ArrayList<>(selectedAssignees));
assigneesPicker.setOnAssigneesSelectedListener(
selected -> {
selectedAssignees = selected;
updateAssigneesDisplay();
updateAssigneesClearButtonVisibility();
});
assigneesPicker.show(getParentFragmentManager(), "ASSIGNEES_PICKER");
}
private void updateAssigneesDisplay() {
if (selectedAssignees.isEmpty()) {
binding.cardAssignees.tvSelectedText.setText(R.string.add_assignees);
} else {
binding.cardAssignees.tvSelectedText.setText(String.join(", ", selectedAssignees));
}
}
private void updateAssigneesClearButtonVisibility() {
binding.cardAssignees.btnClear.setVisibility(
selectedAssignees.isEmpty() ? View.GONE : View.VISIBLE);
}
private void openReviewersPicker() {
String currentUser = getCurrentUserName();
BottomSheetAssigneesPicker reviewersPicker =
BottomSheetAssigneesPicker.newInstance(
repoContext, new ArrayList<>(selectedReviewers), currentUser);
reviewersPicker.setOnAssigneesSelectedListener(
selected -> {
selectedReviewers = selected;
updateReviewersDisplay();
updateReviewersClearButtonVisibility();
});
reviewersPicker.show(getParentFragmentManager(), "REVIEWERS_PICKER");
}
private String getCurrentUserName() {
UserAccountsApi userAccountsApi =
BaseApi.getInstance(requireContext(), UserAccountsApi.class);
if (userAccountsApi != null) {
int currentAccountId = tinyDB.getInt("currentActiveAccountId", -1);
UserAccount currentAccount = userAccountsApi.getAccountById(currentAccountId);
if (currentAccount != null) {
return currentAccount.getUserName();
}
}
return null;
}
private void updateReviewersDisplay() {
if (selectedReviewers.isEmpty()) {
binding.cardReviewers.tvSelectedText.setText(R.string.add_reviewers);
} else {
binding.cardReviewers.tvSelectedText.setText(String.join(", ", selectedReviewers));
}
}
private void updateReviewersClearButtonVisibility() {
binding.cardReviewers.btnClear.setVisibility(
selectedReviewers.isEmpty() ? View.GONE : View.VISIBLE);
}
private void openDatePicker() {
MaterialDatePicker<Long> datePicker = getLongMaterialDatePicker();
datePicker.addOnPositiveButtonClickListener(
selection -> {
Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
calendar.setTimeInMillis(selection);
String[] locale_ =
AppDatabaseSettings.getSettingsValue(
requireContext(), AppDatabaseSettings.APP_LOCALE_KEY)
.split("\\|");
SimpleDateFormat format =
new SimpleDateFormat("yyyy-MM-dd", new Locale(locale_[1]));
selectedDueDate = format.format(calendar.getTime());
updateDueDateDisplay();
updateDueDateClearButtonVisibility();
});
datePicker.show(getParentFragmentManager(), "DATE_PICKER");
}
@NonNull private MaterialDatePicker<Long> getLongMaterialDatePicker() {
MaterialDatePicker.Builder<Long> builder = MaterialDatePicker.Builder.datePicker();
builder.setTitleText(R.string.newIssueDueDateTitle);
if (selectedDueDate != null) {
try {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
Date date = sdf.parse(selectedDueDate);
if (date != null) {
builder.setSelection(date.getTime());
}
} catch (ParseException e) {
builder.setSelection(Calendar.getInstance().getTimeInMillis());
}
} else {
builder.setSelection(Calendar.getInstance().getTimeInMillis());
}
return builder.build();
}
private void updateDueDateDisplay() {
if (selectedDueDate == null || selectedDueDate.isEmpty()) {
binding.cardDueDate.tvSelectedText.setText(R.string.add_due_date);
} else {
binding.cardDueDate.tvSelectedText.setText(formatDateForDisplay(selectedDueDate));
}
}
private void updateDueDateClearButtonVisibility() {
binding.cardDueDate.btnClear.setVisibility(
selectedDueDate == null || selectedDueDate.isEmpty() ? View.GONE : View.VISIBLE);
}
private void loadAttachmentLimits() {
UserAccountsApi userAccountsApi =
BaseApi.getInstance(requireContext(), UserAccountsApi.class);
if (userAccountsApi != null) {
UserAccount userAccount =
userAccountsApi.getAccountById(tinyDB.getInt("currentActiveAccountId", -1));
if (userAccount != null) {
maxAttachmentSize = userAccount.getMaxAttachmentsSize();
maxNumberOfAttachments = userAccount.getMaxNumberOfAttachments();
}
}
}
private void setupAttachments() {
loadAttachmentLimits();
if (maxNumberOfAttachments == 0) {
binding.cardAttachments.getRoot().setVisibility(View.GONE);
return;
}
binding.cardAttachments.getRoot().setVisibility(View.VISIBLE);
attachmentManager = new AttachmentManager(requireContext());
if (maxAttachmentSize > 0) {
attachmentManager.setMaxFileSize((long) maxAttachmentSize * 1024 * 1024);
}
if (maxNumberOfAttachments > 0) {
attachmentManager.setMaxFileCount(maxNumberOfAttachments);
}
attachmentManager.setListener(
new AttachmentManager.AttachmentListener() {
@SuppressLint("SetTextI18n")
@Override
public void onAttachmentsChanged(int count) {
binding.cardAttachments.attachmentCount.setText("(" + count + ")");
updateAttachmentsEmptyState();
}
@Override
public void onAttachmentAdded(Uri uri) {
attachmentsViewModel.addPendingUpload(uri);
}
@Override
public void onAttachmentRemoved(int position) {
attachmentsViewModel.clearPendingUploads();
for (Uri uri : attachmentManager.getPendingUris()) {
attachmentsViewModel.addPendingUpload(uri);
}
}
@Override
public void onAttachmentRejected(String reason) {
Toasty.show(requireContext(), reason);
}
});
ActivityResultLauncher<Intent> filePickerLauncher =
registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK
&& result.getData() != null) {
Uri uri = result.getData().getData();
if (uri != null) {
attachmentManager.handleFilePickerResult(uri);
}
ClipData clipData = result.getData().getClipData();
if (clipData != null) {
for (int i = 0; i < clipData.getItemCount(); i++) {
Uri clipUri = clipData.getItemAt(i).getUri();
if (clipUri != null) {
attachmentManager.handleFilePickerResult(clipUri);
}
}
}
}
});
attachmentManager.registerFilePicker(filePickerLauncher);
CreateAttachmentsAdapter attachmentsAdapter = attachmentManager.createAdapter();
binding.cardAttachments.attachmentsRecyclerView.setLayoutManager(
new LinearLayoutManager(requireContext()));
binding.cardAttachments.attachmentsRecyclerView.setAdapter(attachmentsAdapter);
updateAttachmentsEmptyState();
}
private void updateAttachmentsEmptyState() {
boolean hasAttachments =
attachmentManager != null && attachmentManager.getAttachmentCount() > 0;
binding.cardAttachments.attachmentEmptyState.setVisibility(
hasAttachments ? View.GONE : View.VISIBLE);
binding.cardAttachments.attachmentsRecyclerView.setVisibility(
hasAttachments ? View.VISIBLE : View.GONE);
}
private void observeAttachmentsViewModel() {
attachmentsViewModel
.getIsUploading()
.observe(
getViewLifecycleOwner(),
isUploading -> {
if (!isUploading) {
binding.btnSubmit.setEnabled(true);
}
});
attachmentsViewModel
.getUploadError()
.observe(
getViewLifecycleOwner(),
error -> {
if (error != null && !error.isEmpty()) {
Toasty.show(requireContext(), R.string.attachmentsSaveError);
attachmentsViewModel.clearError();
}
});
attachmentsViewModel
.getUploadComplete()
.observe(
getViewLifecycleOwner(),
complete -> {
if (complete != null && complete) {
String successMsg =
getString(
prToEdit != null
? R.string.updatePrSuccess
: R.string.prCreateSuccess);
Toasty.show(requireContext(), successMsg);
dismiss();
}
});
}
private void handlePrSuccess(long prNumber) {
if (attachmentManager != null && attachmentManager.getAttachmentCount() > 0) {
attachmentsViewModel.uploadAttachments(
requireContext(), repoContext.getOwner(), repoContext.getName(), prNumber);
} else {
String successMsg =
getString(
prToEdit != null ? R.string.updatePrSuccess : R.string.prCreateSuccess);
Toasty.show(requireContext(), successMsg);
dismiss();
}
}
private String formatDateForDisplay(Date date) {
if (date == null) return "";
SimpleDateFormat displayFormat = new SimpleDateFormat("MMM dd, yyyy", Locale.getDefault());
return displayFormat.format(date);
}
private String formatDateForDisplay(String dateString) {
if (dateString == null || dateString.isEmpty()) return "";
try {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
Date date = sdf.parse(dateString);
return formatDateForDisplay(date);
} catch (ParseException e) {
return dateString;
}
}
private Date getDueDateForApi() {
if (selectedDueDate == null || selectedDueDate.isEmpty()) return null;
try {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
return sdf.parse(selectedDueDate);
} catch (ParseException e) {
return null;
}
}
@Override
public void onStart() {
super.onStart();
Dialog dialog = getDialog();
if (dialog instanceof BottomSheetDialog) {
AppUtil.applyFullScreenSheetStyle((BottomSheetDialog) dialog, false);
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
if (attachmentManager != null) {
attachmentManager.clear();
}
}
}

View File

@@ -17,7 +17,6 @@ import java.util.ArrayList;
import java.util.List;
import org.gitnex.tea4j.v2.models.PullRequest;
import org.mian.gitnex.R;
import org.mian.gitnex.activities.CreatePullRequestActivity;
import org.mian.gitnex.activities.RepoDetailActivity;
import org.mian.gitnex.adapters.PullRequestsAdapter;
import org.mian.gitnex.databinding.FragmentPullRequestsBinding;
@@ -130,8 +129,8 @@ public class PullRequestsFragment extends Fragment implements RepoDetailActivity
break;
case "PR_CREATE_NEW":
startActivity(
repository.getIntent(requireContext(), CreatePullRequestActivity.class));
BottomSheetCreatePullRequest.newInstance(repository, null)
.show(getParentFragmentManager(), "CREATE_PULL_REQUEST");
break;
}
}
@@ -281,6 +280,22 @@ public class PullRequestsFragment extends Fragment implements RepoDetailActivity
binding.pullToRefresh.setRefreshing(false);
}
});
viewModel
.getActionResult()
.observe(
getViewLifecycleOwner(),
code -> {
if (code == null || code == -1) return;
if (code == 200 || code == 201) {
refreshData();
} else {
Toasty.show(requireContext(), R.string.genericError);
}
viewModel.resetActionResult();
});
}
private void updateUiState() {

View File

@@ -8,7 +8,10 @@ import androidx.lifecycle.ViewModel;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.gitnex.tea4j.v2.models.CreatePullRequestOption;
import org.gitnex.tea4j.v2.models.EditPullRequestOption;
import org.gitnex.tea4j.v2.models.PullRequest;
import org.mian.gitnex.R;
import org.mian.gitnex.clients.RetrofitClient;
import retrofit2.Call;
import retrofit2.Callback;
@@ -25,6 +28,13 @@ public class PullRequestsViewModel extends ViewModel {
private final MutableLiveData<Boolean> hasLoadedOnce = new MutableLiveData<>(false);
private final MutableLiveData<String> errorMessage = new MutableLiveData<>();
private final MutableLiveData<Integer> repoTotalPrCountLiveData = new MutableLiveData<>(-1);
private final MutableLiveData<Boolean> isCreating = new MutableLiveData<>(false);
private final MutableLiveData<Boolean> isUpdating = new MutableLiveData<>(false);
private final MutableLiveData<PullRequest> createdPr = new MutableLiveData<>();
private final MutableLiveData<PullRequest> updatedPr = new MutableLiveData<>();
private final MutableLiveData<String> createError = new MutableLiveData<>();
private final MutableLiveData<String> updateError = new MutableLiveData<>();
private final MutableLiveData<Integer> actionResult = new MutableLiveData<>(-1);
private boolean isLastPage = false;
private int totalCount = -1;
@@ -45,10 +55,58 @@ public class PullRequestsViewModel extends ViewModel {
return errorMessage;
}
public LiveData<Boolean> getIsCreating() {
return isCreating;
}
public LiveData<Boolean> getIsUpdating() {
return isUpdating;
}
public LiveData<PullRequest> getCreatedPr() {
return createdPr;
}
public LiveData<PullRequest> getUpdatedPr() {
return updatedPr;
}
public LiveData<String> getCreateError() {
return createError;
}
public LiveData<String> getUpdateError() {
return updateError;
}
public LiveData<Integer> getActionResult() {
return actionResult;
}
public LiveData<Integer> getRepoPrTotalCount() {
return repoTotalPrCountLiveData;
}
public void clearCreatedPr() {
createdPr.setValue(null);
}
public void clearUpdatedPr() {
updatedPr.setValue(null);
}
public void clearCreateError() {
createError.setValue(null);
}
public void clearUpdateError() {
updateError.setValue(null);
}
public void resetActionResult() {
actionResult.setValue(-1);
}
public void resetPagination() {
isLastPage = false;
totalCount = -1;
@@ -132,4 +190,86 @@ public class PullRequestsViewModel extends ViewModel {
public void prefetchPrCounts(Context ctx, String owner, String repo) {
fetchPullRequests(ctx, owner, repo, "open", 1, 1, true);
}
public void createPullRequest(
Context ctx, String owner, String repo, CreatePullRequestOption prData) {
isCreating.setValue(true);
createError.setValue(null);
Call<PullRequest> call =
RetrofitClient.getApiInterface(ctx).repoCreatePullRequest(owner, repo, prData);
call.enqueue(
new Callback<>() {
@Override
public void onResponse(
@NonNull Call<PullRequest> call,
@NonNull Response<PullRequest> response) {
isCreating.setValue(false);
if (response.isSuccessful() && response.body() != null) {
createdPr.setValue(response.body());
actionResult.setValue(201);
} else {
handleCreateUpdateError(response.code(), ctx, true);
}
}
@Override
public void onFailure(@NonNull Call<PullRequest> call, @NonNull Throwable t) {
isCreating.setValue(false);
createError.setValue(t.getMessage());
}
});
}
public void updatePullRequest(
Context ctx, String owner, String repo, long prIndex, EditPullRequestOption prData) {
isUpdating.setValue(true);
updateError.setValue(null);
Call<PullRequest> call =
RetrofitClient.getApiInterface(ctx)
.repoEditPullRequest(owner, repo, prIndex, prData);
call.enqueue(
new Callback<>() {
@Override
public void onResponse(
@NonNull Call<PullRequest> call,
@NonNull Response<PullRequest> response) {
isUpdating.setValue(false);
if (response.isSuccessful() && response.body() != null) {
updatedPr.setValue(response.body());
actionResult.setValue(200);
} else {
handleCreateUpdateError(response.code(), ctx, false);
}
}
@Override
public void onFailure(@NonNull Call<PullRequest> call, @NonNull Throwable t) {
isUpdating.setValue(false);
updateError.setValue(t.getMessage());
}
});
}
private void handleCreateUpdateError(int code, Context ctx, boolean isCreate) {
String errorMsg =
switch (code) {
case 401 -> "UNAUTHORIZED";
case 403 -> ctx.getString(R.string.authorizeError);
case 404 -> ctx.getString(R.string.apiNotFound);
case 409 -> ctx.getString(R.string.prAlreadyExists);
default -> ctx.getString(R.string.genericError);
};
if (isCreate) {
createError.setValue(errorMsg);
} else {
updateError.setValue(errorMsg);
}
}
}

View File

@@ -1,251 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/primaryBackgroundColor"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/primaryBackgroundColor">
<com.google.android.material.appbar.CollapsingToolbarLayout
style="?attr/collapsingToolbarLayoutLargeStyle"
android:layout_width="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
android:background="?attr/primaryBackgroundColor"
app:contentScrim="?attr/primaryBackgroundColor"
android:layout_height="?attr/collapsingToolbarLayoutLargeSize">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/topAppBar"
android:layout_width="match_parent"
android:elevation="0dp"
android:layout_height="?attr/actionBarSize"
app:title="@string/pageTitleNewPullRequest"
app:layout_collapseMode="pin"
app:menu="@menu/create_issue_menu"
app:navigationIcon="@drawable/ic_close" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressBar"
style="@style/Widget.Material3.LinearProgressIndicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="gone"
app:indicatorColor="?attr/progressIndicatorColor" />
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/dimen16dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/prTitleLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen8dp"
android:layout_marginBottom="@dimen/dimen8dp"
android:hint="@string/newIssueTitle"
android:textColorHint="?attr/hintColor"
app:boxStrokeErrorColor="@color/darkRed"
app:counterEnabled="true"
app:counterMaxLength="255"
app:counterTextColor="?attr/inputTextColor"
app:endIconMode="clear_text"
app:endIconTint="?attr/iconsColor"
app:hintTextColor="?attr/hintColor">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/prTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionNext"
android:inputType="textCapSentences"
android:singleLine="true"
android:textColor="?attr/inputTextColor"
android:textColorHint="?attr/hintColor"
android:textSize="@dimen/dimen16sp"/>
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/insertNote"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="@dimen/dimen8dp"
android:layout_marginBottom="@dimen/dimen0dp"
android:text="@string/insertNote"
android:textColor="?attr/primaryTextColor"
android:textSize="@dimen/dimen14sp"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/prBodyLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen8dp"
android:layout_marginBottom="@dimen/dimen8dp"
android:hint="@string/description"
android:textColorHint="?attr/hintColor"
app:boxStrokeErrorColor="@color/darkRed"
app:endIconMode="clear_text"
app:endIconTint="?attr/iconsColor"
app:hintTextColor="?attr/hintColor">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/prBody"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen180dp"
android:gravity="top|start"
android:inputType="textCapSentences|textMultiLine"
android:scrollbars="vertical"
android:textColor="?attr/inputTextColor"
android:textColorHint="?attr/hintColor"
android:textSize="@dimen/dimen16sp"/>
</com.google.android.material.textfield.TextInputLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/markdown_preview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/dimen186dp"
android:layout_marginTop="@dimen/dimen8dp"
android:layout_marginBottom="@dimen/dimen8dp"
android:textColor="?attr/primaryTextColor"
android:textIsSelectable="true"
android:textSize="@dimen/dimen14sp"
android:visibility="gone" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/mergeIntoBranchSpinnerLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen8dp"
android:layout_marginBottom="@dimen/dimen8dp"
android:hint="@string/mergeIntoBranch"
android:textColorHint="?attr/hintColor"
app:hintTextColor="?attr/hintColor">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/mergeIntoBranchSpinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:textColor="?attr/inputTextColor"
android:textColorHint="?attr/hintColor"
android:textSize="@dimen/dimen16sp"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/pullFromBranchSpinnerLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen8dp"
android:layout_marginBottom="@dimen/dimen8dp"
android:hint="@string/pullFromBranch"
android:textColorHint="?attr/hintColor"
app:hintTextColor="?attr/hintColor">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/pullFromBranchSpinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:textColor="?attr/inputTextColor"
android:textColorHint="?attr/hintColor"
android:textSize="@dimen/dimen16sp"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/milestonesSpinnerLayout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen8dp"
android:layout_marginBottom="@dimen/dimen8dp"
android:hint="@string/milestone"
android:textColorHint="?attr/hintColor"
app:endIconTint="?attr/iconsColor"
app:hintTextColor="?attr/hintColor">
<AutoCompleteTextView
android:id="@+id/milestonesSpinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:labelFor="@+id/milestonesSpinner"
android:textColor="?attr/inputTextColor"
android:textSize="@dimen/dimen16sp"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/prLabelsLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen8dp"
android:layout_marginBottom="@dimen/dimen8dp"
android:hint="@string/newIssueLabelsTitle"
android:textColorHint="?attr/hintColor"
app:hintTextColor="?attr/hintColor">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/prLabels"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="false"
android:textColor="?attr/inputTextColor"
android:textColorHint="?attr/hintColor"
android:textSize="@dimen/dimen16sp"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/prDueDateLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen8dp"
android:layout_marginBottom="@dimen/dimen8dp"
android:hint="@string/newIssueDueDateTitle"
android:textColorHint="?attr/hintColor"
app:boxStrokeErrorColor="@color/darkRed"
app:endIconMode="clear_text"
app:endIconTint="?attr/iconsColor"
app:hintTextColor="?attr/hintColor">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/prDueDate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="false"
android:maxLines="1"
android:textColor="?attr/inputTextColor"
android:textColorHint="?attr/hintColor"
android:textSize="@dimen/dimen16sp"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -1,183 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/primaryBackgroundColor"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/primaryBackgroundColor">
<com.google.android.material.appbar.CollapsingToolbarLayout
style="?attr/collapsingToolbarLayoutLargeStyle"
android:layout_width="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
android:background="?attr/primaryBackgroundColor"
app:contentScrim="?attr/primaryBackgroundColor"
android:layout_height="?attr/collapsingToolbarLayoutLargeSize">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/topAppBar"
android:layout_width="match_parent"
android:elevation="0dp"
android:layout_height="?attr/actionBarSize"
app:title="@string/editIssueNavHeader"
app:layout_collapseMode="pin"
app:menu="@menu/create_issue_menu"
app:navigationIcon="@drawable/ic_close" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/dimen16dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/editIssueTitleLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen8dp"
android:layout_marginBottom="@dimen/dimen8dp"
android:hint="@string/newIssueTitle"
android:textColorHint="?attr/hintColor"
app:boxStrokeErrorColor="@color/darkRed"
app:endIconMode="clear_text"
app:endIconTint="?attr/iconsColor"
app:hintTextColor="?attr/hintColor">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editIssueTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionNext"
android:inputType="textCapSentences"
android:singleLine="true"
android:textColor="?attr/inputTextColor"
android:textColorHint="?attr/hintColor"
android:textSize="@dimen/dimen16sp"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/editIssueDescriptionLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen8dp"
android:layout_marginBottom="@dimen/dimen8dp"
android:hint="@string/description"
android:textColorHint="?attr/hintColor"
app:boxStrokeErrorColor="@color/darkRed"
app:endIconMode="clear_text"
app:endIconTint="?attr/iconsColor"
app:hintTextColor="?attr/hintColor">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editIssueDescription"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen180dp"
android:gravity="top|start"
android:inputType="textCapSentences|textMultiLine"
android:scrollbars="vertical"
android:textColor="?attr/inputTextColor"
android:textColorHint="?attr/hintColor"
android:textSize="@dimen/dimen16sp"/>
</com.google.android.material.textfield.TextInputLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/markdown_preview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/dimen186dp"
android:layout_marginTop="@dimen/dimen8dp"
android:layout_marginBottom="@dimen/dimen8dp"
android:textColor="?attr/primaryTextColor"
android:textIsSelectable="true"
android:textSize="@dimen/dimen14sp"
android:visibility="gone" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/editIssueMilestoneSpinnerLayout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen8dp"
android:layout_marginBottom="@dimen/dimen8dp"
android:hint="@string/milestone"
android:textColorHint="?attr/hintColor"
app:endIconTint="?attr/iconsColor"
app:hintTextColor="?attr/hintColor">
<AutoCompleteTextView
android:id="@+id/editIssueMilestoneSpinner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:labelFor="@+id/editIssueMilestoneSpinner"
android:textColor="?attr/inputTextColor"
android:textSize="@dimen/dimen16sp"/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/editIssueDueDateLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen8dp"
android:layout_marginBottom="@dimen/dimen8dp"
android:hint="@string/newIssueDueDateTitle"
android:textColorHint="?attr/hintColor"
app:boxStrokeErrorColor="@color/darkRed"
app:endIconMode="clear_text"
app:endIconTint="?attr/iconsColor"
app:hintTextColor="?attr/hintColor">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editIssueDueDate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="false"
android:maxLines="1"
android:textColor="?attr/inputTextColor"
android:textColorHint="?attr/hintColor"
android:textSize="@dimen/dimen16sp"/>
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:id="@+id/attachmentFrame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:orientation="vertical">
<LinearLayout
android:id="@+id/attachmentsView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen12dp"
android:orientation="horizontal">
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -4,12 +4,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/drag_handle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/header_layout"
android:layout_width="match_parent"
@@ -17,7 +11,7 @@
android:paddingHorizontal="@dimen/dimen24dp"
android:paddingTop="@dimen/dimen8dp"
android:clipToPadding="false"
app:layout_constraintTop_toBottomOf="@id/drag_handle">
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/sheet_title"

View File

@@ -16,7 +16,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/dimen24dp"
android:text="@string/newIssueAssigneesListTitle"
android:text="@string/pageTitleUsers"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View File

@@ -5,18 +5,12 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/drag_handle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/header_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/dimen24dp"
app:layout_constraintTop_toBottomOf="@id/drag_handle">
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/sheet_title"

View File

@@ -0,0 +1,216 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/root_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/header_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/dimen24dp"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/sheet_title"
style="@style/TextAppearance.Material3.HeadlineSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/create_pr"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/btn_close"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_close"
style="@style/Widget.Material3Expressive.Button.IconButton.Tonal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/close"
app:icon="@drawable/ic_close"
app:layout_constraintBottom_toBottomOf="@id/sheet_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/sheet_title" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="@dimen/dimen16dp"
android:clipToPadding="false"
android:paddingHorizontal="@dimen/dimen24dp"
android:paddingBottom="@dimen/dimen32dp"
app:layout_constrainedHeight="true"
app:layout_constraintTop_toBottomOf="@id/header_layout"
app:layout_constraintBottom_toBottomOf="parent">
<LinearLayout
android:id="@+id/form_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/prTitleLayout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen12dp"
android:hint="@string/title"
app:counterEnabled="true"
app:counterMaxLength="255"
app:endIconMode="clear_text"
app:endIconTint="?attr/iconsColor"
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.Medium">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/prTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionNext"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/description_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen12dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/prBodyLayout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/description"
app:endIconMode="clear_text"
app:endIconTint="?attr/iconsColor"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.Medium">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/prBody"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen224dp"
android:gravity="top"
android:scrollbars="vertical"
android:paddingTop="@dimen/dimen40dp"
android:inputType="textMultiLine" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_expand"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen6dp"
android:layout_marginEnd="@dimen/dimen8dp"
android:text="@string/fullscreen"
app:icon="@drawable/ic_maximize"
app:iconSize="@dimen/dimen20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<include
android:id="@+id/card_merge_into"
layout="@layout/item_metadata_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen16dp" />
<include
android:id="@+id/card_pull_from"
layout="@layout/item_metadata_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen16dp" />
<include
android:id="@+id/card_milestone"
layout="@layout/item_metadata_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen16dp" />
<include
android:id="@+id/card_labels"
layout="@layout/item_metadata_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen16dp" />
<include
android:id="@+id/card_assignees"
layout="@layout/item_metadata_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen16dp" />
<include
android:id="@+id/card_reviewers"
layout="@layout/item_metadata_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen16dp" />
<include
android:id="@+id/card_due_date"
layout="@layout/item_metadata_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen16dp" />
<include
android:id="@+id/card_attachments"
layout="@layout/item_metadata_attachment_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen16dp" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_allow_maintainer_edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen16dp"
android:checked="true"
android:visibility="gone"
android:text="@string/allow_maintainer_edit" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen32dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_submit"
style="@style/Widget.Material3.Button"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen56dp"
android:text="@string/create_pr"
app:cornerRadius="@dimen/dimen32dp" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/loading_indicator"
android:layout_width="@dimen/dimen24dp"
android:layout_height="@dimen/dimen24dp"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone"
app:indicatorSize="@dimen/dimen24dp" />
</FrameLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -5,18 +5,12 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/drag_handle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/header_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/dimen24dp"
app:layout_constraintTop_toBottomOf="@id/drag_handle">
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/sheet_title"

View File

@@ -4,18 +4,12 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/drag_handle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/header_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/dimen24dp"
app:layout_constraintTop_toBottomOf="@id/drag_handle">
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/sheet_title"
@@ -44,7 +38,7 @@
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="0dp"
android:layout_marginTop="@dimen/dimen16dp"
android:clipToPadding="false"
android:paddingHorizontal="@dimen/dimen24dp"

View File

@@ -5,18 +5,12 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/drag_handle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/header_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/dimen24dp"
app:layout_constraintTop_toBottomOf="@id/drag_handle">
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/sheet_title"

View File

@@ -1,40 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
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="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/dimen12dp">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/pullToRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</FrameLayout>
<com.google.android.material.loadingindicator.LoadingIndicator
android:id="@+id/expressiveLoader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
app:indicatorSize="@dimen/dimen48dp"
app:trackThickness="@dimen/dimen4dp" />
<include
android:id="@+id/layoutEmpty"
layout="@layout/layout_empty_state"
android:visibility="gone" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -36,7 +36,7 @@
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:paddingHorizontal="@dimen/dimen6dp" />
android:paddingHorizontal="@dimen/dimen8dp" />
</LinearLayout>

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/save"
android:orderInCategory="1"
android:title="@string/saveButton"
app:showAsAction="ifRoom"/>
</menu>

View File

@@ -480,6 +480,7 @@
<string name="editRelease">Edit Release</string>
<string name="selectBranchError">Select a branch for release/tag</string>
<string name="add_release_branch">Tap to select a branch</string>
<string name="add_reviewers">Tap to select reviewers</string>
<string name="create_release_tag">Create Release/Tag</string>
<!-- release -->
@@ -661,6 +662,7 @@
<string name="collapse">Collapse</string>
<string name="fullscreen">Fullscreen</string>
<string name="type">Type</string>
<string name="reviewers">Reviewers</string>
<!-- generic copy -->
<string name="exploreUsers">Explore users</string>
@@ -708,6 +710,8 @@
<string name="mergeNotAllowed">Not allowed to merge [Reason: Does not have enough approvals]</string>
<string name="deleteBranch">Delete Branch</string>
<string name="switch_branch">Switch Branch</string>
<string name="allow_maintainer_edit">Allow maintainers to edit this pr</string>
<string name="edit_pr">Edit Pull Request</string>
<string name="waitLoadingDownloadFile">Please wait for the file to load to memory</string>
<string name="downloadFileSaved">File saved successfully</string>
@@ -1170,7 +1174,7 @@
<string name="labelColorHex">Hex Color</string>
<string name="invalid_color">Invalid color code</string>
<string name="invalid_length">Must be 6 characters</string>
<string name="create_pr">Create PR</string>
<string name="create_pr">Create Pull Request</string>
<string name="search_filter">Search &amp; filter</string>
<string name="create_issue">Create Issue</string>
<string name="search_repos">Search Repositories</string>
@@ -1184,7 +1188,7 @@
<string name="no_attachments">No attachments</string>
<string name="attachment_limit_count">Maximum %d files allowed</string>
<string name="attachment_limit_size">File exceeds %s limit</string>
<string name="attachment_limit_total_size">Total size would exceed %s</string>
<string name="cannotAddSelfAsReviewer">You cannot add yourself as a reviewer</string>
<string name="time_in_moments">in moments</string>
<string name="time_in_minute">in %d minute</string>