repository topics (show, add, delete)

This commit is contained in:
M M Arif
2025-11-09 19:25:08 +05:00
parent ddad33f71c
commit 5cece62923
8 changed files with 336 additions and 16 deletions

View File

@@ -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
[<img alt='Get it on F-Droid' src='https://codeberg.org/gitnex/GitNex/raw/branch/main/assets/fdroid.png' height="80"/>](https://f-droid.org/en/packages/org.mian.gitnex/)
[<img alt='Get it on Google Play' src='https://codeberg.org/gitnex/GitNex/raw/branch/main/assets/google-play.png' height="80"/>](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
[<img alt='Get it on OpenAPK' src='https://codeberg.org/gitnex/GitNex/raw/branch/main/assets/openapk.png' height="82"/>](https://www.openapk.net/gitnex-for-forgejo-and-gitea/org.mian.gitnex/)
[<img alt='Get it on IzzyOnDroid' src='https://codeberg.org/gitnex/GitNex/raw/branch/main/assets/IzzyOnDroid.png' height="82"/>](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
| | | | |
|:---:|:---:|:---:|:---:|
| [<img src="https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/001.png" alt="Home Screen" width="200"/>](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/001.png) | [<img src="https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/002.png" alt="Repository View" width="200"/>](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/002.png) | [<img src="https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/003.png" alt="Issues" width="200"/>](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/003.png) | [<img src="https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/004.png" alt="Code View" width="200"/>](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/004.png) |
| [<img src="https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/005.png" alt="Pull Requests" width="200"/>](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/005.png) | [<img src="https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/006.png" alt="Notifications" width="200"/>](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/006.png) | [<img src="https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/007.png" alt="User Profile" width="200"/>](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/007.png) | [<img src="https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/008.png" alt="Settings" width="200"/>](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/008.png) |
[<img src="https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/001.png" alt="001.png" width="200"/>](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/001.png) | [<img src="https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/002.png" alt="002.png" width="200"/>](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/002.png) | [<img src="https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/003.png" alt="003.png" width="200"/>](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/003.png) | [<img src="https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/004.png" alt="004.png" width="200"/>](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/004.png)
---|---|---|---
[<img src="https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/005.png" alt="005.png" width="200"/>](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/005.png) | [<img src="https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/006.png" alt="006.png" width="200"/>](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/006.png) | [<img src="https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/007.png" alt="007.png" width="200"/>](https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/007.png) | [<img src="https://codeberg.org/gitnex/GitNex/raw/branch/main/fastlane/metadata/android/en-US/images/phoneScreenshots/008.png" alt="008.png" width="200"/>](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:

View File

@@ -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<Topics> 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<Void> addRepoTopic(
@Path("owner") String owner, @Path("repo") String repo, @Path("topic") String topic);
@DELETE("repos/{owner}/{repo}/topics/{topic}") // delete a repo topic
Call<Void> deleteRepoTopic(
@Path("owner") String owner, @Path("repo") String repo, @Path("topic") String topic);
}

View File

@@ -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<String> topics;
public List<String> getTopics() {
return topics;
}
}

View File

@@ -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<Topics> call =
Objects.requireNonNull(ApiRetrofitClient.getInstance(getContext()))
.getRepoTopics(repository.getOwner(), repository.getName(), 1, resultLimit);
call.enqueue(
new Callback<>() {
@Override
public void onResponse(
@NonNull Call<Topics> call, @NonNull Response<Topics> 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<Topics> call, @NonNull Throwable t) {
if (isAdded()) {
Toasty.error(ctx, ctx.getString(R.string.errorLoadingTopics));
binding.repoTopicsContainer.setVisibility(View.GONE);
}
}
});
}
private void displayTopics(List<String> 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<Void> call =
Objects.requireNonNull(ApiRetrofitClient.getInstance(getContext()))
.deleteRepoTopic(repository.getOwner(), repository.getName(), topic);
call.enqueue(
new Callback<>() {
@Override
public void onResponse(
@NonNull Call<Void> call, @NonNull Response<Void> 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<Void> 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<Void> call =
Objects.requireNonNull(ApiRetrofitClient.getInstance(getContext()))
.addRepoTopic(repository.getOwner(), repository.getName(), topic);
call.enqueue(
new Callback<>() {
@Override
public void onResponse(
@NonNull Call<Void> call, @NonNull Response<Void> 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<Void> call, @NonNull Throwable t) {
if (isAdded()) {
Toasty.error(ctx, ctx.getString(R.string.errorAddingTopic));
}
}
});
}
}

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="wrap_content"
android:orientation="vertical"
android:padding="@dimen/dimen16dp">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/topicName"
app:boxBackgroundMode="outline">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/topicInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

View File

@@ -94,6 +94,48 @@
android:textColorLink="@color/lightBlue"
android:textSize="@dimen/dimen16sp"/>
<LinearLayout
android:id="@+id/repoTopicsContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone"
android:layout_marginBottom="@dimen/dimen16dp"
android:clipChildren="false"
android:clipToPadding="false">
<com.google.android.material.chip.ChipGroup
android:id="@+id/repoTopicsChipGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/dimen4dp"
app:singleLine="false"
app:chipSpacingHorizontal="@dimen/dimen8dp"
app:chipSpacingVertical="@dimen/dimen2dp">
<com.google.android.material.chip.Chip
android:id="@+id/addTopicChip"
style="@style/Widget.MaterialComponents.Chip.Action"
android:layout_width="@dimen/dimen48dp"
android:layout_height="@dimen/dimen48dp"
android:text=""
android:clickable="true"
android:focusable="true"
app:chipIcon="@drawable/ic_add"
app:chipIconVisible="true"
app:chipIconSize="@dimen/dimen20dp"
app:chipIconTint="?attr/primaryTextColor"
app:chipBackgroundColor="@android:color/transparent"
app:chipStrokeWidth="0dp"
app:checkedIconVisible="false"
app:rippleColor="@android:color/transparent"
app:closeIconVisible="false"
app:ensureMinTouchTargetSize="false" />
</com.google.android.material.chip.ChipGroup>
</LinearLayout>
<com.google.android.material.card.MaterialCardView
android:id="@+id/language_stats_card"
android:layout_width="match_parent"

View File

@@ -89,4 +89,10 @@
<color name="alert_warning_border">#DAA038</color>
<color name="alert_caution_border">#D73A49</color>
<color name="alert_text">#24292E</color>
<color name="chipColor1">#E3F2FD</color>
<color name="chipColor2">#E8F5E8</color>
<color name="chipColor3">#FFF3E0</color>
<color name="chipColor4">#F3E5F5</color>
<color name="chipColor5">#FFEBEE</color>
</resources>

View File

@@ -1039,4 +1039,14 @@
<string name="create_variable_header">Variables will be passed to certain actions and cannot be read otherwise.</string>
<string name="variable_name_helper">Case-insensitive, alphanumeric characters or underscores only, cannot start with GITEA_ or GITHUB_</string>
<string name="variable_value_helper">Input any content. Whitespace at the start and end will be omitted.</string>
</resources>
<string name="addTopic">Add Topic</string>
<string name="addNewTopic">Add New Topic</string>
<string name="topicName">Topic Name</string>
<string name="topicAddedSuccessfully">Topic added successfully</string>
<string name="topicDeletedSuccessfully">Topic deleted successfully</string>
<string name="errorAddingTopic">Error adding topic</string>
<string name="errorDeletingTopic">Error deleting topic</string>
<string name="errorLoadingTopics">Error loading topics</string>
<string name="invalidTopicName">Invalid topic name</string>
</resources>