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
[
](https://f-droid.org/en/packages/org.mian.gitnex/)
[
](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
[
](https://www.openapk.net/gitnex-for-forgejo-and-gitea/org.mian.gitnex/)
[
](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
-| | | | |
-|:---:|:---:|:---:|:---:|
-| [
](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/001.png) | [
](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/002.png) | [
](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/003.png) | [
](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/004.png) |
-| [
](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/005.png) | [
](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/006.png) | [
](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/007.png) | [
](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/008.png) |
+[
](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/001.png) | [
](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/002.png) | [
](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/003.png) | [
](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/004.png)
+---|---|---|---
+[
](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/005.png) | [
](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/006.png) | [
](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/007.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
+