Implement new UI for creating release/tag + can edit release. New content viewer

This commit is contained in:
M M Arif
2026-04-14 23:21:43 +05:00
parent 751ce89c34
commit ab6906c191
19 changed files with 1607 additions and 794 deletions

View File

@@ -39,9 +39,6 @@
<activity
android:name=".activities.AdminGetUsersActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|keyboard|keyboardHidden|navigation"/>
<activity
android:name=".activities.CreateReleaseActivity"
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"

View File

@@ -1,482 +0,0 @@
package org.mian.gitnex.activities;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.vdurmont.emoji.EmojiParser;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.gitnex.tea4j.v2.models.Branch;
import org.gitnex.tea4j.v2.models.CreateReleaseOption;
import org.gitnex.tea4j.v2.models.CreateTagOption;
import org.gitnex.tea4j.v2.models.Release;
import org.gitnex.tea4j.v2.models.Tag;
import org.mian.gitnex.R;
import org.mian.gitnex.adapters.BranchAdapter;
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.ActivityCreateReleaseBinding;
import org.mian.gitnex.databinding.CustomInsertNoteBinding;
import org.mian.gitnex.helpers.AlertDialogs;
import org.mian.gitnex.helpers.Constants;
import org.mian.gitnex.helpers.Markdown;
import org.mian.gitnex.helpers.Toasty;
import org.mian.gitnex.helpers.contexts.RepositoryContext;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
/**
* @author mmarif
*/
public class CreateReleaseActivity extends BaseActivity {
private ActivityCreateReleaseBinding binding;
List<String> branchesList = new ArrayList<>();
private String selectedBranch;
private RepositoryContext repository;
private boolean renderMd = false;
private MaterialAlertDialogBuilder materialAlertDialogBuilderNotes;
private CustomInsertNoteBinding customInsertNoteBinding;
private NotesAdapter adapter;
private NotesApi notesApi;
public AlertDialog dialogNotes;
@SuppressLint("ClickableViewAccessibility")
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityCreateReleaseBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
repository = RepositoryContext.fromIntent(getIntent());
materialAlertDialogBuilderNotes =
new MaterialAlertDialogBuilder(ctx, R.style.ThemeOverlay_Material3_Dialog_Alert);
binding.releaseContent.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;
});
binding.topAppBar.setNavigationOnClickListener(v -> finish());
binding.topAppBar.setOnMenuItemClickListener(
menuItem -> {
int id = menuItem.getItemId();
if (id == R.id.markdown) {
if (!renderMd) {
Markdown.render(
ctx,
EmojiParser.parseToUnicode(
Objects.requireNonNull(
Objects.requireNonNull(
binding.releaseContent
.getText())
.toString())),
binding.markdownPreview);
binding.markdownPreview.setVisibility(View.VISIBLE);
binding.releaseContentLayout.setVisibility(View.GONE);
renderMd = true;
} else {
binding.markdownPreview.setVisibility(View.GONE);
binding.releaseContentLayout.setVisibility(View.VISIBLE);
renderMd = false;
}
return true;
} else if (id == R.id.create) {
processNewRelease();
return true;
} else if (id == R.id.create_tag) {
createNewTag();
return true;
} else {
return super.onOptionsItemSelected(menuItem);
}
});
binding.insertNote.setOnClickListener(insertNote -> showAllNotes());
binding.releaseBranch.setKeyListener(null);
binding.releaseBranch.setCursorVisible(false);
binding.releaseBranch.setOnFocusChangeListener(
(v, hasFocus) -> {
if (hasFocus) {
getBranches();
binding.releaseBranch.clearFocus();
}
});
}
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", "release");
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);
}
});
}
private void createNewTag() {
String tagName = Objects.requireNonNull(binding.releaseTagName.getText()).toString();
String message =
Objects.requireNonNull(binding.releaseTitle.getText())
+ "\n\n"
+ Objects.requireNonNull(binding.releaseContent.getText());
if (tagName.isEmpty()) {
Toasty.show(ctx, getString(R.string.tagNameErrorEmpty));
return;
}
if (selectedBranch == null) {
Toasty.show(ctx, getString(R.string.selectBranchError));
return;
}
CreateTagOption createReleaseJson = new CreateTagOption();
createReleaseJson.setMessage(message);
createReleaseJson.setTagName(tagName);
createReleaseJson.setTarget(selectedBranch);
Call<Tag> call =
RetrofitClient.getApiInterface(ctx)
.repoCreateTag(
repository.getOwner(), repository.getName(), createReleaseJson);
call.enqueue(
new Callback<>() {
@Override
public void onResponse(
@NonNull Call<Tag> call, @NonNull retrofit2.Response<Tag> response) {
if (response.code() == 201) {
// RepoDetailActivity.updateFABActions = true;
Toasty.show(ctx, getString(R.string.tagCreated));
new Handler().postDelayed(() -> finish(), 3000);
} else if (response.code() == 401) {
AlertDialogs.authorizationTokenRevokedDialog(ctx);
} else if (response.code() == 403) {
Toasty.show(ctx, getString(R.string.authorizeError));
} else if (response.code() == 404) {
Toasty.show(ctx, getString(R.string.apiNotFound));
} else {
Toasty.show(ctx, getString(R.string.genericError));
}
}
@Override
public void onFailure(@NonNull Call<Tag> call, @NonNull Throwable t) {}
});
}
private void processNewRelease() {
String newReleaseTagName =
Objects.requireNonNull(binding.releaseTagName.getText()).toString();
String newReleaseTitle = Objects.requireNonNull(binding.releaseTitle.getText()).toString();
String newReleaseContent =
Objects.requireNonNull(binding.releaseContent.getText()).toString();
String checkBranch = selectedBranch;
boolean newReleaseType = binding.releaseType.isChecked();
boolean newReleaseDraft = binding.releaseDraft.isChecked();
if (newReleaseTitle.isEmpty()) {
Toasty.show(ctx, getString(R.string.titleErrorEmpty));
return;
}
if (newReleaseTagName.isEmpty()) {
Toasty.show(ctx, getString(R.string.tagNameErrorEmpty));
return;
}
if (checkBranch == null) {
Toasty.show(ctx, getString(R.string.selectBranchError));
return;
}
createNewReleaseFunc(
repository.getOwner(),
repository.getName(),
newReleaseTagName,
newReleaseTitle,
newReleaseContent,
selectedBranch,
newReleaseType,
newReleaseDraft);
}
private void createNewReleaseFunc(
String repoOwner,
String repoName,
String newReleaseTagName,
String newReleaseTitle,
String newReleaseContent,
String selectedBranch,
boolean newReleaseType,
boolean newReleaseDraft) {
CreateReleaseOption createReleaseJson = new CreateReleaseOption();
createReleaseJson.setName(newReleaseTitle);
createReleaseJson.setTagName(newReleaseTagName);
createReleaseJson.setBody(newReleaseContent);
createReleaseJson.setDraft(newReleaseDraft);
createReleaseJson.setPrerelease(newReleaseType);
createReleaseJson.setTargetCommitish(selectedBranch);
Call<Release> call =
RetrofitClient.getApiInterface(ctx)
.repoCreateRelease(repoOwner, repoName, createReleaseJson);
call.enqueue(
new Callback<>() {
@Override
public void onResponse(
@NonNull Call<Release> call,
@NonNull retrofit2.Response<Release> response) {
if (response.code() == 201) {
// RepoDetailActivity.updateFABActions = true;
Toasty.show(ctx, getString(R.string.releaseCreatedText));
new Handler().postDelayed(() -> finish(), 3000);
} else if (response.code() == 401) {
AlertDialogs.authorizationTokenRevokedDialog(ctx);
} else if (response.code() == 403) {
Toasty.show(ctx, getString(R.string.authorizeError));
} else if (response.code() == 404) {
Toasty.show(ctx, getString(R.string.apiNotFound));
} else {
Toasty.show(ctx, getString(R.string.genericError));
}
}
@Override
public void onFailure(@NonNull Call<Release> call, @NonNull Throwable t) {}
});
}
private void getBranches() {
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 -> {
binding.releaseBranch.setText(branchName);
selectedBranch = 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();
}
@Override
public void onResume() {
super.onResume();
repository.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.BottomSheetCreateRelease;
import org.mian.gitnex.fragments.BottomsheetRepoMenu;
import org.mian.gitnex.fragments.CollaboratorsFragment;
import org.mian.gitnex.fragments.FilesFragment;
@@ -625,7 +626,8 @@ public class RepoDetailActivity extends BaseActivity
case "newRelease":
switchTab("releases", R.id.btn_nav_releases);
startActivity(repository.getIntent(this, CreateReleaseActivity.class));
BottomSheetCreateRelease.newInstance(repository, null)
.show(getSupportFragmentManager(), "CREATE_RELEASE");
break;
case "wiki":

View File

@@ -22,7 +22,6 @@ 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.activities.CreateReleaseActivity;
import org.mian.gitnex.database.api.BaseApi;
import org.mian.gitnex.database.api.NotesApi;
import org.mian.gitnex.database.models.Notes;
@@ -107,10 +106,7 @@ public class NotesAdapter extends RecyclerView.Adapter<NotesAdapter.NotesViewHol
EditText targetField = null;
AlertDialog dialogToDismiss = null;
if (activity instanceof CreateReleaseActivity releaseAct) {
targetField = releaseAct.findViewById(R.id.releaseContent);
dialogToDismiss = releaseAct.dialogNotes;
} else if (activity instanceof CreatePullRequestActivity prAct) {
if (activity instanceof CreatePullRequestActivity prAct) {
targetField = prAct.findViewById(R.id.prBody);
dialogToDismiss = prAct.dialogNotes;
}

View File

@@ -19,7 +19,6 @@ import java.util.Locale;
import org.gitnex.tea4j.v2.models.Release;
import org.mian.gitnex.R;
import org.mian.gitnex.databinding.ListReleasesBinding;
import org.mian.gitnex.fragments.ReleasesFragment;
import org.mian.gitnex.helpers.AvatarGenerator;
import org.mian.gitnex.helpers.Markdown;
import org.mian.gitnex.helpers.TimeHelper;
@@ -33,13 +32,19 @@ public class ReleasesAdapter extends RecyclerView.Adapter<ReleasesAdapter.Releas
private final Context context;
private List<Release> releasesList;
private final boolean canDelete;
private final ReleasesFragment.OnReleaseItemClickListener listener;
private final OnReleaseItemClickListener listener;
public interface OnReleaseItemClickListener {
void onMenuClick(Release release, int position);
void onDownload(String url);
}
public ReleasesAdapter(
Context context,
List<Release> releases,
boolean canDelete,
ReleasesFragment.OnReleaseItemClickListener listener) {
OnReleaseItemClickListener listener) {
this.context = context;
this.releasesList = releases;
this.canDelete = canDelete;
@@ -71,14 +76,6 @@ public class ReleasesAdapter extends RecyclerView.Adapter<ReleasesAdapter.Releas
notifyDataSetChanged();
}
public void removeItem(int position) {
if (position >= 0 && position < releasesList.size()) {
releasesList.remove(position);
notifyItemRemoved(position);
notifyItemRangeChanged(position, releasesList.size());
}
}
public class ReleasesViewHolder extends RecyclerView.ViewHolder {
private final ListReleasesBinding binding;
@@ -137,7 +134,7 @@ public class ReleasesAdapter extends RecyclerView.Adapter<ReleasesAdapter.Releas
if (release.getAssets() != null && !release.getAssets().isEmpty()) {
binding.downloadList.setVisibility(View.VISIBLE);
ReleasesDownloadsAdapter downloadsAdapter =
new ReleasesDownloadsAdapter(release.getAssets(), listener);
new ReleasesDownloadsAdapter(release.getAssets(), listener::onDownload);
binding.downloadList.setLayoutManager(new LinearLayoutManager(context));
binding.downloadList.setAdapter(downloadsAdapter);
@@ -148,7 +145,7 @@ public class ReleasesAdapter extends RecyclerView.Adapter<ReleasesAdapter.Releas
binding.itemMenu.setVisibility(canDelete ? View.VISIBLE : View.GONE);
binding.itemMenu.setOnClickListener(
v -> listener.onDelete(release, getBindingAdapterPosition()));
v -> listener.onMenuClick(release, getBindingAdapterPosition()));
binding.btnAssets.setOnClickListener(
v -> {

View File

@@ -0,0 +1,253 @@
package org.mian.gitnex.fragments;
import android.app.Dialog;
import android.os.Bundle;
import android.text.Spannable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import org.mian.gitnex.R;
import org.mian.gitnex.databinding.BottomsheetContentViewerBinding;
import org.mian.gitnex.helpers.AppUtil;
import org.mian.gitnex.helpers.Markdown;
import org.mian.gitnex.helpers.contexts.RepositoryContext;
/**
* @author mmarif
*/
public class BottomSheetContentViewer extends BottomSheetDialogFragment {
public enum Feature {
MARKDOWN_PREVIEW, // Enable markdown preview toggle
START_IN_MARKDOWN, // Start showing Markdown instead of raw content
ALLOW_COPY, // Show copy button (always visible by default)
ALLOW_SHARE, // Show share button (always visible by default)
SYNTAX_HIGHLIGHT, // Use renderWithHighlights for code
SHOW_TITLE, // Show title in header
}
/*
Spannable highlighted = SyntaxHighlighter.highlight(code, "java");
BottomSheetContentViewer.newInstance(
highlighted,
"Main.java",
repoContext,
BottomSheetContentViewer.Feature.SYNTAX_HIGHLIGHT,
ContentViewerBottomSheet.Feature.MARKDOWN_PREVIEW,
ContentViewerBottomSheet.Feature.SHOW_TITLE
).show(fm, "viewer");
*/
private static final String ARG_CONTENT = "content";
private static final String ARG_TITLE = "title";
private static final String ARG_REPO_CONTEXT = "repo_context";
private static final String ARG_FEATURES = "features";
private static final String ARG_IS_SPANNABLE = "is_spannable";
private BottomsheetContentViewerBinding binding;
private final Set<Feature> enabledFeatures = new HashSet<>();
private RepositoryContext repoContext;
private String rawContent;
private String title;
private Spannable spannableContent;
private boolean isSpannable = false;
private boolean isMarkdownMode = false;
public static BottomSheetContentViewer newInstance(
String content,
@Nullable String title,
@Nullable RepositoryContext repoContext,
Feature... features) {
BottomSheetContentViewer fragment = new BottomSheetContentViewer();
Bundle args = new Bundle();
args.putString(ARG_CONTENT, content);
args.putBoolean(ARG_IS_SPANNABLE, false);
if (title != null) args.putString(ARG_TITLE, title);
if (repoContext != null) args.putSerializable(ARG_REPO_CONTEXT, repoContext);
args.putStringArrayList(ARG_FEATURES, featureNamesToList(features));
fragment.setArguments(args);
return fragment;
}
public static BottomSheetContentViewer newInstance(
Spannable spannable,
@Nullable String title,
@Nullable RepositoryContext repoContext,
Feature... features) {
BottomSheetContentViewer fragment = new BottomSheetContentViewer();
Bundle args = new Bundle();
args.putCharSequence(ARG_CONTENT, spannable);
args.putBoolean(ARG_IS_SPANNABLE, true);
if (title != null) args.putString(ARG_TITLE, title);
if (repoContext != null) args.putSerializable(ARG_REPO_CONTEXT, repoContext);
args.putStringArrayList(ARG_FEATURES, featureNamesToList(features));
fragment.setArguments(args);
return fragment;
}
private static ArrayList<String> featureNamesToList(Feature... features) {
ArrayList<String> names = new ArrayList<>();
for (Feature f : features) {
names.add(f.name());
}
return names;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
isSpannable = getArguments().getBoolean(ARG_IS_SPANNABLE, false);
if (isSpannable) {
spannableContent = (Spannable) getArguments().getCharSequence(ARG_CONTENT);
} else {
rawContent = getArguments().getString(ARG_CONTENT, "");
}
title = getArguments().getString(ARG_TITLE);
repoContext = (RepositoryContext) getArguments().getSerializable(ARG_REPO_CONTEXT);
ArrayList<String> featureNames = getArguments().getStringArrayList(ARG_FEATURES);
if (featureNames != null) {
for (String name : featureNames) {
try {
enabledFeatures.add(Feature.valueOf(name));
} catch (IllegalArgumentException ignored) {
}
}
}
}
isMarkdownMode = enabledFeatures.contains(Feature.START_IN_MARKDOWN);
}
@Nullable @Override
public View onCreateView(
@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
binding = BottomsheetContentViewerBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setupUI();
renderContent();
}
private void setupUI() {
binding.btnClose.setOnClickListener(v -> dismiss());
if (enabledFeatures.contains(Feature.ALLOW_COPY)) {
binding.btnCopy.setVisibility(View.VISIBLE);
binding.btnCopy.setOnClickListener(v -> copyContent());
}
if (enabledFeatures.contains(Feature.ALLOW_SHARE)) {
binding.btnShare.setVisibility(View.VISIBLE);
binding.btnShare.setOnClickListener(v -> shareContent());
}
if (enabledFeatures.contains(Feature.MARKDOWN_PREVIEW)) {
binding.btnMarkdown.setVisibility(View.VISIBLE);
binding.btnMarkdown.setOnClickListener(v -> toggleMarkdownMode());
updateMarkdownIcon();
}
if (enabledFeatures.contains(Feature.SHOW_TITLE) && title != null) {
binding.viewerTitle.setText(title);
binding.viewerTitle.setVisibility(View.VISIBLE);
}
}
private void renderContent() {
if (isMarkdownMode) {
renderMarkdown();
} else {
renderRaw();
}
}
private void renderRaw() {
binding.rawContentScroll.setVisibility(View.VISIBLE);
binding.markdownPreviewScroll.setVisibility(View.GONE);
binding.markdownPreview.setVisibility(View.GONE);
binding.markdownPreviewText.setVisibility(View.GONE);
String content = getContentAsString();
binding.rawContentText.setText(content);
}
private void renderMarkdown() {
binding.rawContentScroll.setVisibility(View.GONE);
binding.markdownPreviewScroll.setVisibility(View.VISIBLE);
String content = getContentAsString();
if (content == null) content = "";
if (enabledFeatures.contains(Feature.SYNTAX_HIGHLIGHT) && spannableContent != null) {
binding.markdownPreview.setVisibility(View.VISIBLE);
binding.markdownPreviewText.setVisibility(View.GONE);
Markdown.renderWithHighlights(
requireContext(), spannableContent, binding.markdownPreview, repoContext);
} else if (repoContext != null) {
binding.markdownPreview.setVisibility(View.VISIBLE);
binding.markdownPreviewText.setVisibility(View.GONE);
Markdown.render(requireContext(), content, binding.markdownPreview, repoContext);
} else {
binding.markdownPreview.setVisibility(View.GONE);
binding.markdownPreviewText.setVisibility(View.VISIBLE);
Markdown.render(requireContext(), content, binding.markdownPreviewText);
}
}
private String getContentAsString() {
return isSpannable && spannableContent != null ? spannableContent.toString() : rawContent;
}
private void toggleMarkdownMode() {
isMarkdownMode = !isMarkdownMode;
updateMarkdownIcon();
renderContent();
}
private void updateMarkdownIcon() {
binding.btnMarkdown.setIconResource(
isMarkdownMode ? R.drawable.ic_edit : R.drawable.ic_markdown);
}
private void copyContent() {
String content = getContentAsString();
AppUtil.copyToClipboard(requireContext(), content, getString(R.string.copied_to_clipboard));
}
private void shareContent() {
String content = getContentAsString();
AppUtil.sharingIntent(requireContext(), content);
}
@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;
}
}

View File

@@ -0,0 +1,460 @@
package org.mian.gitnex.fragments;
import android.app.Dialog;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import java.util.Objects;
import org.gitnex.tea4j.v2.models.CreateReleaseOption;
import org.gitnex.tea4j.v2.models.CreateTagOption;
import org.gitnex.tea4j.v2.models.Release;
import org.mian.gitnex.R;
import org.mian.gitnex.databinding.BottomsheetCreateReleaseBinding;
import org.mian.gitnex.helpers.AlertDialogs;
import org.mian.gitnex.helpers.AppUtil;
import org.mian.gitnex.helpers.Toasty;
import org.mian.gitnex.helpers.contexts.RepositoryContext;
import org.mian.gitnex.viewmodels.ReleasesViewModel;
/**
* @author mmarif
*/
public class BottomSheetCreateRelease extends BottomSheetDialogFragment {
private BottomsheetCreateReleaseBinding binding;
private ReleasesViewModel viewModel;
private RepositoryContext repoContext;
private Release releaseToEdit;
private String selectedBranch = null;
private boolean isReleaseMode = true;
public static BottomSheetCreateRelease newInstance(
RepositoryContext repository, @Nullable Release release) {
BottomSheetCreateRelease fragment = new BottomSheetCreateRelease();
Bundle args = new Bundle();
args.putSerializable("repo_context", repository);
if (release != null) {
args.putSerializable("release_item", release);
}
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
repoContext = (RepositoryContext) getArguments().getSerializable("repo_context");
releaseToEdit = (Release) getArguments().getSerializable("release_item");
}
}
@Nullable @Override
public View onCreateView(
@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
binding = BottomsheetCreateReleaseBinding.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(ReleasesViewModel.class);
viewModel.clearCreatedRelease();
viewModel.clearCreatedTag();
viewModel.clearUpdatedRelease();
setupUI();
setupListeners();
observeViewModel();
ViewGroup.LayoutParams editTextParams = binding.releaseContent.getLayoutParams();
editTextParams.height = (int) (224 * getResources().getDisplayMetrics().density);
binding.releaseContent.setLayoutParams(editTextParams);
}
private void setupUI() {
boolean hasWriteAccess =
repoContext.getPermissions() != null && repoContext.getPermissions().isPush();
binding.cardBranch.cardIcon.setImageResource(R.drawable.ic_branch);
binding.cardBranch.tvCardLabel.setText(R.string.pageTitleChooseBranch);
binding.cardBranch.getRoot().setVisibility(hasWriteAccess ? View.VISIBLE : View.GONE);
if (releaseToEdit != null) {
binding.sheetTitle.setText(R.string.editRelease);
binding.createTypeToggle.setVisibility(View.GONE);
binding.releaseTagNameLayout.setVisibility(View.GONE);
binding.switchDraft.setVisibility(View.GONE);
binding.descriptionContainer.setVisibility(View.VISIBLE);
binding.switchPrerelease.setVisibility(View.VISIBLE);
binding.releaseTitle.setText(releaseToEdit.getName());
binding.releaseContent.setText(releaseToEdit.getBody());
binding.switchPrerelease.setChecked(releaseToEdit.isPrerelease());
binding.cardBranch.getRoot().setVisibility(View.GONE);
binding.btnSubmit.setText(R.string.update);
} else {
binding.sheetTitle.setText(R.string.createRelease);
binding.descriptionContainer.setVisibility(View.VISIBLE);
binding.switchPrerelease.setVisibility(View.VISIBLE);
binding.switchDraft.setVisibility(View.VISIBLE);
if (hasWriteAccess) {
updateBranchDisplay();
}
}
updateBranchClearButtonVisibility();
}
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();
if (hasWriteAccess) {
binding.cardBranch.getRoot().setOnClickListener(v -> openBranchPicker());
binding.cardBranch.btnClear.setOnClickListener(
v -> {
selectedBranch = null;
updateBranchDisplay();
updateBranchClearButtonVisibility();
});
}
binding.createTypeToggle.addOnButtonCheckedListener(
(group, checkedId, isChecked) -> {
if (isChecked) {
isReleaseMode = checkedId == R.id.btn_create_release;
updateUIForMode();
}
});
}
private void updateUIForMode() {
if (releaseToEdit == null) {
if (isReleaseMode) {
binding.releaseTitleLayout.setHint(R.string.releaseTitleText);
binding.descriptionContainer.setVisibility(View.VISIBLE);
binding.switchPrerelease.setVisibility(View.VISIBLE);
binding.switchDraft.setVisibility(View.VISIBLE);
binding.btnSubmit.setText(R.string.createRelease);
} else {
binding.releaseTitleLayout.setHint(R.string.description);
binding.descriptionContainer.setVisibility(View.GONE);
binding.switchPrerelease.setVisibility(View.GONE);
binding.switchDraft.setVisibility(View.GONE);
binding.btnSubmit.setText(R.string.createTag);
}
}
}
private void openBranchPicker() {
BottomsheetBranchPicker branchPicker =
BottomsheetBranchPicker.newInstance(
repoContext.getOwner(),
repoContext.getName(),
selectedBranch != null ? selectedBranch : repoContext.getBranchRef());
branchPicker.setOnBranchSelectedListener(
branchName -> {
selectedBranch = branchName;
updateBranchDisplay();
updateBranchClearButtonVisibility();
});
branchPicker.show(getParentFragmentManager(), "BRANCH_PICKER");
}
private void updateBranchDisplay() {
if (selectedBranch == null || selectedBranch.isEmpty()) {
binding.cardBranch.tvSelectedText.setText(R.string.add_release_branch);
} else {
binding.cardBranch.tvSelectedText.setText(selectedBranch);
}
}
private void updateBranchClearButtonVisibility() {
binding.cardBranch.btnClear.setVisibility(
selectedBranch == null || selectedBranch.isEmpty() ? View.GONE : View.VISIBLE);
}
private void openFullScreenEditor() {
BottomSheetFullScreenEditor editorBottomSheet =
BottomSheetFullScreenEditor.newInstance(
Objects.requireNonNull(binding.releaseContent.getText()).toString(),
repoContext,
true,
true);
editorBottomSheet.setEditorListener(
newContent -> {
binding.releaseContent.setText(newContent);
binding.releaseContent.setSelection(
newContent != null ? newContent.length() : 0);
});
editorBottomSheet.show(getParentFragmentManager(), "FULLSCREEN_EDITOR");
}
private void submitAction() {
if (releaseToEdit != null) {
submitUpdateRelease();
} else if (isReleaseMode) {
submitCreateRelease();
} else {
submitCreateTag();
}
}
private void submitCreateRelease() {
String tagName =
binding.releaseTagName.getText() != null
? binding.releaseTagName.getText().toString().trim()
: "";
String title =
binding.releaseTitle.getText() != null
? binding.releaseTitle.getText().toString().trim()
: "";
String content =
binding.releaseContent.getText() != null
? binding.releaseContent.getText().toString().trim()
: "";
if (tagName.isEmpty()) {
Toasty.show(requireContext(), R.string.tagNameErrorEmpty);
return;
}
if (title.isEmpty()) {
Toasty.show(requireContext(), R.string.titleErrorEmpty);
return;
}
if (selectedBranch == null || selectedBranch.isEmpty()) {
Toasty.show(requireContext(), R.string.selectBranchError);
return;
}
CreateReleaseOption releaseData = new CreateReleaseOption();
releaseData.setTagName(tagName);
releaseData.setName(title);
releaseData.setName(title);
releaseData.setBody(content);
releaseData.setTargetCommitish(selectedBranch);
releaseData.setDraft(binding.switchDraft.isChecked());
releaseData.setPrerelease(binding.switchPrerelease.isChecked());
viewModel.createRelease(
requireContext(), repoContext.getOwner(), repoContext.getName(), releaseData);
}
private void submitCreateTag() {
String tagName =
binding.releaseTagName.getText() != null
? binding.releaseTagName.getText().toString().trim()
: "";
String message =
binding.releaseTitle.getText() != null
? binding.releaseTitle.getText().toString().trim()
: "";
if (tagName.isEmpty()) {
Toasty.show(requireContext(), R.string.tagNameErrorEmpty);
return;
}
if (selectedBranch == null || selectedBranch.isEmpty()) {
Toasty.show(requireContext(), R.string.selectBranchError);
return;
}
CreateTagOption tagData = new CreateTagOption();
tagData.setTagName(tagName);
tagData.setMessage(message);
tagData.setTarget(selectedBranch);
viewModel.createTag(
requireContext(), repoContext.getOwner(), repoContext.getName(), tagData);
}
private void submitUpdateRelease() {
String title =
binding.releaseTitle.getText() != null
? binding.releaseTitle.getText().toString().trim()
: "";
String content =
binding.releaseContent.getText() != null
? binding.releaseContent.getText().toString().trim()
: "";
if (title.isEmpty()) {
Toasty.show(requireContext(), R.string.titleErrorEmpty);
return;
}
viewModel.updateRelease(
requireContext(),
repoContext.getOwner(),
repoContext.getName(),
releaseToEdit.getId(),
title,
content,
binding.switchPrerelease.isChecked());
}
private void observeViewModel() {
viewModel
.getIsCreatingRelease()
.observe(
getViewLifecycleOwner(),
isCreating -> {
binding.loadingIndicator.setVisibility(
isCreating ? View.VISIBLE : View.GONE);
binding.btnSubmit.setEnabled(!isCreating);
binding.btnSubmit.setText(
isCreating
? ""
: getString(
releaseToEdit != null
? R.string.update
: (isReleaseMode
? R.string.createRelease
: R.string.createTag)));
});
viewModel
.getCreatedRelease()
.observe(
getViewLifecycleOwner(),
release -> {
if (release != null) {
Toasty.show(requireContext(), R.string.releaseCreatedText);
dismiss();
}
});
viewModel
.getCreateReleaseError()
.observe(
getViewLifecycleOwner(),
error -> {
if (error != null && !error.isEmpty()) {
handleError(error);
viewModel.clearCreateReleaseError();
}
});
viewModel
.getIsCreatingTag()
.observe(
getViewLifecycleOwner(),
isCreating -> {
if (!isReleaseMode) {
binding.loadingIndicator.setVisibility(
isCreating ? View.VISIBLE : View.GONE);
binding.btnSubmit.setEnabled(!isCreating);
binding.btnSubmit.setText(
isCreating ? "" : getString(R.string.createTag));
}
});
viewModel
.getCreatedTag()
.observe(
getViewLifecycleOwner(),
tag -> {
if (tag != null) {
Toasty.show(requireContext(), R.string.tagCreated);
dismiss();
}
});
viewModel
.getCreateTagError()
.observe(
getViewLifecycleOwner(),
error -> {
if (error != null && !error.isEmpty()) {
handleError(error);
viewModel.clearCreateTagError();
}
});
viewModel
.getIsUpdatingRelease()
.observe(
getViewLifecycleOwner(),
isUpdating -> {
if (releaseToEdit != null) {
binding.loadingIndicator.setVisibility(
isUpdating ? View.VISIBLE : View.GONE);
binding.btnSubmit.setEnabled(!isUpdating);
binding.btnSubmit.setText(
isUpdating ? "" : getString(R.string.update));
}
});
viewModel
.getUpdatedRelease()
.observe(
getViewLifecycleOwner(),
release -> {
if (release != null) {
Toasty.show(requireContext(), R.string.editReleaseSuccessMessage);
dismiss();
}
});
viewModel
.getUpdateReleaseError()
.observe(
getViewLifecycleOwner(),
error -> {
if (error != null && !error.isEmpty()) {
handleError(error);
viewModel.clearUpdateReleaseError();
}
});
}
private void handleError(String error) {
if (error.equals("UNAUTHORIZED")) {
AlertDialogs.authorizationTokenRevokedDialog(requireContext());
} else if (error.equals(getString(R.string.tagNameConflictError))) {
Toasty.show(requireContext(), R.string.tagNameConflictError);
} else {
Toasty.show(requireContext(), error);
}
}
@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;
}
}

View File

@@ -122,8 +122,6 @@ public class BottomSheetFullScreenEditor extends BottomSheetDialogFragment {
isMarkdownMode = !isMarkdownMode;
binding.fullscreenBtnMarkdown.setIconResource(
isMarkdownMode ? R.drawable.ic_edit : R.drawable.ic_markdown);
binding.fullscreenBtnMarkdown.setText(
isMarkdownMode ? R.string.menuEditText : R.string.strMarkdown);
binding.fullscreenBtnMarkdown.setIconSize(52);
if (isMarkdownMode) {

View File

@@ -39,7 +39,6 @@ import org.gitnex.tea4j.v2.models.Release;
import org.gitnex.tea4j.v2.models.Tag;
import org.mian.gitnex.R;
import org.mian.gitnex.activities.BaseActivity;
import org.mian.gitnex.activities.CreateReleaseActivity;
import org.mian.gitnex.activities.RepoDetailActivity;
import org.mian.gitnex.adapters.ReleasesAdapter;
import org.mian.gitnex.adapters.TagsAdapter;
@@ -108,7 +107,7 @@ public class ReleasesFragment extends Fragment implements RepoDetailActivity.Rep
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
binding = FragmentReleasesBinding.inflate(inflater, container, false);
viewModel = new ViewModelProvider(this).get(ReleasesViewModel.class);
viewModel = new ViewModelProvider(requireActivity()).get(ReleasesViewModel.class);
resultLimit = Constants.getCurrentResultLimit(requireContext());
@@ -138,7 +137,7 @@ public class ReleasesFragment extends Fragment implements RepoDetailActivity.Rep
items.add(
new RepositoryMenuItemModel(
"RELEASE_CREATE_NEW",
R.string.createRelease,
R.string.create_release_tag,
R.drawable.ic_add,
R.attr.colorPrimaryContainer,
R.attr.colorOnPrimaryContainer));
@@ -156,7 +155,8 @@ public class ReleasesFragment extends Fragment implements RepoDetailActivity.Rep
break;
case "RELEASE_CREATE_NEW":
startActivity(repository.getIntent(requireContext(), CreateReleaseActivity.class));
BottomSheetCreateRelease.newInstance(repository, null)
.show(getChildFragmentManager(), "CREATE_RELEASE");
break;
}
}
@@ -295,14 +295,17 @@ public class ReleasesFragment extends Fragment implements RepoDetailActivity.Rep
.observe(
getViewLifecycleOwner(),
code -> {
if (code == -1) return;
if (code == null || code == -1) return;
if (code == 204) {
int messageRes =
repository.isReleasesViewTypeIsTag()
? R.string.tagDeleted
: R.string.releaseDeleted;
Toasty.show(requireContext(), messageRes);
refreshData();
} else if (code == 200 || code == 201) {
refreshData();
} else {
Toasty.show(requireContext(), R.string.genericError);
}
@@ -334,10 +337,9 @@ public class ReleasesFragment extends Fragment implements RepoDetailActivity.Rep
requireContext(),
list,
canDelete,
new OnReleaseItemClickListener() {
new ReleasesAdapter.OnReleaseItemClickListener() {
@Override
public void onDelete(Object item, int position) {
Release release = (Release) item;
public void onMenuClick(Release release, int position) {
showReleaseOptionsBottomSheet(release, position);
}
@@ -382,6 +384,13 @@ public class ReleasesFragment extends Fragment implements RepoDetailActivity.Rep
.show();
});
menuBinding.editRelease.setOnClickListener(
v -> {
dialog.dismiss();
BottomSheetCreateRelease.newInstance(repository, release)
.show(getParentFragmentManager(), "EDIT_RELEASE");
});
dialog.show();
}

View File

@@ -1,5 +1,8 @@
package org.mian.gitnex.fragments;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -22,6 +25,7 @@ import org.mian.gitnex.activities.WikiActivity;
import org.mian.gitnex.adapters.WikiListAdapter;
import org.mian.gitnex.databinding.BottomsheetWikiItemMenuBinding;
import org.mian.gitnex.databinding.FragmentWikiBinding;
import org.mian.gitnex.helpers.AlertDialogs;
import org.mian.gitnex.helpers.AppUtil;
import org.mian.gitnex.helpers.Constants;
import org.mian.gitnex.helpers.EndlessRecyclerViewScrollListener;
@@ -43,6 +47,7 @@ public class WikiFragment extends Fragment implements RepoDetailActivity.RepoHub
private RepositoryContext repository;
private int resultLimit;
private boolean isFirstLoad = true;
private String pendingPageName = null;
public static WikiFragment newInstance(RepositoryContext repository) {
WikiFragment fragment = new WikiFragment();
@@ -197,14 +202,73 @@ public class WikiFragment extends Fragment implements RepoDetailActivity.RepoHub
err -> {
if (err != null) Toasty.show(requireContext(), err);
});
viewModel
.getIsLoadingPage()
.observe(
getViewLifecycleOwner(),
isLoading -> {
binding.expressiveLoader.setVisibility(VISIBLE);
});
viewModel
.getPageContent()
.observe(
getViewLifecycleOwner(),
content -> {
if (content != null && pendingPageName != null) {
showContentViewer(pendingPageName, content);
pendingPageName = null;
viewModel.clearPageContent();
binding.expressiveLoader.setVisibility(GONE);
}
});
viewModel
.getPageError()
.observe(
getViewLifecycleOwner(),
error -> {
if (error != null && !error.isEmpty()) {
if (error.equals("UNAUTHORIZED")) {
AlertDialogs.authorizationTokenRevokedDialog(requireContext());
} else {
Toasty.show(requireContext(), error);
}
pendingPageName = null;
viewModel.clearPageError();
}
});
}
private void openWiki(WikiPageMetaData wikiPage, String action) {
Intent intent = new Intent(requireContext(), WikiActivity.class);
intent.putExtra("pageName", wikiPage.getTitle());
if (action != null) intent.putExtra("action", action);
intent.putExtra(RepositoryContext.INTENT_EXTRA, repository);
startActivity(intent);
if (action != null && action.equals("edit")) {
Intent intent = new Intent(requireContext(), WikiActivity.class);
intent.putExtra("pageName", wikiPage.getSubUrl());
intent.putExtra("action", action);
intent.putExtra(RepositoryContext.INTENT_EXTRA, repository);
startActivity(intent);
} else {
pendingPageName = wikiPage.getTitle();
viewModel.fetchWikiPageContent(
requireContext(),
repository.getOwner(),
repository.getName(),
wikiPage.getSubUrl());
}
}
private void showContentViewer(String title, String content) {
BottomSheetContentViewer.newInstance(
content,
title,
repository,
BottomSheetContentViewer.Feature.ALLOW_COPY,
BottomSheetContentViewer.Feature.ALLOW_SHARE,
BottomSheetContentViewer.Feature.MARKDOWN_PREVIEW,
BottomSheetContentViewer.Feature.START_IN_MARKDOWN,
BottomSheetContentViewer.Feature.SHOW_TITLE)
.show(getParentFragmentManager(), "WIKI_VIEWER");
}
private void showDeleteDialog(WikiPageMetaData wikiPage) {
@@ -219,7 +283,7 @@ public class WikiFragment extends Fragment implements RepoDetailActivity.RepoHub
requireContext(),
repository.getOwner(),
repository.getName(),
wikiPage.getTitle());
wikiPage.getSubUrl());
})
.setNegativeButton(R.string.cancelButton, null)
.show();
@@ -253,14 +317,12 @@ public class WikiFragment extends Fragment implements RepoDetailActivity.RepoHub
boolean hasData = adapter != null && adapter.getItemCount() > 0;
boolean hasLoadedOnce = Boolean.TRUE.equals(viewModel.getHasLoadedOnce().getValue());
binding.expressiveLoader.setVisibility(isLoading && !hasData ? View.VISIBLE : View.GONE);
binding.expressiveLoader.setVisibility(isLoading && !hasData ? VISIBLE : GONE);
if (isLoading) {
binding.layoutEmpty.getRoot().setVisibility(View.GONE);
binding.layoutEmpty.getRoot().setVisibility(GONE);
} else {
binding.layoutEmpty
.getRoot()
.setVisibility(!hasData && hasLoadedOnce ? View.VISIBLE : View.GONE);
binding.layoutEmpty.getRoot().setVisibility(!hasData && hasLoadedOnce ? VISIBLE : GONE);
}
}

