diff --git a/README.md b/README.md index c5cc44a4..3e89888f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ GitNex is a FOSS Android client (available both as gratis & paid) for the Git re Licensed under the GPLv3 License. Please refer to the LICENSE file for the full text of the license. **No trackers are used**, and the source code is available here for anyone to audit. -## đŸ“Ĩ Downloads +## Downloads [Get it on F-Droid](https://f-droid.org/en/packages/org.mian.gitnex/) [Get it on Google Play](https://play.google.com/store/apps/details?id=org.mian.gitnex.pro) @@ -16,17 +16,17 @@ Licensed under the GPLv3 License. Please refer to the LICENSE file for the full [Get it on OpenAPK](https://www.openapk.net/gitnex-for-forgejo-and-gitea/org.mian.gitnex/) [Get it on IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/org.mian.gitnex) -## â„šī¸ Note About Forgejo and Gitea Version +## Note About Forgejo and Gitea Version For the best experience, please ensure that you are using the latest stable release or later of your Forgejo or Gitea instance. -## 🔨 Build from Source +## Build from Source **Option 1:** Download the source code, open it in Android Studio, and build it there. **Option 2:** Open the terminal (Linux) and navigate to the project directory. Then, run: `./gradlew assembleFree` -## ✨ Features +## Features - Multiple accounts support - File and directory browser @@ -40,24 +40,23 @@ For the best experience, please ensure that you are using the latest stable rele - Repositories / issues list - [And much more...](https://codeberg.org/gitnex/GitNex/wiki/Features) -## 🤝 Contributing +## Contributing We welcome contributions! For information regarding contribution guidelines, please click [here](https://codeberg.org/gitnex/GitNex/wiki/Contributing). -## 🌐 Translation +## Translation We use [Crowdin](https://crowdin.com/project/gitnex) for translations. If you can, please help improve the translation for your language. If your language is not listed, please request to add it to the project [here](https://codeberg.org/gitnex/GitNex/issues). **Translation Portal: https://crowdin.com/project/GitNex** -## 📸 Screenshots +## Screenshots -| | | | | -|:---:|:---:|:---:|:---:| -| [Home Screen](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/001.png) | [Repository View](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/002.png) | [Issues](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/003.png) | [Code View](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/004.png) | -| [Pull Requests](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/005.png) | [Notifications](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/006.png) | [User Profile](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/007.png) | [Settings](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/008.png) | +[001.png](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/001.png) | [002.png](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/002.png) | [003.png](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/003.png) | [004.png](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/004.png) +---|---|---|--- +[005.png](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/005.png) | [006.png](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/006.png) | [007.png](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/007.png) | [008.png](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/008.png) -## 🔗 Add a Custom URL Scheme +## Add a Custom URL Scheme Starting with version 11.0.0, GitNex supports a custom URL scheme. This feature allows you to seamlessly open links directly in GitNex for issues, pull requests, commits, profiles, and repositories by using third-party apps like [URL Check](https://github.com/TrianguloY/URLCheck). @@ -77,7 +76,7 @@ JSON Configuration: } ``` -## 🔗 Links +## Links - [Website](https://gitnex.com) - [Wiki](https://codeberg.org/gitnex/GitNex/wiki/Home) @@ -85,7 +84,7 @@ JSON Configuration: - [FAQ](https://codeberg.org/gitnex/GitNex/wiki/FAQ) - [Release Blog](https://gitnex.codeberg.page) -## 🙏 Thanks +## Thanks Thank you to all the open source libraries, contributors, and donors who make GitNex possible. @@ -115,7 +114,7 @@ Thank you to all the open source libraries, contributors, and donors who make Gi - [google/material-design-icons](https://github.com/google/material-design-icons) - [tabler/tabler-icons](https://github.com/tabler/tabler-icons) -## 📱 Social +## Social Follow on social media: diff --git a/app/src/main/java/org/mian/gitnex/api/clients/ApiInterface.java b/app/src/main/java/org/mian/gitnex/api/clients/ApiInterface.java index 3bd1ca76..59578173 100644 --- a/app/src/main/java/org/mian/gitnex/api/clients/ApiInterface.java +++ b/app/src/main/java/org/mian/gitnex/api/clients/ApiInterface.java @@ -2,8 +2,11 @@ package org.mian.gitnex.api.clients; import java.util.List; import org.mian.gitnex.api.models.contents.RepoGetContentsList; +import org.mian.gitnex.api.models.topics.Topics; import retrofit2.Call; +import retrofit2.http.DELETE; import retrofit2.http.GET; +import retrofit2.http.PUT; import retrofit2.http.Path; import retrofit2.http.Query; @@ -22,4 +25,19 @@ public interface ApiInterface { @Path("repo") String repo, @Path("path") String path, @Query("ref") String ref); + + @GET("repos/{owner}/{repo}/topics") // get list of topics for a repo + Call getRepoTopics( + @Path("owner") String owner, + @Path("repo") String repo, + @Query("page") int page, + @Query("limit") int limit); + + @PUT("repos/{owner}/{repo}/topics/{topic}") // add a new topic for a repo + Call addRepoTopic( + @Path("owner") String owner, @Path("repo") String repo, @Path("topic") String topic); + + @DELETE("repos/{owner}/{repo}/topics/{topic}") // delete a repo topic + Call deleteRepoTopic( + @Path("owner") String owner, @Path("repo") String repo, @Path("topic") String topic); } diff --git a/app/src/main/java/org/mian/gitnex/api/models/topics/Topics.java b/app/src/main/java/org/mian/gitnex/api/models/topics/Topics.java new file mode 100644 index 00000000..672b3c06 --- /dev/null +++ b/app/src/main/java/org/mian/gitnex/api/models/topics/Topics.java @@ -0,0 +1,17 @@ +package org.mian.gitnex.api.models.topics; + +import com.google.gson.annotations.SerializedName; +import java.util.List; + +/** + * @author mmarif + */ +public class Topics { + + @SerializedName("topics") + private List topics; + + public List getTopics() { + return topics; + } +} 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 e2bc9ef2..cdc58424 100644 --- a/app/src/main/java/org/mian/gitnex/fragments/RepoInfoFragment.java +++ b/app/src/main/java/org/mian/gitnex/fragments/RepoInfoFragment.java @@ -10,6 +10,7 @@ import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.ViewParent; import android.widget.LinearLayout; import android.widget.TextView; import androidx.annotation.NonNull; @@ -20,10 +21,13 @@ import com.google.android.material.chip.Chip; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.shape.CornerFamily; import com.google.android.material.shape.ShapeAppearanceModel; +import com.google.android.material.textfield.TextInputEditText; import java.io.IOException; import java.util.ArrayList; +import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import okhttp3.ResponseBody; import org.apache.commons.io.FileUtils; import org.gitnex.tea4j.v2.models.Organization; @@ -36,11 +40,14 @@ import org.mian.gitnex.activities.RepoDetailActivity; import org.mian.gitnex.activities.RepoForksActivity; import org.mian.gitnex.activities.RepoStargazersActivity; import org.mian.gitnex.activities.RepoWatchersActivity; +import org.mian.gitnex.api.clients.ApiRetrofitClient; +import org.mian.gitnex.api.models.topics.Topics; import org.mian.gitnex.clients.RetrofitClient; import org.mian.gitnex.databinding.FragmentRepoInfoBinding; import org.mian.gitnex.helpers.AlertDialogs; import org.mian.gitnex.helpers.AppUtil; import org.mian.gitnex.helpers.ClickListener; +import org.mian.gitnex.helpers.Constants; import org.mian.gitnex.helpers.Markdown; import org.mian.gitnex.helpers.TimeHelper; import org.mian.gitnex.helpers.Toasty; @@ -89,6 +96,9 @@ public class RepoInfoFragment extends Fragment { setRepoInfo(locale); + loadRepoTopics(); + binding.addTopicChip.setOnClickListener(v -> showAddTopicDialog()); + if (repository.isStarred()) { binding.repoMetaStarsIcon.setImageDrawable( ContextCompat.getDrawable(requireContext(), R.drawable.ic_star)); @@ -488,4 +498,199 @@ public class RepoInfoFragment extends Fragment { } }); } + + private void loadRepoTopics() { + int resultLimit = Constants.getCurrentResultLimit(requireContext()); + + Call call = + Objects.requireNonNull(ApiRetrofitClient.getInstance(getContext())) + .getRepoTopics(repository.getOwner(), repository.getName(), 1, resultLimit); + + call.enqueue( + new Callback<>() { + @Override + public void onResponse( + @NonNull Call call, @NonNull Response response) { + if (isAdded()) { + switch (response.code()) { + case 200: + if (response.body() != null + && !response.body().getTopics().isEmpty()) { + binding.repoTopicsContainer.setVisibility(View.VISIBLE); + displayTopics(response.body().getTopics()); + } else { + binding.repoTopicsContainer.setVisibility(View.GONE); + } + break; + case 401: + AlertDialogs.authorizationTokenRevokedDialog(ctx); + break; + default: + binding.repoTopicsContainer.setVisibility(View.GONE); + break; + } + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + if (isAdded()) { + Toasty.error(ctx, ctx.getString(R.string.errorLoadingTopics)); + binding.repoTopicsContainer.setVisibility(View.GONE); + } + } + }); + } + + private void displayTopics(List topics) { + binding.repoTopicsChipGroup.removeAllViews(); + + int[] chipColors = { + ContextCompat.getColor(ctx, R.color.chipColor1), + ContextCompat.getColor(ctx, R.color.chipColor2), + ContextCompat.getColor(ctx, R.color.chipColor3), + ContextCompat.getColor(ctx, R.color.chipColor4), + ContextCompat.getColor(ctx, R.color.chipColor5) + }; + + for (int i = 0; i < topics.size(); i++) { + String topic = topics.get(i); + Chip chip = createTopicChip(topic, chipColors[i % chipColors.length]); + binding.repoTopicsChipGroup.addView(chip); + } + + Chip plusChip = binding.addTopicChip; + ViewParent parent = plusChip.getParent(); + if (parent instanceof ViewGroup) { + ((ViewGroup) parent).removeView(plusChip); + } + binding.repoTopicsChipGroup.addView(plusChip); + } + + private Chip createTopicChip(String topic, int backgroundColor) { + Chip chip = new Chip(ctx); + chip.setText(topic); + chip.setCloseIconVisible(true); + chip.setCloseIconTint( + ColorStateList.valueOf(ContextCompat.getColor(ctx, R.color.colorRed))); + chip.setChipBackgroundColor(ColorStateList.valueOf(backgroundColor)); + chip.setTextColor(isLightColor(backgroundColor) ? Color.BLACK : Color.WHITE); + + chip.setShapeAppearanceModel( + new ShapeAppearanceModel() + .toBuilder() + .setAllCorners( + CornerFamily.ROUNDED, + getResources().getDimension(R.dimen.dimen8dp)) + .build()); + + chip.setOnCloseIconClickListener(v -> deleteTopic(topic)); + + return chip; + } + + private void deleteTopic(String topic) { + Call call = + Objects.requireNonNull(ApiRetrofitClient.getInstance(getContext())) + .deleteRepoTopic(repository.getOwner(), repository.getName(), topic); + + call.enqueue( + new Callback<>() { + @Override + public void onResponse( + @NonNull Call call, @NonNull Response response) { + if (isAdded()) { + switch (response.code()) { + case 204: + Toasty.success( + ctx, ctx.getString(R.string.topicDeletedSuccessfully)); + loadRepoTopics(); + break; + case 401: + AlertDialogs.authorizationTokenRevokedDialog(ctx); + break; + case 403: + Toasty.error(ctx, ctx.getString(R.string.unauthorizedApiError)); + break; + default: + Toasty.error(ctx, ctx.getString(R.string.errorDeletingTopic)); + break; + } + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + if (isAdded()) { + Toasty.error(ctx, ctx.getString(R.string.errorDeletingTopic)); + } + } + }); + } + + private void showAddTopicDialog() { + View dialogView = LayoutInflater.from(ctx).inflate(R.layout.custom_dialog_add_topic, null); + TextInputEditText topicInput = dialogView.findViewById(R.id.topicInput); + + MaterialAlertDialogBuilder dialogBuilder = + new MaterialAlertDialogBuilder(ctx) + .setTitle(R.string.addNewTopic) + .setView(dialogView) + .setPositiveButton( + R.string.addButton, + (dialog, which) -> { + String topicName = + Objects.requireNonNull(topicInput.getText()) + .toString() + .trim(); + if (!topicName.isEmpty()) { + addNewTopic(topicName); + } + }) + .setNegativeButton(R.string.cancelButton, null); + + dialogBuilder.create().show(); + } + + private void addNewTopic(String topic) { + Call call = + Objects.requireNonNull(ApiRetrofitClient.getInstance(getContext())) + .addRepoTopic(repository.getOwner(), repository.getName(), topic); + + call.enqueue( + new Callback<>() { + @Override + public void onResponse( + @NonNull Call call, @NonNull Response response) { + if (isAdded()) { + switch (response.code()) { + case 204: + Toasty.success( + ctx, ctx.getString(R.string.topicAddedSuccessfully)); + loadRepoTopics(); + break; + case 401: + AlertDialogs.authorizationTokenRevokedDialog(ctx); + break; + case 403: + Toasty.error(ctx, ctx.getString(R.string.unauthorizedApiError)); + break; + case 422: + Toasty.error(ctx, ctx.getString(R.string.invalidTopicName)); + break; + default: + Toasty.error(ctx, ctx.getString(R.string.errorAddingTopic)); + break; + } + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + if (isAdded()) { + Toasty.error(ctx, ctx.getString(R.string.errorAddingTopic)); + } + } + }); + } } diff --git a/app/src/main/res/layout/custom_dialog_add_topic.xml b/app/src/main/res/layout/custom_dialog_add_topic.xml new file mode 100644 index 00000000..beb88688 --- /dev/null +++ b/app/src/main/res/layout/custom_dialog_add_topic.xml @@ -0,0 +1,23 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_repo_info.xml b/app/src/main/res/layout/fragment_repo_info.xml index 418d15f2..2d025a45 100644 --- a/app/src/main/res/layout/fragment_repo_info.xml +++ b/app/src/main/res/layout/fragment_repo_info.xml @@ -94,6 +94,48 @@ android:textColorLink="@color/lightBlue" android:textSize="@dimen/dimen16sp"/> + + + + + + + + + + #DAA038 #D73A49 #24292E + + #E3F2FD + #E8F5E8 + #FFF3E0 + #F3E5F5 + #FFEBEE diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 90fb9092..63e7f933 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1039,4 +1039,14 @@ Variables will be passed to certain actions and cannot be read otherwise. Case-insensitive, alphanumeric characters or underscores only, cannot start with GITEA_ or GITHUB_ Input any content. Whitespace at the start and end will be omitted. - + + Add Topic + Add New Topic + Topic Name + Topic added successfully + Topic deleted successfully + Error adding topic + Error deleting topic + Error loading topics + Invalid topic name +