diff --git a/app/build.gradle b/app/build.gradle index 06c5293a..29ad4bc5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,8 +6,8 @@ android { applicationId "org.mian.gitnex" minSdkVersion 21 targetSdkVersion 28 - versionCode 60 - versionName "2.1.0" + versionCode 63 + versionName "2.1.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { @@ -24,7 +24,7 @@ android { dependencies { def lifecycle_version = "2.2.0-alpha05" - final def markwon_version = "4.1.1" + final def markwon_version = "4.1.1" implementation fileTree(include: ['*.jar'], dir: 'libs') implementation 'androidx.appcompat:appcompat:1.1.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 036e310e..d90c021d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,8 +11,11 @@ android:supportsRtl="true" android:theme="@style/AppTheme"> + @@ -64,8 +67,8 @@ - - + + \ No newline at end of file diff --git a/app/src/main/java/org/mian/gitnex/activities/FileViewActivity.java b/app/src/main/java/org/mian/gitnex/activities/FileViewActivity.java new file mode 100644 index 00000000..5c4065d1 --- /dev/null +++ b/app/src/main/java/org/mian/gitnex/activities/FileViewActivity.java @@ -0,0 +1,147 @@ +package org.mian.gitnex.activities; + +import android.content.Context; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import org.mian.gitnex.R; +import org.mian.gitnex.clients.RetrofitClient; +import org.mian.gitnex.helpers.AlertDialogs; +import org.mian.gitnex.helpers.Toasty; +import org.mian.gitnex.models.Files; +import org.mian.gitnex.util.AppUtil; +import org.mian.gitnex.util.TinyDB; +import retrofit2.Call; +import retrofit2.Callback; + +/** + * Author M M Arif + */ + +public class FileViewActivity extends AppCompatActivity { + + private View.OnClickListener onClickListener; + private TextView singleFileContents; + final Context ctx = this; + private ProgressBar mProgressBar; + + @Override + protected void onCreate(Bundle savedInstanceState) { + + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_file_view); + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + final TinyDB tinyDb = new TinyDB(getApplicationContext()); + String repoFullName = tinyDb.getString("repoFullName"); + String[] parts = repoFullName.split("/"); + final String repoOwner = parts[0]; + final String repoName = parts[1]; + final String instanceUrl = tinyDb.getString("instanceUrl"); + final String loginUid = tinyDb.getString("loginUid"); + final String instanceToken = "token " + tinyDb.getString(loginUid + "-token"); + + boolean connToInternet = AppUtil.haveNetworkConnection(getApplicationContext()); + + ImageView closeActivity = findViewById(R.id.close); + singleFileContents = findViewById(R.id.singleFileContents); + singleFileContents.setVisibility(View.GONE); + mProgressBar = findViewById(R.id.progress_bar); + + String singleFileName = getIntent().getStringExtra("singleFileName"); + + TextView toolbar_title = findViewById(R.id.toolbar_title); + toolbar_title.setText(singleFileName); + + initCloseListener(); + closeActivity.setOnClickListener(onClickListener); + + if(connToInternet) { + getSingleFileContents(instanceUrl, instanceToken, repoOwner, repoName, singleFileName); + } + else { + Toasty.info(getApplicationContext(), getString(R.string.checkNetConnection)); + } + + } + + private void getSingleFileContents(String instanceUrl, String token, final String owner, String repo, final String filename) { + + Call call = RetrofitClient + .getInstance(instanceUrl) + .getApiInterface() + .getSingleFileContents(token, owner, repo, filename); + + call.enqueue(new Callback() { + + @Override + public void onResponse(@NonNull Call call, @NonNull retrofit2.Response response) { + + if (response.code() == 200) { + + AppUtil appUtil = new AppUtil(); + assert response.body() != null; + + if(!response.body().getContent().equals("")) { + singleFileContents.setVisibility(View.VISIBLE); + mProgressBar.setVisibility(View.GONE); + singleFileContents.setText(appUtil.decodeBase64(response.body().getContent())); + } + else { + singleFileContents.setText(""); + mProgressBar.setVisibility(View.GONE); + } + + } + else if(response.code() == 401) { + + AlertDialogs.authorizationTokenRevokedDialog(ctx, getResources().getString(R.string.alertDialogTokenRevokedTitle), + getResources().getString(R.string.alertDialogTokenRevokedMessage), + getResources().getString(R.string.alertDialogTokenRevokedCopyNegativeButton), + getResources().getString(R.string.alertDialogTokenRevokedCopyPositiveButton)); + + } + else if(response.code() == 403) { + + Toasty.info(ctx, ctx.getString(R.string.authorizeError)); + + } + else if(response.code() == 404) { + + Toasty.info(ctx, ctx.getString(R.string.apiNotFound)); + + } + else { + + Toasty.info(getApplicationContext(), getString(R.string.labelGeneralError)); + + } + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + Log.e("onFailure", t.toString()); + } + }); + + } + + private void initCloseListener() { + onClickListener = new View.OnClickListener() { + @Override + public void onClick(View view) { + getIntent().removeExtra("singleFileName"); + finish(); + } + }; + } + +} diff --git a/app/src/main/java/org/mian/gitnex/activities/MainActivity.java b/app/src/main/java/org/mian/gitnex/activities/MainActivity.java index 6860135e..0d0b877a 100644 --- a/app/src/main/java/org/mian/gitnex/activities/MainActivity.java +++ b/app/src/main/java/org/mian/gitnex/activities/MainActivity.java @@ -354,9 +354,9 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On userAvatar = hView.findViewById(R.id.userAvatar); if (!Objects.requireNonNull(userDetails).getAvatar().equals("")) { - Picasso.get().load(userDetails.getAvatar()).transform(new RoundedTransformation(8, 0)).resize(120, 120).centerCrop().into(userAvatar); + Picasso.get().load(userDetails.getAvatar()).transform(new RoundedTransformation(8, 0)).resize(160, 160).centerCrop().into(userAvatar); } else { - userAvatar.setImageResource(R.mipmap.ic_launcher_round); + userAvatar.setImageResource(R.mipmap.app_logo_round); } userFullName = hView.findViewById(R.id.userFullname); diff --git a/app/src/main/java/org/mian/gitnex/activities/RepoDetailActivity.java b/app/src/main/java/org/mian/gitnex/activities/RepoDetailActivity.java index b1dded86..887b0659 100644 --- a/app/src/main/java/org/mian/gitnex/activities/RepoDetailActivity.java +++ b/app/src/main/java/org/mian/gitnex/activities/RepoDetailActivity.java @@ -26,6 +26,7 @@ import org.mian.gitnex.clients.RetrofitClient; import org.mian.gitnex.fragments.BranchesFragment; import org.mian.gitnex.fragments.ClosedIssuesFragment; import org.mian.gitnex.fragments.CollaboratorsFragment; +import org.mian.gitnex.fragments.FilesFragment; import org.mian.gitnex.fragments.IssuesFragment; import org.mian.gitnex.fragments.LabelsFragment; import org.mian.gitnex.fragments.MilestonesFragment; @@ -87,9 +88,9 @@ public class RepoDetailActivity extends AppCompatActivity implements RepoBottomS if(!tinyDb.getString("issuesCounter").isEmpty()) { getRepoInfo(instanceUrl, Authorization.returnAuthentication(getApplicationContext(), loginUid, instanceToken), repoOwner, repoName1); } - Objects.requireNonNull(tabLayout.getTabAt(1)).setCustomView(tabHeader); + Objects.requireNonNull(tabLayout.getTabAt(2)).setCustomView(tabHeader); - TabLayout.Tab tabOpenIssues = tabLayout.getTabAt(1); + TabLayout.Tab tabOpenIssues = tabLayout.getTabAt(2); ColorStateList textColor = tabLayout.getTabTextColors(); assert tabOpenIssues != null; TextView openIssueTabView = Objects.requireNonNull(tabOpenIssues.getCustomView()).findViewById(R.id.counterBadgeText); @@ -199,21 +200,23 @@ public class RepoDetailActivity extends AppCompatActivity implements RepoBottomS switch (position) { case 0: // information return RepoInfoFragment.newInstance(repoOwner, repoName); - case 1: // issues + case 1: // files + return FilesFragment.newInstance(repoOwner, repoName); + case 2: // issues fragment = new IssuesFragment(); break; - case 2: // closed issues + case 3: // closed issues fragment = new ClosedIssuesFragment(); break; - case 3: // milestones + case 4: // milestones return MilestonesFragment.newInstance(repoOwner, repoName); - case 4: // labels + case 5: // labels return LabelsFragment.newInstance(repoOwner, repoName); - case 5: // branches + case 6: // branches return BranchesFragment.newInstance(repoOwner, repoName); - case 6: // releases + case 7: // releases return ReleasesFragment.newInstance(repoOwner, repoName); - case 7: // collaborators + case 8: // collaborators return CollaboratorsFragment.newInstance(repoOwner, repoName); } return fragment; @@ -221,7 +224,7 @@ public class RepoDetailActivity extends AppCompatActivity implements RepoBottomS @Override public int getCount() { - return 8; + return 9; } } diff --git a/app/src/main/java/org/mian/gitnex/adapters/FilesAdapter.java b/app/src/main/java/org/mian/gitnex/adapters/FilesAdapter.java new file mode 100644 index 00000000..23560292 --- /dev/null +++ b/app/src/main/java/org/mian/gitnex/adapters/FilesAdapter.java @@ -0,0 +1,211 @@ +package org.mian.gitnex.adapters; + +import android.content.Context; +import android.content.Intent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Filter; +import android.widget.Filterable; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import org.mian.gitnex.R; +import org.mian.gitnex.activities.FileViewActivity; +import org.mian.gitnex.helpers.Toasty; +import org.mian.gitnex.models.Files; +import org.mian.gitnex.util.TinyDB; +import java.util.ArrayList; +import java.util.List; + +/** + * Author M M Arif + */ + +public class FilesAdapter extends RecyclerView.Adapter implements Filterable { + + private List filesList; + private Context mCtx; + private List filesListFull; + + class FilesViewHolder extends RecyclerView.ViewHolder { + + private ImageView fileTypeImage; + private TextView fileName; + private TextView fileType; + + private FilesViewHolder(View itemView) { + + super(itemView); + fileName = itemView.findViewById(R.id.fileName); + fileTypeImage = itemView.findViewById(R.id.fileImage); + fileType = itemView.findViewById(R.id.fileType); + + //ImageView filesDropdownMenu = itemView.findViewById(R.id.filesDropdownMenu); + + fileName.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + + Context context = v.getContext(); + TinyDB tinyDb = new TinyDB(context); + + if(fileType.getText().toString().equals("file")) { + Intent intent = new Intent(context, FileViewActivity.class); + intent.putExtra("singleFileName", fileName.getText().toString()); + context.startActivity(intent); + } + else if(fileType.getText().toString().equals("dir")) { + //tinyDb.putString("filesDir", fileName.getText().toString()); + Toasty.info(context, context.getString(R.string.filesDirNotSupportedYet)); + } + else { + Toasty.info(context, context.getString(R.string.filesGenericError)); + } + + } + }); + + + /*filesDropdownMenu.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + + final Context context = v.getContext(); + Context context_ = new ContextThemeWrapper(context, R.style.popupMenuStyle); + + PopupMenu popupMenu = new PopupMenu(context_, v); + popupMenu.inflate(R.menu.files_dotted_list_menu); + + Object menuHelper; + Class[] argTypes; + try { + + Field fMenuHelper = PopupMenu.class.getDeclaredField("mPopup"); + fMenuHelper.setAccessible(true); + menuHelper = fMenuHelper.get(popupMenu); + argTypes = new Class[] { boolean.class }; + menuHelper.getClass().getDeclaredMethod("setForceShowIcon", + argTypes).invoke(menuHelper, true); + + } catch (Exception e) { + + popupMenu.show(); + return; + + } + + popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.deleteFile: + + Intent intent = new Intent(context, DeleteFileActivity.class); + intent.putExtra("repoFullNameForDeleteFile", fullName.getText()); + context.startActivity(intent); + break; + + case R.id.editFile: + + Intent intentW = new Intent(context, EditFileActivity.class); + intentW.putExtra("repoFullNameForEditFile", fullName.getText()); + context.startActivity(intentW); + break; + + case R.id.openInBrowser: + + Intent intentOpenInBrowser = new Intent(context, OpenFileInBrowserActivity.class); + intentOpenInBrowser.putExtra("fileFullNameBrowser", fullName.getText()); + context.startActivity(intentOpenInBrowser); + break; + + } + return false; + } + }); + + popupMenu.show(); + + } + });*/ + + } + } + + public FilesAdapter(Context mCtx, List filesListMain) { + this.mCtx = mCtx; + this.filesList = filesListMain; + filesListFull = new ArrayList<>(filesList); + } + + @NonNull + @Override + public FilesAdapter.FilesViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.files_list, parent, false); + return new FilesAdapter.FilesViewHolder(v); + } + + @Override + public void onBindViewHolder(@NonNull FilesAdapter.FilesViewHolder holder, int position) { + + Files currentItem = filesList.get(position); + + holder.fileType.setText(currentItem.getType()); + holder.fileName.setText(currentItem.getName()); + + if(currentItem.getType().equals("file")) { + holder.fileTypeImage.setImageDrawable(mCtx.getResources().getDrawable(R.drawable.ic_file_new)); + } + else if(currentItem.getType().equals("dir")) { + holder.fileTypeImage.setImageDrawable(mCtx.getResources().getDrawable(R.drawable.ic_folder_24)); + } + else { + holder.fileTypeImage.setImageDrawable(mCtx.getResources().getDrawable(R.drawable.ic_question_mark_24)); + } + + } + + @Override + public int getItemCount() { + return filesList.size(); + } + + @Override + public Filter getFilter() { + return filesFilter; + } + + private Filter filesFilter = new Filter() { + @Override + protected FilterResults performFiltering(CharSequence constraint) { + List filteredList = new ArrayList<>(); + + if (constraint == null || constraint.length() == 0) { + filteredList.addAll(filesListFull); + } else { + String filterPattern = constraint.toString().toLowerCase().trim(); + + for (Files item : filesListFull) { + if (item.getName().toLowerCase().contains(filterPattern) || item.getPath().toLowerCase().contains(filterPattern)) { + filteredList.add(item); + } + } + } + + FilterResults results = new FilterResults(); + results.values = filteredList; + + return results; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + filesList.clear(); + filesList.addAll((List) results.values); + notifyDataSetChanged(); + } + }; + +} diff --git a/app/src/main/java/org/mian/gitnex/adapters/MilestonesAdapter.java b/app/src/main/java/org/mian/gitnex/adapters/MilestonesAdapter.java index 239e2c75..c69d4062 100644 --- a/app/src/main/java/org/mian/gitnex/adapters/MilestonesAdapter.java +++ b/app/src/main/java/org/mian/gitnex/adapters/MilestonesAdapter.java @@ -237,6 +237,7 @@ public class MilestonesAdapter extends RecyclerView.Adapter>() { + @Override + public void onChanged(@Nullable List filesListMain) { + adapter = new FilesAdapter(getContext(), filesListMain); + if(adapter.getItemCount() > 0) { + mRecyclerView.setAdapter(adapter); + filesFrame.setVisibility(View.VISIBLE); + noDataFiles.setVisibility(View.GONE); + } + else { + adapter.notifyDataSetChanged(); + mRecyclerView.setAdapter(adapter); + filesFrame.setVisibility(View.VISIBLE); + noDataFiles.setVisibility(View.VISIBLE); + } + filesFrame.setVisibility(View.VISIBLE); + mProgressBar.setVisibility(View.GONE); + } + }); + + } + + private void fetchDataAsyncSub(String instanceUrl, String instanceToken, String owner, String repo, String filesDir) { + + FilesViewModel filesModel2 = new ViewModelProvider(this).get(FilesViewModel.class); + + filesModel2.getFilesList2(instanceUrl, instanceToken, owner, repo, filesDir).observe(this, new Observer>() { + @Override + public void onChanged(@Nullable List filesListMain2) { + adapter = new FilesAdapter(getContext(), filesListMain2); + if(adapter.getItemCount() > 0) { + mRecyclerView.setAdapter(adapter); + filesFrame.setVisibility(View.VISIBLE); + noDataFiles.setVisibility(View.GONE); + } + else { + adapter.notifyDataSetChanged(); + mRecyclerView.setAdapter(adapter); + filesFrame.setVisibility(View.VISIBLE); + noDataFiles.setVisibility(View.VISIBLE); + } + filesFrame.setVisibility(View.VISIBLE); + mProgressBar.setVisibility(View.GONE); + } + }); + + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + + boolean connToInternet = AppUtil.haveNetworkConnection(Objects.requireNonNull(getContext())); + + inflater.inflate(R.menu.search_menu, menu); + super.onCreateOptionsMenu(menu, inflater); + + MenuItem searchItem = menu.findItem(R.id.action_search); + androidx.appcompat.widget.SearchView searchView = (androidx.appcompat.widget.SearchView) searchItem.getActionView(); + searchView.setImeOptions(EditorInfo.IME_ACTION_DONE); + searchView.setQueryHint(getContext().getString(R.string.strFilter)); + + if(!connToInternet) { + return; + } + + searchView.setOnQueryTextListener(new androidx.appcompat.widget.SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + if(mRecyclerView.getAdapter() != null) { + adapter.getFilter().filter(newText); + } + return false; + } + }); + + } + + public void onButtonPressed(Uri uri) { + if (mListener != null) { + mListener.onFragmentInteraction(uri); + } + } + + @Override + public void onDetach() { + super.onDetach(); + mListener = null; + } + + public interface OnFragmentInteractionListener { + void onFragmentInteraction(Uri uri); + } +} diff --git a/app/src/main/java/org/mian/gitnex/fragments/RepoInfoFragment.java b/app/src/main/java/org/mian/gitnex/fragments/RepoInfoFragment.java index c3fab270..9bc55b87 100644 --- a/app/src/main/java/org/mian/gitnex/fragments/RepoInfoFragment.java +++ b/app/src/main/java/org/mian/gitnex/fragments/RepoInfoFragment.java @@ -247,7 +247,7 @@ public class RepoInfoFragment extends Fragment { repoRepoUrlInfo.setText(repoInfo.getHtml_url()); repoForksCountInfo.setText(repoInfo.getForks_count()); - if(repoInfo.getHas_issues()) { + if(repoInfo.getHas_issues() != null) { tinyDb.putBoolean("hasIssues", repoInfo.getHas_issues()); } else { diff --git a/app/src/main/java/org/mian/gitnex/interfaces/ApiInterface.java b/app/src/main/java/org/mian/gitnex/interfaces/ApiInterface.java index a59a64de..ff37c741 100644 --- a/app/src/main/java/org/mian/gitnex/interfaces/ApiInterface.java +++ b/app/src/main/java/org/mian/gitnex/interfaces/ApiInterface.java @@ -3,6 +3,7 @@ package org.mian.gitnex.interfaces; import com.google.gson.JsonElement; import org.mian.gitnex.models.AddEmail; import org.mian.gitnex.models.Branches; +import org.mian.gitnex.models.Files; import org.mian.gitnex.models.NewFile; import org.mian.gitnex.models.UpdateIssueAssignee; import org.mian.gitnex.models.UpdateIssueState; @@ -214,4 +215,13 @@ public interface ApiInterface { @POST("repos/{owner}/{repo}/contents/{file}") // create new file Call createNewFile(@Header("Authorization") String token, @Path("owner") String ownerName, @Path("repo") String repoName, @Path("file") String fileName, @Body NewFile jsonStr); + + @GET("repos/{owner}/{repo}/contents") // get all the files and dirs of a repository + Call> getFiles(@Header("Authorization") String token, @Path("owner") String ownerName, @Path("repo") String repoName); + + @GET("repos/{owner}/{repo}/contents/{file}") // get single file contents + Call getSingleFileContents(@Header("Authorization") String token, @Path("owner") String ownerName, @Path("repo") String repoName, @Path("file") String file); + + @GET("repos/{owner}/{repo}/contents/{fileDir}") // get all the sub files and dirs of a repository + Call> getDirFiles(@Header("Authorization") String token, @Path("owner") String ownerName, @Path("repo") String repoName, @Path("fileDir") String fileDir); } \ No newline at end of file diff --git a/app/src/main/java/org/mian/gitnex/models/Files.java b/app/src/main/java/org/mian/gitnex/models/Files.java new file mode 100644 index 00000000..4d2509af --- /dev/null +++ b/app/src/main/java/org/mian/gitnex/models/Files.java @@ -0,0 +1,74 @@ +package org.mian.gitnex.models; + +/** + * Author M M Arif + */ + +public class Files { + + private String name; + private String path; + private String sha; + private String type; + private int size; + private String encoding; + private String content; + private String target; + private String url; + private String html_url; + private String git_url; + private String download_url; + private String submodule_git_url; + + public String getName() { + return name; + } + + public String getPath() { + return path; + } + + public String getSha() { + return sha; + } + + public String getType() { + return type; + } + + public int getSize() { + return size; + } + + public String getEncoding() { + return encoding; + } + + public String getContent() { + return content; + } + + public String getTarget() { + return target; + } + + public String getUrl() { + return url; + } + + public String getHtml_url() { + return html_url; + } + + public String getGit_url() { + return git_url; + } + + public String getDownload_url() { + return download_url; + } + + public String getSubmodule_git_url() { + return submodule_git_url; + } +} diff --git a/app/src/main/java/org/mian/gitnex/viewmodels/FilesViewModel.java b/app/src/main/java/org/mian/gitnex/viewmodels/FilesViewModel.java new file mode 100644 index 00000000..c55aba8e --- /dev/null +++ b/app/src/main/java/org/mian/gitnex/viewmodels/FilesViewModel.java @@ -0,0 +1,112 @@ +package org.mian.gitnex.viewmodels; + +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; +import org.mian.gitnex.clients.RetrofitClient; +import org.mian.gitnex.models.Files; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +/** + * Author M M Arif + */ + +public class FilesViewModel extends ViewModel { + + private static MutableLiveData> filesList; + private static MutableLiveData> filesList2; + + public LiveData> getFilesList(String instanceUrl, String token, String owner, String repo) { + + filesList = new MutableLiveData<>(); + loadFilesList(instanceUrl, token, owner, repo); + + return filesList; + } + + public static void loadFilesList(String instanceUrl, String token, String owner, String repo) { + + Call> call = RetrofitClient + .getInstance(instanceUrl) + .getApiInterface() + .getFiles(token, owner, repo); + + call.enqueue(new Callback>() { + + @Override + public void onResponse(@NonNull Call> call, @NonNull Response> response) { + + Collections.sort(response.body(), new Comparator() { + @Override + public int compare(Files byType1, Files byType2) { + return byType1.getType().compareTo(byType2.getType()); + } + }); + + if (response.isSuccessful()) { + filesList.postValue(response.body()); + } else { + Log.i("onResponse", String.valueOf(response.code())); + } + + } + + @Override + public void onFailure(@NonNull Call> call, Throwable t) { + Log.i("onFailure", t.toString()); + } + + }); + } + + public LiveData> getFilesList2(String instanceUrl, String token, String owner, String repo, String filesDir) { + + filesList = new MutableLiveData<>(); + loadFilesList2(instanceUrl, token, owner, repo, filesDir); + + return filesList; + } + + public static void loadFilesList2(String instanceUrl, String token, String owner, String repo, String filesDir) { + + Call> call = RetrofitClient + .getInstance(instanceUrl) + .getApiInterface() + .getDirFiles(token, owner, repo, filesDir); + + call.enqueue(new Callback>() { + + @Override + public void onResponse(@NonNull Call> call, @NonNull Response> response) { + + Collections.sort(response.body(), new Comparator() { + @Override + public int compare(Files byType1, Files byType2) { + return byType1.getType().compareTo(byType2.getType()); + } + }); + + if (response.isSuccessful()) { + filesList.postValue(response.body()); + } else { + Log.i("onResponse", String.valueOf(response.code())); + } + + } + + @Override + public void onFailure(@NonNull Call> call, Throwable t) { + Log.i("onFailure", t.toString()); + } + + }); + } + +} diff --git a/app/src/main/res/drawable/ic_file_new.xml b/app/src/main/res/drawable/ic_file_new.xml new file mode 100644 index 00000000..65334152 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_new.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_folder_24.xml b/app/src/main/res/drawable/ic_folder_24.xml new file mode 100644 index 00000000..82347288 --- /dev/null +++ b/app/src/main/res/drawable/ic_folder_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_question_mark_24.xml b/app/src/main/res/drawable/ic_question_mark_24.xml new file mode 100644 index 00000000..5ec2577a --- /dev/null +++ b/app/src/main/res/drawable/ic_question_mark_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/progress_bar.xml b/app/src/main/res/drawable/progress_bar.xml index 3a724587..49ed5a6c 100644 --- a/app/src/main/res/drawable/progress_bar.xml +++ b/app/src/main/res/drawable/progress_bar.xml @@ -4,8 +4,8 @@ @@ -16,12 +16,12 @@ - + diff --git a/app/src/main/res/layout/activity_file_view.xml b/app/src/main/res/layout/activity_file_view.xml new file mode 100644 index 00000000..a22d64c8 --- /dev/null +++ b/app/src/main/res/layout/activity_file_view.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 15c882c4..892bbc4b 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -22,7 +22,7 @@ android:layout_marginBottom="20dp" android:baselineAligned="false" android:contentDescription="@string/app_name" - android:src="@mipmap/app_logo_round" /> + android:src="@mipmap/app_logo" /> + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_about.xml b/app/src/main/res/layout/fragment_about.xml index c4b740b7..258667d0 100644 --- a/app/src/main/res/layout/fragment_about.xml +++ b/app/src/main/res/layout/fragment_about.xml @@ -25,7 +25,7 @@ android:layout_marginTop="20dp" android:baselineAligned="false" android:contentDescription="@string/logo" - android:src="@mipmap/app_logo_round" /> + android:src="@mipmap/app_logo" /> + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/milestones_list.xml b/app/src/main/res/layout/milestones_list.xml index a7ec2a11..ab9f79d9 100644 --- a/app/src/main/res/layout/milestones_list.xml +++ b/app/src/main/res/layout/milestones_list.xml @@ -67,7 +67,7 @@ style="@style/Widget.AppCompat.ProgressBar.Horizontal" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_weight="82" + android:layout_weight="80" android:progress="50" android:layout_marginTop="2dp" android:progressDrawable="@drawable/progress_bar" diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index c4a603d4..00000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index c4a603d4..00000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/app_logo_foreground.png b/app/src/main/res/mipmap-hdpi/app_logo_foreground.png deleted file mode 100644 index 8f484afe..00000000 Binary files a/app/src/main/res/mipmap-hdpi/app_logo_foreground.png and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index b90640fa..00000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png deleted file mode 100644 index 679bec2e..00000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index 40e7342e..00000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/app_logo_foreground.png b/app/src/main/res/mipmap-mdpi/app_logo_foreground.png deleted file mode 100644 index 2ca2d59c..00000000 Binary files a/app/src/main/res/mipmap-mdpi/app_logo_foreground.png and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 8c3ea71f..00000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png deleted file mode 100644 index fb1f052f..00000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index 065d8df9..00000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/app_logo_foreground.png b/app/src/main/res/mipmap-xhdpi/app_logo_foreground.png deleted file mode 100644 index 423e26d8..00000000 Binary files a/app/src/main/res/mipmap-xhdpi/app_logo_foreground.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index d1c4adc8..00000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png deleted file mode 100644 index 45308007..00000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index 0b0d2bea..00000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/app_logo_foreground.png b/app/src/main/res/mipmap-xxhdpi/app_logo_foreground.png deleted file mode 100644 index f6529801..00000000 Binary files a/app/src/main/res/mipmap-xxhdpi/app_logo_foreground.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index dd998185..00000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png deleted file mode 100644 index 2346a242..00000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index bc103529..00000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/app_logo_foreground.png b/app/src/main/res/mipmap-xxxhdpi/app_logo_foreground.png deleted file mode 100644 index dc083f24..00000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/app_logo_foreground.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index eed78913..00000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png deleted file mode 100644 index ed1859f0..00000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index e6005c03..00000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 29579777..d84edbfc 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -114,6 +114,7 @@ Anzahl an Forks Erstellt Letzte Aktualisierung + um Erstellt\u0020 Fälligkeitsdatum @@ -135,6 +136,7 @@ Kommentar gepostet Etwas ist schief gelaufen. Bitte versuche es erneut Benutzerbild + Kein Meilenstein gefunden! Commit Autor: %1$s Commit Hash \n%1$s%2$s @@ -370,11 +372,16 @@ Issue Link kopieren Issue Link in Zwischenablage kopiert %1$d\uFF05 abgeschlossen + + + Keine Dateien gefunden + Keine Dateien gefunden Ordner sind noch nicht unterstützt Datei kann nicht angezeigt werden, da die API einen Fehler meldet Wurzel + OK Fertig diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 9b11fa9a..ad2fe689 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -114,6 +114,8 @@ Forks Count Created Last Updated + master + à Created\u0020 Due Date @@ -135,6 +137,7 @@ Comment posted Something went wrong, please try again IMG + No Milestone found! Commit author : %1$s Commit hash \n%1$s%2$s @@ -370,11 +373,16 @@ Copier l Issue URL Issue URL copied to clipboard %1$d\uFF05 terminé + + + Aucun fichier trouvé + Aucun fichier trouvé Directory browsing is not supported yet Sorry this file cannot be viewed as API returned an error Root + OK Done diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 7f54bda3..5161f921 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -114,6 +114,8 @@ Кол-во форков Создан Последнее обновление + master + в Создано\u0020 Дата исполнения @@ -135,6 +137,7 @@ Комментарий отправлен! Что-то пошло не так. Пожалуйста, попытайтесь еще раз. Аватар + Вех не обнаружено Автор коммита: %1$s Хеш коммита \n%1$s%2$s @@ -370,11 +373,16 @@ Copy Issue URL Issue URL copied to clipboard %1$d\uFF05 выполненный + + + Файлов не найдено + Файлов не найдено Directory browsing is not supported yet Sorry this file cannot be viewed as API returned an error Root + OK Готово diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 18630a54..f24e3eba 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1,10 +1,6 @@ - - - - 我的仓库 已点赞 @@ -66,7 +62,6 @@ Login to Gitea 1- Choose the correct protocol(https or http). \n2- Enter Gitea url e.g: try.gitea.io. \n3- If you have enabled 2FA for your account, enter the code in the OTP Code field. \n4- For HTTP basic auth use USERNAME@DOMAIN.COM in the URL field. Wrong username/password - :// It is not recommended to use HTTP protocol unless you are testing on local network. Malformed JSON was received. Server response was not successful. Instance URL is required @@ -93,13 +88,11 @@ Org Repo Pri - 12345 - 98324 - 54321 Remove Instance has returned an error. Code\u0020 RepoDetailActivity Details + Files Open Issues Closed Issues Milestones @@ -121,12 +114,7 @@ Forks Created Last Updated - 2018–10–30 08:25:25 - - 0 - master at - # Opened\u0020 Due Date Status: open @@ -138,7 +126,6 @@ Type: Issue Type: Pull Request Commenter:\u0020 - #%1$d %2$s Milestone %1$s Due %1$s Opened %1$s @@ -148,7 +135,6 @@ Comment posted Something went wrong, please try again Avatar - GitNex No milestones found Commit author: %1$s Commit hash \n%1$s%2$s @@ -166,7 +152,6 @@ Title Description Due Date - %1$d-%2$d-%3$d Milestone title is empty Milestone title is not valid. [a–z A–Z 0–9 – _] Milestone description exceeds the max 255 characters limit @@ -185,8 +170,6 @@ Due Date Milestone Labels - %1$s [%2$s] - %1$s - @%2$s Issue title is empty Issue description is empty New issue created successfully @@ -211,15 +194,15 @@ My Repositories Select Home Screen - No more data available. + No more data available New Label Repo Menu Label Name Label Color - Label name is empty. - Label name is not valid. + Label name is empty + Label name is not valid Label created. - Something went wrong, please try again. + Something went wrong, please try again Label updated. No labels found Desc @@ -232,7 +215,7 @@ Authorization Error - It seems that the Access Token is revoked OR your are not allowed to see these contents. In case of revoked Token, please logout and login again. + It seems that the Access Token is revoked OR your are not allowed to see these contents. In case of revoked Token, please logout and login again Cancel Logout Delete\u0020 @@ -258,16 +241,16 @@ Description Permission Access Controls - Members can view and clone team repositories. - Members can read and push to team repositories. - Members can pull and push to team repositories and add collaborators to them. - Please enter team name. - Team name should contain only alphanumeric, dash (-), underscore (_) and dot (.) characters. - Please select permission. - Team description have illegal characters. - Team description have more than 100 characters. - Team created successfully. - Something went wrong, please try again. + Members can view and clone team repositories + Members can read and push to team repositories + Members can pull and push to team repositories and add collaborators to them + Please enter team name + Team name should contain only alphanumeric, dash (-), underscore (_) and dot (.) characters + Please select permission + Team description have illegal characters + Team description have more than 100 characters + Team created successfully + Something went wrong, please try again Edit Comment @@ -291,8 +274,8 @@ Email Address New email added successfully. Email address is empty. - Email address is not valid. - Email address is already in use. + Email address is not valid + Email address is already in use Primary Emails @@ -387,6 +370,11 @@ Copy Issue URL Issue URL copied to clipboard %1$d\uFF05 completed + + No files found + Directory browsing is not supported yet + Sorry this file cannot be viewed as API returned an error + Root OK Done @@ -408,6 +396,5 @@ Location Max 255 characters All fields are required - Translate GitNex with Crowdin diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8cfb0ada..05a40be6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - + GitNex Developer : M M Arif gitnex@gitnex.com @@ -14,14 +14,10 @@ https://www.patreon.com/mmarif %1$s / build %2$d GitNex is a free, open-source Android client for Git repository management tool Gitea. GitNex is Licensed under GPLv3.\n\nThanks to all the contributors and sponsors for your generous work and donations. - https://crowdin.com/project/gitnex - - Report issues at Gitea Support the App on Liberapay Become a Patreon - My Repositories @@ -92,7 +88,7 @@ Login to Gitea 1- Choose the correct protocol(https or http). \n2- Enter Gitea url e.g: try.gitea.io. \n3- If you have enabled 2FA for your account, enter the code in the OTP Code field. \n4- For HTTP basic auth use USERNAME@DOMAIN.COM in the URL field. Wrong username/password - :// + :// It is not recommended to use HTTP protocol unless you are testing on local network. Malformed JSON was received. Server response was not successful. Instance URL is required @@ -124,14 +120,15 @@ Org Repo Pri - 12345 - 98324 - 54321 + 12345 + 98324 + 54321 Remove Instance has returned an error. Code\u0020 RepoDetailActivity Details + Files Open Issues Closed Issues Milestones @@ -155,13 +152,13 @@ Forks Created Last Updated - 2018–10–30 08:25:25 - - 0 - master + 2018–10–30 08:25:25 + + 0 + master at - # + # Opened\u0020 Due Date Status: open @@ -173,7 +170,7 @@ Type: Issue Type: Pull Request Commenter:\u0020 - #%1$d %2$s + #%1$d %2$s Milestone %1$s Due %1$s Opened %1$s @@ -184,7 +181,7 @@ Something went wrong, please try again Avatar - GitNex + GitNex No milestones found @@ -210,7 +207,7 @@ Title Description Due Date - %1$d-%2$d-%3$d + %1$d-%2$d-%3$d Milestone title is empty Milestone title is not valid. [a–z A–Z 0–9 – _] Milestone description exceeds the max 255 characters limit @@ -230,8 +227,8 @@ Due Date Milestone Labels - %1$s [%2$s] - %1$s - @%2$s + %1$s [%2$s] + %1$s - @%2$s Issue title is empty Issue description is empty New issue created successfully @@ -259,16 +256,16 @@ Select Home Screen - No more data available. + No more data available New Label Repo Menu Label Name Label Color - Label name is empty. - Label name is not valid. + Label name is empty + Label name is not valid Label created. - Something went wrong, please try again. + Something went wrong, please try again Label updated. No labels found @@ -287,7 +284,7 @@ Authorization Error - It seems that the Access Token is revoked OR your are not allowed to see these contents. In case of revoked Token, please logout and login again. + It seems that the Access Token is revoked OR your are not allowed to see these contents. In case of revoked Token, please logout and login again Cancel Logout Delete\u0020 @@ -315,18 +312,18 @@ Description Permission Access Controls - Members can view and clone team repositories. - Members can read and push to team repositories. - Members can pull and push to team repositories and add collaborators to them. + Members can view and clone team repositories + Members can read and push to team repositories + Members can pull and push to team repositories and add collaborators to them %1$s%2$s,\u0020 %1$s%2$s,\u0020 - Please enter team name. - Team name should contain only alphanumeric, dash (-), underscore (_) and dot (.) characters. - Please select permission. - Team description have illegal characters. - Team description have more than 100 characters. - Team created successfully. - Something went wrong, please try again. + Please enter team name + Team name should contain only alphanumeric, dash (-), underscore (_) and dot (.) characters + Please select permission + Team description have illegal characters + Team description have more than 100 characters + Team created successfully + Something went wrong, please try again @@ -353,8 +350,8 @@ Email Address New email added successfully. Email address is empty. - Email address is not valid. - Email address is already in use. + Email address is not valid + Email address is already in use Primary Emails \u0040\u0020%1$s @@ -470,6 +467,12 @@ %1$d\uFF05 completed + + No files found + Directory browsing is not supported yet + Sorry this file cannot be viewed as API returned an error + Root + OK Done @@ -494,7 +497,6 @@ Location Max 255 characters All fields are required - Translate GitNex with Crowdin diff --git a/build.gradle b/build.gradle index e29809a6..020c4010 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:3.5.1' } } diff --git a/fastlane/metadata/android/en-US/changelogs/61.txt b/fastlane/metadata/android/en-US/changelogs/61.txt new file mode 100644 index 00000000..5d5caf18 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/61.txt @@ -0,0 +1,6 @@ +Bugfix: crash when repo avatar in not present in api nodes. old Gitea instances +Bugfix: Milestone progress bar fix +Bugfix: Create new issue enable/disable fix + +For more, check the release notes. +https://gitea.com/mmarif/GitNex/releases \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/62.txt b/fastlane/metadata/android/en-US/changelogs/62.txt new file mode 100644 index 00000000..6400e6fd --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/62.txt @@ -0,0 +1,4 @@ +Bugfix: crash when my repo, starred repos are accessed without repo avatar. old Gitea instances + +For more, check the release notes. +https://gitea.com/mmarif/GitNex/releases \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/63.txt b/fastlane/metadata/android/en-US/changelogs/63.txt new file mode 100644 index 00000000..7bf7544f --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/63.txt @@ -0,0 +1,13 @@ +2.1.3 +Bugfix: another old Gitea instances API issue + +2.1.0 +- New: Font - Roboto +- New: Milestone progress bar @6543 +- New: Redesign issues list +- New: Copy issue URL to clipboard @6543 +- New: Redesign milestones list +- New: Added repository avatars + +For more, check the release notes. +https://gitea.com/mmarif/GitNex/releases \ No newline at end of file