View File

@@ -8,8 +8,12 @@ import androidx.lifecycle.ViewModel;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.gitnex.tea4j.v2.models.CreateReleaseOption;
import org.gitnex.tea4j.v2.models.CreateTagOption;
import org.gitnex.tea4j.v2.models.EditReleaseOption;
import org.gitnex.tea4j.v2.models.Release;
import org.gitnex.tea4j.v2.models.Tag;
import org.mian.gitnex.R;
import org.mian.gitnex.clients.RetrofitClient;
import retrofit2.Call;
import retrofit2.Callback;
@@ -28,6 +32,15 @@ public class ReleasesViewModel extends ViewModel {
private final MutableLiveData<Boolean> isTagsLoading = new MutableLiveData<>(false);
private final MutableLiveData<Integer> actionResult = new MutableLiveData<>(-1);
private final MutableLiveData<Integer> repoReleasesCountLiveData = new MutableLiveData<>(-1);
private final MutableLiveData<Boolean> isCreatingRelease = new MutableLiveData<>(false);
private final MutableLiveData<Boolean> isCreatingTag = new MutableLiveData<>(false);
private final MutableLiveData<Boolean> isUpdatingRelease = new MutableLiveData<>(false);
private final MutableLiveData<Release> createdRelease = new MutableLiveData<>();
private final MutableLiveData<Tag> createdTag = new MutableLiveData<>();
private final MutableLiveData<Release> updatedRelease = new MutableLiveData<>();
private final MutableLiveData<String> createReleaseError = new MutableLiveData<>();
private final MutableLiveData<String> createTagError = new MutableLiveData<>();
private final MutableLiveData<String> updateReleaseError = new MutableLiveData<>();
private int totalCount = -1;
private boolean isLastPage = false;
@@ -62,6 +75,42 @@ public class ReleasesViewModel extends ViewModel {
return repoReleasesCountLiveData;
}
public LiveData<Boolean> getIsCreatingRelease() {
return isCreatingRelease;
}
public LiveData<Boolean> getIsCreatingTag() {
return isCreatingTag;
}
public LiveData<Boolean> getIsUpdatingRelease() {
return isUpdatingRelease;
}
public LiveData<Release> getCreatedRelease() {
return createdRelease;
}
public LiveData<Tag> getCreatedTag() {
return createdTag;
}
public LiveData<Release> getUpdatedRelease() {
return updatedRelease;
}
public LiveData<String> getCreateReleaseError() {
return createReleaseError;
}
public LiveData<String> getCreateTagError() {
return createTagError;
}
public LiveData<String> getUpdateReleaseError() {
return updateReleaseError;
}
public void resetPagination() {
this.isLastPage = false;
this.totalCount = -1;
@@ -75,6 +124,30 @@ public class ReleasesViewModel extends ViewModel {
this.tags.setValue(null);
}
public void clearCreatedRelease() {
createdRelease.setValue(null);
}
public void clearCreatedTag() {
createdTag.setValue(null);
}
public void clearUpdatedRelease() {
updatedRelease.setValue(null);
}
public void clearCreateReleaseError() {
createReleaseError.setValue(null);
}
public void clearCreateTagError() {
createTagError.setValue(null);
}
public void clearUpdateReleaseError() {
updateReleaseError.setValue(null);
}
public void resetActionResult() {
actionResult.setValue(-1);
}
@@ -266,4 +339,124 @@ public class ReleasesViewModel extends ViewModel {
}
});
}
public void createRelease(
Context ctx, String owner, String repo, CreateReleaseOption releaseData) {
isCreatingRelease.setValue(true);
Call<Release> call =
RetrofitClient.getApiInterface(ctx).repoCreateRelease(owner, repo, releaseData);
call.enqueue(
new Callback<>() {
@Override
public void onResponse(
@NonNull Call<Release> call, @NonNull Response<Release> response) {
isCreatingRelease.setValue(false);
if (response.isSuccessful() && response.body() != null) {
createdRelease.setValue(response.body());
actionResult.setValue(201);
} else if (response.code() == 401) {
createReleaseError.setValue("UNAUTHORIZED");
} else if (response.code() == 403) {
createReleaseError.setValue(ctx.getString(R.string.authorizeError));
} else if (response.code() == 404) {
createReleaseError.setValue(ctx.getString(R.string.apiNotFound));
} else if (response.code() == 409) {
createReleaseError.setValue(
ctx.getString(R.string.tagNameConflictError));
} else {
createReleaseError.setValue(ctx.getString(R.string.genericError));
}
}
@Override
public void onFailure(@NonNull Call<Release> call, @NonNull Throwable t) {
isCreatingRelease.setValue(false);
createReleaseError.setValue(t.getMessage());
}
});
}
public void createTag(Context ctx, String owner, String repo, CreateTagOption tagData) {
isCreatingTag.setValue(true);
Call<Tag> call = RetrofitClient.getApiInterface(ctx).repoCreateTag(owner, repo, tagData);
call.enqueue(
new Callback<>() {
@Override
public void onResponse(
@NonNull Call<Tag> call, @NonNull Response<Tag> response) {
isCreatingTag.setValue(false);
if (response.isSuccessful() && response.body() != null) {
createdTag.setValue(response.body());
actionResult.setValue(201);
} else if (response.code() == 401) {
createTagError.setValue("UNAUTHORIZED");
} else if (response.code() == 403) {
createTagError.setValue(ctx.getString(R.string.authorizeError));
} else if (response.code() == 404) {
createTagError.setValue(ctx.getString(R.string.apiNotFound));
} else if (response.code() == 409) {
createTagError.setValue(ctx.getString(R.string.tagNameConflictError));
} else {
createTagError.setValue(ctx.getString(R.string.genericError));
}
}
@Override
public void onFailure(@NonNull Call<Tag> call, @NonNull Throwable t) {
isCreatingTag.setValue(false);
createTagError.setValue(t.getMessage());
}
});
}
public void updateRelease(
Context ctx,
String owner,
String repo,
long releaseId,
String name,
String body,
boolean prerelease) {
isUpdatingRelease.setValue(true);
EditReleaseOption editData = new EditReleaseOption();
editData.setName(name);
editData.setBody(body);
editData.setPrerelease(prerelease);
Call<Release> call =
RetrofitClient.getApiInterface(ctx)
.repoEditRelease(owner, repo, releaseId, editData);
call.enqueue(
new Callback<Release>() {
@Override
public void onResponse(
@NonNull Call<Release> call, @NonNull Response<Release> response) {
isUpdatingRelease.setValue(false);
if (response.isSuccessful() && response.body() != null) {
updatedRelease.setValue(response.body());
actionResult.setValue(200);
} else if (response.code() == 401) {
updateReleaseError.setValue("UNAUTHORIZED");
} else if (response.code() == 403) {
updateReleaseError.setValue(ctx.getString(R.string.authorizeError));
} else if (response.code() == 404) {
updateReleaseError.setValue(ctx.getString(R.string.apiNotFound));
} else {
updateReleaseError.setValue(ctx.getString(R.string.genericError));
}
}
@Override
public void onFailure(@NonNull Call<Release> call, @NonNull Throwable t) {
isUpdatingRelease.setValue(false);
updateReleaseError.setValue(t.getMessage());
}
});
}
}

