From 8061078c747b57079d71f72a534ed5b0d5e69a6d Mon Sep 17 00:00:00 2001 From: M M Arif Date: Fri, 9 Jan 2026 19:28:55 +0100 Subject: [PATCH] Features and improvements (#1561) - Add zig lang to files and lang stats bar - Fix bottom nav menu colors - HTTP basic auth - Add pagination to filter labels for issues closes #1559 closes #1560 closes #803 Reviewed-on: https://codeberg.org/gitnex/GitNex/pulls/1561 Co-authored-by: M M Arif Co-committed-by: M M Arif --- README.md | 5 + app/build.gradle | 24 +-- .../mian/gitnex/activities/LoginActivity.java | 161 +++++++++++++++++- .../gitnex/activities/RepoDetailActivity.java | 134 ++++++++++++--- .../gitnex/api/clients/ApiRetrofitClient.java | 48 +++++- .../gitnex/clients/BasicAuthInterceptor.java | 38 +++++ .../mian/gitnex/clients/GlideHttpClient.java | 53 +++++- .../org/mian/gitnex/clients/GlideService.java | 11 +- .../mian/gitnex/clients/RetrofitClient.java | 107 +++++++++++- .../gitnex/database/api/UserAccountsApi.java | 6 + .../gitnex/database/dao/UserAccountsDao.java | 4 + .../gitnex/database/db/GitnexDatabase.java | 16 +- .../gitnex/database/models/UserAccount.java | 18 ++ .../java/org/mian/gitnex/helpers/AppUtil.java | 4 +- .../org/mian/gitnex/helpers/FileIcon.java | 2 + .../languagestatistics/LanguageColor.java | 1 + app/src/main/res/drawable/ic_file_zig.xml | 9 + app/src/main/res/layout/activity_login.xml | 16 ++ app/src/main/res/layout/activity_main.xml | 2 - .../res/layout/custom_dialog_proxy_auth.xml | 73 ++++++++ .../layout/custom_filter_issues_by_labels.xml | 28 ++- .../res/layout/fragment_organization_info.xml | 1 + app/src/main/res/values/gitea_version.xml | 2 +- .../res/values/language_statistics_colors.xml | 1 + app/src/main/res/values/strings.xml | 14 +- app/src/main/res/xml/changelog.xml | 16 +- 26 files changed, 707 insertions(+), 87 deletions(-) create mode 100644 app/src/main/java/org/mian/gitnex/clients/BasicAuthInterceptor.java create mode 100644 app/src/main/res/drawable/ic_file_zig.xml create mode 100644 app/src/main/res/layout/custom_dialog_proxy_auth.xml diff --git a/README.md b/README.md index 3e89888f..6c0d7299 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,11 @@ JSON Configuration: } ``` +## HTTP Basic Auth + +GitNex from version **12.0.0** supports HTTP Basic Authentication for servers behind reverse proxies (nginx, Apache). +[Read the Wiki](https://codeberg.org/gitnex/GitNex/wiki/HTTP-Basic-Auth-for-Reverse-Proxies) + ## Links - [Website](https://gitnex.com) diff --git a/app/build.gradle b/app/build.gradle index e31b1522..f30ebb10 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,5 @@ plugins { - id "com.diffplug.spotless" version "8.0.0" + id "com.diffplug.spotless" version "8.1.0" } apply plugin: 'com.android.application' @@ -8,8 +8,8 @@ android { applicationId = "org.mian.gitnex" minSdkVersion 26 targetSdkVersion 36 - versionCode = 1100 - versionName = "11.0.0" + versionCode = 1195 + versionName = "12.0.0-dev" multiDexEnabled = true testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" compileSdk = 36 @@ -82,19 +82,19 @@ dependencies { implementation 'androidx.viewpager2:viewpager2:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:2.2.1' implementation "androidx.legacy:legacy-support-v4:1.0.0" - implementation "androidx.navigation:navigation-fragment:2.9.5" - implementation "androidx.navigation:navigation-ui:2.9.5" - implementation "androidx.lifecycle:lifecycle-viewmodel:2.9.4" + implementation "androidx.navigation:navigation-fragment:2.9.6" + implementation "androidx.navigation:navigation-ui:2.9.6" + implementation "androidx.lifecycle:lifecycle-viewmodel:2.10.0" testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0' - implementation 'com.squareup.okhttp3:okhttp:5.3.0' + implementation 'com.squareup.okhttp3:okhttp:5.3.2' implementation 'com.google.code.gson:gson:2.13.2' implementation 'com.github.ramseth001:TextDrawable:1.1.3' implementation 'com.squareup.retrofit2:retrofit:3.0.0' implementation 'com.squareup.retrofit2:converter-gson:3.0.0' implementation 'com.squareup.retrofit2:converter-scalars:3.0.0' - implementation 'com.squareup.okhttp3:logging-interceptor:5.3.0' + implementation 'com.squareup.okhttp3:logging-interceptor:5.3.2' implementation 'org.ocpsoft.prettytime:prettytime:5.0.7.Final' implementation "com.github.skydoves:colorpickerview:2.3.0" implementation "io.noties.markwon:core:4.6.2" @@ -114,17 +114,17 @@ dependencies { implementation "com.github.bumptech.glide:okhttp3-integration:5.0.5" annotationProcessor 'com.github.bumptech.glide:compiler:5.0.5' implementation "com.caverock:androidsvg-aar:1.4" - implementation "pl.droidsonroids.gif:android-gif-drawable:1.2.29" + implementation "pl.droidsonroids.gif:android-gif-drawable:1.2.30" implementation 'com.google.guava:guava:33.5.0-jre' //noinspection NewerVersionAvailable,GradleDependency implementation 'commons-io:commons-io:2.5' - implementation 'org.apache.commons:commons-lang3:3.19.0' + implementation 'org.apache.commons:commons-lang3:3.20.0' implementation "com.github.chrisbanes:PhotoView:2.3.0" implementation 'ch.acra:acra-mail:5.13.1' implementation 'ch.acra:acra-limiter:5.13.1' implementation 'ch.acra:acra-notification:5.13.1' - implementation 'androidx.room:room-runtime:2.8.3' - annotationProcessor 'androidx.room:room-compiler:2.8.3' + implementation 'androidx.room:room-runtime:2.8.4' + annotationProcessor 'androidx.room:room-compiler:2.8.4' implementation "androidx.work:work-runtime:2.11.0" implementation "io.mikael:urlbuilder:2.0.9" implementation "org.codeberg.gitnex-garage:emoji-java:v5.1.2" diff --git a/app/src/main/java/org/mian/gitnex/activities/LoginActivity.java b/app/src/main/java/org/mian/gitnex/activities/LoginActivity.java index bc91bae2..7d6ff235 100644 --- a/app/src/main/java/org/mian/gitnex/activities/LoginActivity.java +++ b/app/src/main/java/org/mian/gitnex/activities/LoginActivity.java @@ -13,6 +13,7 @@ import android.os.Bundle; import android.util.TypedValue; import android.view.View; import android.widget.ArrayAdapter; +import android.widget.Button; import android.widget.TextView; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; @@ -20,6 +21,7 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.core.text.HtmlCompat; import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.textfield.TextInputEditText; import io.mikael.urlbuilder.UrlBuilder; import java.io.File; import java.io.FileNotFoundException; @@ -61,6 +63,8 @@ public class LoginActivity extends BaseActivity { private boolean hasShownInitialNetworkError = false; private int btnText; private String selectedProvider = "gitea"; + private String proxyAuthUsername = null; + private String proxyAuthPassword = null; @Override public void onCreate(Bundle savedInstanceState) { @@ -96,6 +100,9 @@ public class LoginActivity extends BaseActivity { UserAccount account = userAccountsApi.getAccountById(accountId); if (account != null) { + proxyAuthUsername = account.getProxyAuthUsername(); + proxyAuthPassword = account.getProxyAuthPassword(); + // Prefill provider selectedProvider = account.getProvider(); if (selectedProvider.equals("gitea")) { @@ -150,6 +157,8 @@ public class LoginActivity extends BaseActivity { activityLoginBinding.restoreFromBackup.setVisibility(View.VISIBLE); } + activityLoginBinding.setupProxyAuth.setOnClickListener(view -> showProxyAuthDialog()); + NetworkStatusObserver networkStatusObserver = NetworkStatusObserver.getInstance(ctx); activityLoginBinding.appVersion.setText(AppUtil.getAppVersion(appCtx)); @@ -253,6 +262,101 @@ public class LoginActivity extends BaseActivity { }); } + private void showProxyAuthDialog() { + View dialogView = getLayoutInflater().inflate(R.layout.custom_dialog_proxy_auth, null); + + TextInputEditText usernameInput = dialogView.findViewById(R.id.proxyUsername); + TextInputEditText passwordInput = dialogView.findViewById(R.id.proxyPassword); + + if (proxyAuthUsername != null && !proxyAuthUsername.isEmpty()) { + usernameInput.setText(proxyAuthUsername); + } + if (proxyAuthPassword != null && !proxyAuthPassword.isEmpty()) { + passwordInput.setText(proxyAuthPassword); + } + + MaterialAlertDialogBuilder dialogBuilder = + new MaterialAlertDialogBuilder(this) + .setView(dialogView) + .setNegativeButton(R.string.clear_proxy_creds, null) + .setNeutralButton(R.string.skip_proxy_creds, null) + .setPositiveButton(R.string.save_proxy_creds, null); + + AlertDialog dialog = dialogBuilder.create(); + + dialog.setOnShowListener( + dialogInterface -> { + Button positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE); + Button negativeButton = dialog.getButton(AlertDialog.BUTTON_NEGATIVE); + Button neutralButton = dialog.getButton(AlertDialog.BUTTON_NEUTRAL); + + positiveButton.setOnClickListener( + view -> { + String enteredUsername = + usernameInput.getText() != null + ? usernameInput.getText().toString().trim() + : ""; + String enteredPassword = + passwordInput.getText() != null + ? passwordInput.getText().toString().trim() + : ""; + + if (enteredUsername.isEmpty() && enteredPassword.isEmpty()) { + proxyAuthUsername = null; + proxyAuthPassword = null; + SnackBar.info( + ctx, + findViewById(android.R.id.content), + getString(R.string.proxy_creds_cleared)); + dialog.dismiss(); + return; + } + + boolean usernameFilled = !enteredUsername.isEmpty(); + boolean passwordFilled = !enteredPassword.isEmpty(); + + if (usernameFilled != passwordFilled) { + SnackBar.error( + ctx, + findViewById(android.R.id.content), + getString(R.string.procy_creds_required_msg)); + + if (usernameFilled) { + passwordInput.setText(""); + passwordInput.requestFocus(); + } else { + usernameInput.setText(""); + usernameInput.requestFocus(); + } + return; + } + + proxyAuthUsername = enteredUsername; + proxyAuthPassword = enteredPassword; + SnackBar.info( + ctx, + findViewById(android.R.id.content), + getString(R.string.proxy_creds_saved)); + dialog.dismiss(); + }); + + negativeButton.setOnClickListener( + view -> { + proxyAuthUsername = null; + proxyAuthPassword = null; + SnackBar.info( + ctx, + findViewById(android.R.id.content), + getString(R.string.proxy_creds_cleared)); + dialog.dismiss(); + }); + + neutralButton.setOnClickListener(view -> dialog.dismiss()); + }); + + dialog.show(); + } + private void showTokenHelpDialog() { MaterialAlertDialogBuilder dialogBuilder = @@ -364,7 +468,13 @@ public class LoginActivity extends BaseActivity { private void serverPageLimitSettings(String instanceUrl, String loginToken) { Call generalAPISettings = - RetrofitClient.getApiInterface(ctx, instanceUrl, "token " + loginToken, null) + RetrofitClient.getApiInterface( + ctx, + instanceUrl, + "token " + loginToken, + null, + proxyAuthUsername, + proxyAuthPassword) .getGeneralAPISettings(); generalAPISettings.enqueue( new Callback<>() { @@ -398,7 +508,12 @@ public class LoginActivity extends BaseActivity { Call callVersion = RetrofitClient.getApiInterface( - ctx, instanceUrl.toString(), "token " + loginToken, null) + ctx, + instanceUrl.toString(), + "token " + loginToken, + null, + proxyAuthUsername, + proxyAuthPassword) .getVersion(); callVersion.enqueue( @@ -496,7 +611,12 @@ public class LoginActivity extends BaseActivity { Call call = RetrofitClient.getApiInterface( - ctx, instanceUrl.toString(), "token " + loginToken, null) + ctx, + instanceUrl.toString(), + "token " + loginToken, + null, + proxyAuthUsername, + proxyAuthPassword) .userGetCurrent(); call.enqueue( @@ -531,6 +651,23 @@ public class LoginActivity extends BaseActivity { maxResponseItems, defaultPagingNumber, selectedProvider); + + // Save or clear proxy credentials + userAccountsApi.updateProxyAuthCredentials( + (int) accountId, + (proxyAuthUsername != null + && !proxyAuthUsername.isEmpty() + && proxyAuthPassword != null + && !proxyAuthPassword.isEmpty()) + ? proxyAuthUsername + : null, + (proxyAuthUsername != null + && !proxyAuthUsername.isEmpty() + && proxyAuthPassword != null + && !proxyAuthPassword.isEmpty()) + ? proxyAuthPassword + : null); + account = userAccountsApi.getAccountById((int) accountId); } else { userAccountsApi.updateTokenByAccountName( @@ -540,6 +677,24 @@ public class LoginActivity extends BaseActivity { userAccountsApi .getAccountByName(accountName) .getAccountId()); + + UserAccount existingAccount = + userAccountsApi.getAccountByName(accountName); + userAccountsApi.updateProxyAuthCredentials( + existingAccount.getAccountId(), + (proxyAuthUsername != null + && !proxyAuthUsername.isEmpty() + && proxyAuthPassword != null + && !proxyAuthPassword.isEmpty()) + ? proxyAuthUsername + : null, + (proxyAuthUsername != null + && !proxyAuthUsername.isEmpty() + && proxyAuthPassword != null + && !proxyAuthPassword.isEmpty()) + ? proxyAuthPassword + : null); + userAccountsApi.login( userAccountsApi .getAccountByName(accountName) 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 cffc5380..64032f22 100644 --- a/app/src/main/java/org/mian/gitnex/activities/RepoDetailActivity.java +++ b/app/src/main/java/org/mian/gitnex/activities/RepoDetailActivity.java @@ -22,6 +22,7 @@ import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.Toolbar; +import androidx.core.widget.NestedScrollView; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; import androidx.viewpager2.adapter.FragmentStateAdapter; @@ -37,6 +38,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.function.Consumer; import java.util.stream.Collectors; import org.gitnex.tea4j.v2.models.Label; import org.gitnex.tea4j.v2.models.Milestone; @@ -386,43 +388,129 @@ public class RepoDetailActivity extends BaseActivity implements BottomSheetListe } private void showLabelFilterDialog() { - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); View dialogView = LayoutInflater.from(this).inflate(R.layout.custom_filter_issues_by_labels, null); FlexboxLayout labelsContainer = dialogView.findViewById(R.id.labelsContainer); Button filterButton = dialogView.findViewById(R.id.filterButton); + LinearProgressIndicator progressIndicator = dialogView.findViewById(R.id.progressBar); + + ViewGroup parent = (ViewGroup) labelsContainer.getParent(); + NestedScrollView scrollView = null; + if (parent != null && parent.getParent() instanceof NestedScrollView) { + scrollView = (NestedScrollView) parent.getParent(); + } labelsContainer.removeAllViews(); - for (Label label : labelsList) { - Chip chip = - (Chip) - LayoutInflater.from(this) - .inflate( - R.layout.list_filter_issues_by_labels, - labelsContainer, - false); - chip.setText(label.getName()); - chip.setCheckable(true); - chip.setChecked( - Boolean.TRUE.equals(selectedStates.getOrDefault(label.getName(), false))); + final Map selectedStates = new HashMap<>(); + final int[] currentPage = {1}; + final boolean[] isLoading = {false}; + final int pageSize = 50; - GradientDrawable dot = new GradientDrawable(); - dot.setShape(GradientDrawable.OVAL); - dot.setSize(16, 16); - dot.setColor(Color.parseColor("#" + label.getColor())); - chip.setChipIcon(dot); + Consumer> addLabelsToView = + (newLabels) -> { + for (Label label : newLabels) { + Chip chip = + (Chip) + LayoutInflater.from(this) + .inflate( + R.layout.list_filter_issues_by_labels, + labelsContainer, + false); + chip.setText(label.getName()); + chip.setCheckable(true); + chip.setChecked(Boolean.TRUE.equals(selectedStates.get(label.getName()))); - chip.setOnCheckedChangeListener( - (buttonView, isChecked) -> { - selectedStates.put(label.getName(), isChecked); - }); + GradientDrawable dot = new GradientDrawable(); + dot.setShape(GradientDrawable.OVAL); + dot.setSize(16, 16); + dot.setColor(Color.parseColor("#" + label.getColor())); + chip.setChipIcon(dot); - labelsContainer.addView(chip); + chip.setOnCheckedChangeListener( + (buttonView, isChecked) -> { + selectedStates.put(label.getName(), isChecked); + }); + + labelsContainer.addView(chip); + } + }; + + Consumer loadLabels = + (page) -> { + if (isLoading[0]) return; + + isLoading[0] = true; + progressIndicator.setVisibility(View.VISIBLE); + + Call> call = + RetrofitClient.getApiInterface(this) + .issueListLabels( + repository.getOwner(), + repository.getName(), + page, + pageSize); + + call.enqueue( + new Callback<>() { + @Override + public void onResponse( + @NonNull Call> call, + @NonNull Response> response) { + if (response.isSuccessful() && response.body() != null) { + List