View File

@@ -7,8 +7,11 @@ import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import java.util.ArrayList;
import java.util.List;
import org.gitnex.tea4j.v2.models.WikiPage;
import org.gitnex.tea4j.v2.models.WikiPageMetaData;
import org.mian.gitnex.R;
import org.mian.gitnex.clients.RetrofitClient;
import org.mian.gitnex.helpers.AppUtil;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
@@ -23,6 +26,9 @@ public class WikiViewModel extends ViewModel {
private final MutableLiveData<Boolean> hasLoadedOnce = new MutableLiveData<>(false);
private final MutableLiveData<String> error = new MutableLiveData<>();
private final MutableLiveData<Integer> actionResult = new MutableLiveData<>(-1);
private final MutableLiveData<Boolean> isLoadingPage = new MutableLiveData<>(false);
private final MutableLiveData<String> pageContent = new MutableLiveData<>();
private final MutableLiveData<String> pageError = new MutableLiveData<>();
private final List<WikiPageMetaData> fullList = new ArrayList<>();
@@ -49,6 +55,18 @@ public class WikiViewModel extends ViewModel {
return actionResult;
}
public LiveData<Boolean> getIsLoadingPage() {
return isLoadingPage;
}
public LiveData<String> getPageContent() {
return pageContent;
}
public LiveData<String> getPageError() {
return pageError;
}
public void resetPagination() {
fullList.clear();
isLastPage = false;
@@ -57,6 +75,14 @@ public class WikiViewModel extends ViewModel {
hasLoadedOnce.setValue(false);
}
public void clearPageContent() {
pageContent.setValue(null);
}
public void clearPageError() {
pageError.setValue(null);
}
public void fetchWikiPages(
Context ctx, String owner, String repo, int page, int limit, boolean isRefresh) {
if (Boolean.TRUE.equals(isLoading.getValue()) && !isRefresh) return;
@@ -120,6 +146,50 @@ public class WikiViewModel extends ViewModel {
});
}
public void fetchWikiPageContent(Context ctx, String owner, String repo, String pageName) {
isLoadingPage.setValue(true);
pageError.setValue(null);
Call<WikiPage> call =
RetrofitClient.getApiInterface(ctx).repoGetWikiPage(owner, repo, pageName);
call.enqueue(
new Callback<>() {
@Override
public void onResponse(
@NonNull Call<WikiPage> call, @NonNull Response<WikiPage> response) {
isLoadingPage.setValue(false);
if (response.isSuccessful() && response.body() != null) {
WikiPage wikiPage = response.body();
String decodedContent =
AppUtil.decodeBase64(wikiPage.getContentBase64());
pageContent.setValue(decodedContent);
} else {
switch (response.code()) {
case 401:
pageError.setValue("UNAUTHORIZED");
break;
case 403:
pageError.setValue(ctx.getString(R.string.authorizeError));
break;
case 404:
pageError.setValue(ctx.getString(R.string.apiNotFound));
break;
default:
pageError.setValue(ctx.getString(R.string.genericError));
}
}
}
@Override
public void onFailure(@NonNull Call<WikiPage> call, @NonNull Throwable t) {
isLoadingPage.setValue(false);
pageError.setValue(t.getMessage());
}
});
}
public void deleteWikiPage(Context ctx, String owner, String repo, String pageName) {
isLoading.setValue(true);
RetrofitClient.getApiInterface(ctx)

View File

@@ -1,196 +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/createRelease"
app:layout_collapseMode="pin"
app:menu="@menu/create_release_tag_menu"
app:popupTheme="@style/Widget.Material3.PopupMenu"
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/releaseTitleLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen8dp"
android:layout_marginBottom="@dimen/dimen8dp"
android:hint="@string/releaseTitleText"
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/releaseTitle"
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/releaseTagNameLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen8dp"
android:layout_marginBottom="@dimen/dimen8dp"
android:hint="@string/releaseTagNameText"
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/releaseTagName"
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/releaseContentLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen8dp"
android:layout_marginBottom="@dimen/dimen8dp"
android:hint="@string/releaseContentText"
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/releaseContent"
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>
<TextView
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/releaseBranchLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen8dp"
android:layout_marginBottom="@dimen/dimen8dp"
android:hint="@string/pageTitleChooseBranch"
android:textColorHint="?attr/hintColor"
app:hintTextColor="?attr/hintColor">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/releaseBranch"
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>
<CheckBox
android:id="@+id/releaseType"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen10dp"
android:checked="false"
android:text="@string/releaseTypeText"
android:textColor="?attr/primaryTextColor"
android:textSize="@dimen/dimen16sp"/>
<CheckBox
android:id="@+id/releaseDraft"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen10dp"
android:checked="false"
android:text="@string/releaseDraftText"
android:textColor="?attr/primaryTextColor"
android:textSize="@dimen/dimen16sp"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,151 @@
<?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:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Header Card -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/header_card"
style="@style/Widget.Material3.CardView.Filled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen8dp"
android:layout_marginHorizontal="@dimen/dimen12dp"
app:cardBackgroundColor="?attr/colorSurfaceContainerHigh"
app:cardElevation="@dimen/dimen0dp"
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.ExtraLargeIncreased"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="@dimen/dimen2dp"
android:paddingHorizontal="@dimen/dimen8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_close"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/close"
app:icon="@drawable/ic_close"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_copy"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/menuCopyText"
android:visibility="gone"
app:icon="@drawable/ic_copy"
app:layout_constraintStart_toEndOf="@id/btn_close"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_share"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/share"
android:visibility="gone"
app:icon="@drawable/ic_share"
app:layout_constraintStart_toEndOf="@id/btn_copy"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_markdown"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/strMarkdown"
android:visibility="gone"
app:icon="@drawable/ic_markdown"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView
android:id="@+id/viewer_title"
style="@style/TextAppearance.Material3.TitleSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/dimen8dp"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:textColor="?attr/colorOnSurfaceVariant"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/btn_markdown"
app:layout_constraintStart_toEndOf="@id/btn_share"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="@dimen/dimen12dp"
app:layout_constraintTop_toBottomOf="@id/header_card"
app:layout_constraintBottom_toBottomOf="parent">
<androidx.core.widget.NestedScrollView
android:id="@+id/raw_content_scroll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingHorizontal="@dimen/dimen16dp">
<TextView
android:id="@+id/raw_content_text"
style="@style/TextAppearance.Material3.BodyLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/dimen16dp"
android:textIsSelectable="true"
android:textColor="?attr/colorOnSurface" />
</androidx.core.widget.NestedScrollView>
<androidx.core.widget.NestedScrollView
android:id="@+id/markdown_preview_scroll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingHorizontal="@dimen/dimen16dp"
android:visibility="gone">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/markdown_preview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/dimen16dp"
android:nestedScrollingEnabled="false"
android:visibility="gone" />
<TextView
android:id="@+id/markdown_preview_text"
style="@style/TextAppearance.Material3.BodyLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/dimen16dp"
android:textIsSelectable="true"
android:textColor="?attr/colorOnSurface"
android:visibility="gone" />
</FrameLayout>
</androidx.core.widget.NestedScrollView>
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,252 @@
<?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">
<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">
<TextView
android:id="@+id/sheet_title"
style="@style/TextAppearance.Material3.HeadlineSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/createRelease"
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="wrap_content"
android:orientation="vertical">
<!-- Type Label + Segmented Buttons -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen12dp"
android:orientation="vertical">
<TextView
style="@style/TextAppearance.Material3.LabelLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/dimen4dp"
android:layout_marginBottom="@dimen/dimen4dp"
android:text="@string/type" />
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/create_type_toggle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:checkedButton="@id/btn_create_release"
app:selectionRequired="true"
app:singleSelection="true">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_create_release"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/release"
app:cornerRadius="0dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_create_tag"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/create_tag"
app:cornerRadius="0dp" />
</com.google.android.material.button.MaterialButtonToggleGroup>
</LinearLayout>
<!-- Tag Name -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/releaseTagNameLayout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen12dp"
android:hint="@string/releaseTagNameText"
app:counterEnabled="true"
app:counterMaxLength="24"
app:endIconMode="clear_text"
app:endIconTint="?attr/iconsColor"
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.Medium">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/releaseTagName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionNext"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/releaseTitleLayout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen12dp"
android:hint="@string/releaseTitleText"
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/releaseTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionNext"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Description Container with Expand Button -->
<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/releaseContentLayout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/releaseContentText"
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/releaseContent"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen224dp"
android:gravity="top"
android:scrollbars="vertical"
android:paddingTop="@dimen/dimen48dp"
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/dimen8dp"
android:layout_marginEnd="@dimen/dimen8dp"
android:text="@string/fullscreen"
app:icon="@drawable/ic_maximize"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- Branch Selector Card -->
<include
android:id="@+id/card_branch"
layout="@layout/item_metadata_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen16dp" />
<!-- Pre-release Switch -->
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_prerelease"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen16dp"
android:text="@string/releaseTypeText"
android:visibility="gone" />
<!-- Draft Switch -->
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/switch_draft"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen8dp"
android:text="@string/releaseDraftText"
android:visibility="gone" />
<!-- Submit Button -->
<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/createRelease"
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

@@ -2,62 +2,84 @@
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:paddingHorizontal="@dimen/dimen12dp">
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/fullscreen_toolbar"
<!-- Header Card -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/header_card"
style="@style/Widget.Material3.CardView.Filled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end"
android:layout_marginTop="@dimen/dimen8dp"
android:layout_marginHorizontal="@dimen/dimen12dp"
app:cardBackgroundColor="?attr/colorSurfaceContainerHigh"
app:cardElevation="@dimen/dimen0dp"
app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.ExtraLargeIncreased"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.button.MaterialButton
android:id="@+id/fullscreen_btn_clear"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/clear"
app:iconSize="@dimen/dimen20dp"
app:icon="@drawable/ic_delete" />
android:paddingVertical="@dimen/dimen2dp"
android:paddingHorizontal="@dimen/dimen8dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/fullscreen_btn_notes"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/navNotes"
android:visibility="gone"
app:iconSize="@dimen/dimen20dp"
app:icon="@drawable/ic_file" />
<com.google.android.material.button.MaterialButton
android:id="@+id/fullscreen_btn_collapse"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/collapse"
app:icon="@drawable/ic_minimize"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/fullscreen_btn_markdown"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:text="@string/strMarkdown"
app:iconSize="@dimen/dimen20dp"
app:icon="@drawable/ic_markdown" />
<com.google.android.material.button.MaterialButton
android:id="@+id/fullscreen_btn_clear"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/clear"
app:icon="@drawable/ic_delete"
app:layout_constraintStart_toEndOf="@id/fullscreen_btn_collapse"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/fullscreen_btn_collapse"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/collapse"
app:iconSize="@dimen/dimen20dp"
app:icon="@drawable/ic_minimize" />
<com.google.android.material.button.MaterialButton
android:id="@+id/fullscreen_btn_notes"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/navNotes"
android:visibility="gone"
app:icon="@drawable/ic_file"
app:layout_constraintStart_toEndOf="@id/fullscreen_btn_clear"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/fullscreen_btn_markdown"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/strMarkdown"
android:visibility="gone"
app:icon="@drawable/ic_markdown"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Content Area -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="@dimen/dimen8dp"
app:layout_constraintTop_toBottomOf="@id/fullscreen_toolbar"
android:layout_marginTop="@dimen/dimen12dp"
android:paddingHorizontal="@dimen/dimen12dp"
app:layout_constraintTop_toBottomOf="@id/header_card"
app:layout_constraintBottom_toBottomOf="parent">
<EditText
@@ -70,7 +92,7 @@
android:hint="@string/description"
android:background="@null"
android:padding="@dimen/dimen8dp"
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" />
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
<androidx.core.widget.NestedScrollView
android:id="@+id/fullscreen_markdown_scroll"

View File

@@ -47,6 +47,54 @@
android:weightSum="3"
tools:ignore="UselessParent">
<com.google.android.material.card.MaterialCardView
android:id="@+id/edit_card"
style="?attr/materialCardViewFilledStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_margin="@dimen/dimen4dp"
app:cardCornerRadius="@dimen/dimen24dp"
app:cardBackgroundColor="?attr/colorPrimarySurface"
app:strokeWidth="0dp">
<LinearLayout
android:id="@+id/edit_release"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:orientation="vertical"
android:paddingVertical="@dimen/dimen16dp">
<ImageView
android:id="@+id/edit_icon"
android:layout_width="@dimen/dimen24dp"
android:layout_height="@dimen/dimen24dp"
app:srcCompat="@drawable/ic_edit"
app:tint="?attr/colorOnPrimarySurface"
android:contentDescription="@string/menuEditText" />
<TextView
android:id="@+id/edit_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen8dp"
android:fontFamily="sans-serif-medium"
android:gravity="center"
android:text="@string/menuEditText"
android:paddingHorizontal="@dimen/dimen6dp"
android:maxLines="1"
android:ellipsize="end"
android:textColor="?attr/colorOnPrimarySurface"
android:textAppearance="@style/TextAppearance.Material3.BodySmall" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/delete_release_card"
style="?attr/materialCardViewFilledStyle"

View File

@@ -1,25 +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/markdown"
android:icon="@drawable/ic_markdown"
android:orderInCategory="0"
android:title="@string/strMarkdown"
app:showAsAction="ifRoom" />
<item
android:id="@+id/create"
android:orderInCategory="1"
android:title="@string/newCreateButtonCopy"
android:contentDescription="@string/newCreateButtonCopy" />
<item
android:id="@+id/create_tag"
android:orderInCategory="2"
android:title="@string/create_tag"
android:contentDescription="@string/create_tag" />
</menu>

View File

@@ -321,8 +321,6 @@
<string name="labelMenuContentDesc">Desc</string>
<string name="labelDeleteText">Label deleted</string>
<string name="selectBranchError">Select a branch for release</string>
<string name="alertDialogTokenRevokedTitle">Authorization Error</string>
<string name="alertDialogTokenRevokedMessage">It seems that the Access Token is revoked OR you are not allowed to see these contents.\n\nIn case of revoked Token, please update the account.</string>
<string name="labelDeleteMessage">Do you really want to delete this label?</string>
@@ -465,7 +463,6 @@
<!-- edit issue -->
<!-- release -->
<string name="createRelease">New Release</string>
<string name="releaseTagNameText">Tag Name</string>
<string name="releaseTitleText">Title</string>
<string name="releaseContentText">Content</string>
@@ -476,6 +473,14 @@
<string name="releaseCreatedText">New release created</string>
<string name="deleteReleaseConfirmation">Do you really want to delete this release?</string>
<string name="releaseDeleted">Release deleted</string>
<string name="tagNameConflictError">Tag name already exists</string>
<string name="editReleaseSuccessMessage">Release updated successfully</string>
<string name="createRelease">Create Release</string>
<string name="createTag">Create Tag</string>
<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="create_release_tag">Create Release/Tag</string>
<!-- release -->
<string name="openWebRepo">Open in Browser</string>
@@ -655,6 +660,7 @@
<string name="select">Select</string>
<string name="collapse">Collapse</string>
<string name="fullscreen">Fullscreen</string>
<string name="type">Type</string>
<!-- generic copy -->
<string name="exploreUsers">Explore users</string>
@@ -908,7 +914,7 @@
<string name="userAvatar">Avatar</string>
<string name="tags">Tags</string>
<string name="releasesTags">Releases/Tags</string>
<string name="create_tag">Create Tag Only</string>
<string name="create_tag">Tag Only</string>
<string name="tagCreated">Tag created</string>
<string name="asRef">Use as reference</string>
<string name="deleteTagConfirmation">Do you really want to delete this tag?</string>