Merge branch 'main' of codeberg.org:gitnex/GitNex

This commit is contained in:
M M Arif
2026-01-11 15:56:34 +05:00
26 changed files with 707 additions and 87 deletions

View File

@@ -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)

View File

@@ -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"

View File

@@ -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> 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<ServerVersion> 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<User> 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)

View File

@@ -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<String, Boolean> 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<List<Label>> 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<Integer> loadLabels =
(page) -> {
if (isLoading[0]) return;
isLoading[0] = true;
progressIndicator.setVisibility(View.VISIBLE);
Call<List<Label>> call =
RetrofitClient.getApiInterface(this)
.issueListLabels(
repository.getOwner(),
repository.getName(),
page,
pageSize);
call.enqueue(
new Callback<>() {
@Override
public void onResponse(
@NonNull Call<List<Label>> call,
@NonNull Response<List<Label>> response) {
if (response.isSuccessful() && response.body() != null) {
List<Label> newLabels = response.body();
if (page == 1) {
selectedStates.clear();
labelsContainer.removeAllViews();
}
if (page == 1) {
labelsList.clear();
}
labelsList.addAll(newLabels);
for (Label label : newLabels) {
selectedStates.putIfAbsent(label.getName(), false);
}
addLabelsToView.accept(newLabels);
}
isLoading[0] = false;
progressIndicator.setVisibility(View.GONE);
}
@Override
public void onFailure(
@NonNull Call<List<Label>> call, @NonNull Throwable t) {
isLoading[0] = false;
progressIndicator.setVisibility(View.GONE);
Toasty.error(
RepoDetailActivity.this,
getString(R.string.genericServerResponseError));
}
});
};
if (scrollView != null) {
scrollView.setOnScrollChangeListener(
(NestedScrollView.OnScrollChangeListener)
(v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
if (!isLoading[0] && v.getChildAt(0) != null) {
View child = v.getChildAt(0);
if (child.getBottom() <= (v.getHeight() + v.getScrollY())) {
currentPage[0]++;
loadLabels.accept(currentPage[0]);
}
}
});
}
loadLabels.accept(currentPage[0]);
AlertDialog dialog = builder.setView(dialogView).create();
filterButton.setOnClickListener(

View File

@@ -16,6 +16,7 @@ import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.mian.gitnex.activities.BaseActivity;
import org.mian.gitnex.clients.BasicAuthInterceptor;
import org.mian.gitnex.helpers.AppDatabaseSettings;
import org.mian.gitnex.helpers.AppUtil;
import org.mian.gitnex.helpers.FilesData;
@@ -38,15 +39,28 @@ public class ApiRetrofitClient {
var account = ((BaseActivity) context).getAccount().getAccount();
String url = account.getInstanceUrl();
String token = ((BaseActivity) context).getAccount().getAuthorization();
String proxyUsername = account.getProxyAuthUsername();
String proxyPassword = account.getProxyAuthPassword();
File cacheFile = new File(context.getCacheDir(), "http-cache");
String key = token.hashCode() + "@" + url;
return instances.computeIfAbsent(key, k -> createApi(context, url, token, cacheFile));
if (proxyUsername != null && proxyPassword != null) {
key += "@proxy@" + proxyUsername.hashCode();
}
return instances.computeIfAbsent(
key, k -> createApi(context, url, token, cacheFile, proxyUsername, proxyPassword));
}
private static ApiInterface createApi(
Context context, String url, String token, File cacheFile) {
OkHttpClient client = buildOkHttpClient(context, token, cacheFile);
Context context,
String url,
String token,
File cacheFile,
String proxyUsername,
String proxyPassword) {
OkHttpClient client =
buildOkHttpClient(context, token, cacheFile, proxyUsername, proxyPassword);
Retrofit retrofit =
new Retrofit.Builder()
.baseUrl(url)
@@ -57,7 +71,17 @@ public class ApiRetrofitClient {
return retrofit.create(ApiInterface.class);
}
private static OkHttpClient buildOkHttpClient(Context context, String token, File cacheFile) {
private static ApiInterface createApi(
Context context, String url, String token, File cacheFile) {
return createApi(context, url, token, cacheFile, null, null);
}
private static OkHttpClient buildOkHttpClient(
Context context,
String token,
File cacheFile,
String proxyUsername,
String proxyPassword) {
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
MemorizingTrustManager trustManager = new MemorizingTrustManager(context);
@@ -73,8 +97,16 @@ public class ApiRetrofitClient {
.hostnameVerifier(
trustManager.wrapHostnameVerifier(
HttpsURLConnection.getDefaultHostnameVerifier()))
.addInterceptor(userAgentInterceptor(context))
.addInterceptor(authInterceptor(token));
.addInterceptor(userAgentInterceptor(context));
if (proxyUsername != null
&& !proxyUsername.isEmpty()
&& proxyPassword != null
&& !proxyPassword.isEmpty()) {
builder.addInterceptor(new BasicAuthInterceptor(proxyUsername, proxyPassword));
}
builder.addInterceptor(authInterceptor(token));
if (cacheFile != null) {
int cacheSize = getCacheSize(context);
@@ -91,6 +123,10 @@ public class ApiRetrofitClient {
}
}
private static OkHttpClient buildOkHttpClient(Context context, String token, File cacheFile) {
return buildOkHttpClient(context, token, cacheFile, null, null);
}
private static Interceptor userAgentInterceptor(Context ctx) {
return chain -> {
Request req =

View File

@@ -0,0 +1,38 @@
package org.mian.gitnex.clients;
import androidx.annotation.NonNull;
import java.io.IOException;
import java.util.Base64;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
/**
* @author mmarif
*/
public class BasicAuthInterceptor implements Interceptor {
private final String username;
private final String password;
public BasicAuthInterceptor(String username, String password) {
this.username = username;
this.password = password;
}
@NonNull @Override
public Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
String credentials = username + ":" + password;
String encodedCredentials = Base64.getEncoder().encodeToString(credentials.getBytes());
Request modifiedRequest =
originalRequest
.newBuilder()
.header("X-Proxy-Auth", "Basic " + encodedCredentials)
.build();
return chain.proceed(modifiedRequest);
}
}

View File

@@ -8,6 +8,7 @@ import javax.net.ssl.X509TrustManager;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import org.gitnex.tea4j.v2.auth.ApiKeyAuth;
import org.mian.gitnex.activities.BaseActivity;
import org.mian.gitnex.helpers.AppUtil;
import org.mian.gitnex.helpers.ssl.MemorizingTrustManager;
@@ -16,7 +17,8 @@ import org.mian.gitnex.helpers.ssl.MemorizingTrustManager;
*/
public class GlideHttpClient {
public static OkHttpClient getOkHttpClient(Context context, String token) {
public static OkHttpClient getOkHttpClient(
Context context, String token, String proxyUsername, String proxyPassword) {
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
MemorizingTrustManager memorizingTrustManager = new MemorizingTrustManager(context);
@@ -49,8 +51,16 @@ public class GlideHttpClient {
+ ")")
.build();
return chain.proceed(modifiedRequest);
})
.addInterceptor(auth);
});
if (proxyUsername != null
&& !proxyUsername.isEmpty()
&& proxyPassword != null
&& !proxyPassword.isEmpty()) {
builder.addInterceptor(new BasicAuthInterceptor(proxyUsername, proxyPassword));
}
builder.addInterceptor(auth);
return builder.build();
@@ -59,7 +69,30 @@ public class GlideHttpClient {
}
}
public static OkHttpClient getUnsafeOkHttpClient(String token) {
public static OkHttpClient getOkHttpClient(Context context, String token) {
return getOkHttpClient(context, token, null, null);
}
public static OkHttpClient getOkHttpClientForCurrentAccount(Context context) {
if (!(context instanceof BaseActivity)) {
return getOkHttpClient(context, "");
}
var accountWrapper = ((BaseActivity) context).getAccount();
if (accountWrapper == null || accountWrapper.getAccount() == null) {
return getOkHttpClient(context, "");
}
var account = accountWrapper.getAccount();
String token = accountWrapper.getAuthorization();
String proxyUsername = account.getProxyAuthUsername();
String proxyPassword = account.getProxyAuthPassword();
return getOkHttpClient(context, token, proxyUsername, proxyPassword);
}
public static OkHttpClient getUnsafeOkHttpClient(
String token, String proxyUsername, String proxyPassword) {
try {
@SuppressWarnings("CustomX509TrustManager")
final X509TrustManager trustAllCerts =
@@ -90,8 +123,16 @@ public class GlideHttpClient {
OkHttpClient.Builder builder =
new OkHttpClient.Builder()
.sslSocketFactory(sslSocketFactory, trustAllCerts)
.hostnameVerifier((hostname, session) -> true)
.addInterceptor(auth);
.hostnameVerifier((hostname, session) -> true);
if (proxyUsername != null
&& !proxyUsername.isEmpty()
&& proxyPassword != null
&& !proxyPassword.isEmpty()) {
builder.addInterceptor(new BasicAuthInterceptor(proxyUsername, proxyPassword));
}
builder.addInterceptor(auth);
return builder.build();

View File

@@ -27,11 +27,16 @@ public class GlideService extends AppGlideModule {
@Override
public void registerComponents(
@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
String token = "";
OkHttpClient okHttpClient;
if (context instanceof BaseActivity) {
token = ((BaseActivity) context).getAccount().getAuthorization();
okHttpClient = GlideHttpClient.getOkHttpClientForCurrentAccount(context);
} else {
String token = "";
okHttpClient = GlideHttpClient.getOkHttpClient(context, token);
}
OkHttpClient okHttpClient = GlideHttpClient.getOkHttpClient(context, token);
registry.replace(
GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory(okHttpClient));
}

View File

@@ -58,7 +58,12 @@ public class RetrofitClient {
private static final int CACHE_SIZE_MB = 50;
private static final int MAX_STALE_SECONDS = 60 * 60 * 24 * 30;
private static OkHttpClient buildOkHttpClient(Context context, String token, File cacheFile) {
private static OkHttpClient buildOkHttpClient(
Context context,
String token,
File cacheFile,
String proxyUsername,
String proxyPassword) {
// HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
// logging.setLevel(HttpLoggingInterceptor.Level.BODY);
@@ -97,6 +102,13 @@ public class RetrofitClient {
return chain.proceed(modifiedRequest);
});
if (proxyUsername != null
&& !proxyUsername.isEmpty()
&& proxyPassword != null
&& !proxyPassword.isEmpty()) {
okHttpClient.addInterceptor(new BasicAuthInterceptor(proxyUsername, proxyPassword));
}
if (cacheFile != null) {
int cacheSize = CACHE_SIZE_MB;
try {
@@ -178,7 +190,18 @@ public class RetrofitClient {
private static Retrofit createRetrofit(
Context context, String instanceUrl, String token, File cacheFile) {
OkHttpClient okHttpClient = buildOkHttpClient(context, token, cacheFile);
return createRetrofit(context, instanceUrl, token, cacheFile, null, null);
}
private static Retrofit createRetrofit(
Context context,
String instanceUrl,
String token,
File cacheFile,
String proxyUsername,
String proxyPassword) {
OkHttpClient okHttpClient =
buildOkHttpClient(context, token, cacheFile, proxyUsername, proxyPassword);
return new Retrofit.Builder()
.baseUrl(instanceUrl)
.client(okHttpClient)
@@ -194,7 +217,13 @@ public class RetrofitClient {
public static OkHttpClient getOkHttpClient(Context context, String token) {
File cacheFile = new File(context.getCacheDir(), "http-cache");
return buildOkHttpClient(context, token, cacheFile);
return buildOkHttpClient(context, token, cacheFile, null, null);
}
public static OkHttpClient getOkHttpClient(
Context context, String token, String proxyUsername, String proxyPassword) {
File cacheFile = new File(context.getCacheDir(), "http-cache");
return buildOkHttpClient(context, token, cacheFile, proxyUsername, proxyPassword);
}
public static ApiInterface getApiInterface(Context context) {
@@ -204,39 +233,79 @@ public class RetrofitClient {
throw new IllegalStateException(
"No active account available. Use explicit URL and token.");
}
var account = ((BaseActivity) context).getAccount().getAccount();
String proxyUsername = account.getProxyAuthUsername();
String proxyPassword = account.getProxyAuthPassword();
return getApiInterface(
context,
((BaseActivity) context).getAccount().getAccount().getInstanceUrl(),
account.getInstanceUrl(),
((BaseActivity) context).getAccount().getAuthorization(),
((BaseActivity) context).getAccount().getCacheDir(context));
((BaseActivity) context).getAccount().getCacheDir(context),
proxyUsername,
proxyPassword);
}
public static WebApi getWebInterface(Context context) {
String instanceUrl = ((BaseActivity) context).getAccount().getAccount().getInstanceUrl();
instanceUrl = instanceUrl.substring(0, instanceUrl.lastIndexOf("api/v1/"));
var account = ((BaseActivity) context).getAccount().getAccount();
String proxyUsername = account.getProxyAuthUsername();
String proxyPassword = account.getProxyAuthPassword();
return getWebInterface(
context,
instanceUrl,
((BaseActivity) context).getAccount().getWebAuthorization(),
((BaseActivity) context).getAccount().getCacheDir(context));
((BaseActivity) context).getAccount().getCacheDir(context),
proxyUsername,
proxyPassword);
}
public static WebApi getWebInterface(Context context, String url) {
var account = ((BaseActivity) context).getAccount().getAccount();
String proxyUsername = account.getProxyAuthUsername();
String proxyPassword = account.getProxyAuthPassword();
return getWebInterface(
context,
url,
((BaseActivity) context).getAccount().getAuthorization(),
((BaseActivity) context).getAccount().getCacheDir(context));
((BaseActivity) context).getAccount().getCacheDir(context),
proxyUsername,
proxyPassword);
}
public static ApiInterface getApiInterface(
Context context, String url, String token, File cacheFile) {
return getApiInterface(context, url, token, cacheFile, null, null);
}
public static ApiInterface getApiInterface(
Context context,
String url,
String token,
File cacheFile,
String proxyUsername,
String proxyPassword) {
String key = (token != null ? token.hashCode() : 0) + "@" + url;
if (proxyUsername != null && proxyPassword != null) {
key += "@proxy@" + proxyUsername.hashCode();
}
if (cacheFile == null || !apiInterfaces.containsKey(key)) {
synchronized (RetrofitClient.class) {
if (cacheFile == null || !apiInterfaces.containsKey(key)) {
ApiInterface apiInterface =
Objects.requireNonNull(createRetrofit(context, url, token, cacheFile))
Objects.requireNonNull(
createRetrofit(
context,
url,
token,
cacheFile,
proxyUsername,
proxyPassword))
.create(ApiInterface.class);
if (cacheFile != null) {
apiInterfaces.put(key, apiInterface);
@@ -250,12 +319,32 @@ public class RetrofitClient {
public static WebApi getWebInterface(
Context context, String url, String token, File cacheFile) {
return getWebInterface(context, url, token, cacheFile, null, null);
}
public static WebApi getWebInterface(
Context context,
String url,
String token,
File cacheFile,
String proxyUsername,
String proxyPassword) {
String key = (token != null ? token.hashCode() : 0) + "@" + url;
if (proxyUsername != null && proxyPassword != null) {
key += "@proxy@" + proxyUsername.hashCode();
}
if (!webInterfaces.containsKey(key)) {
synchronized (RetrofitClient.class) {
if (!webInterfaces.containsKey(key)) {
WebApi webInterface =
Objects.requireNonNull(createRetrofit(context, url, token, cacheFile))
Objects.requireNonNull(
createRetrofit(
context,
url,
token,
cacheFile,
proxyUsername,
proxyPassword))
.create(WebApi.class);
webInterfaces.put(key, webInterface);
return webInterface;

View File

@@ -123,4 +123,10 @@ public class UserAccountsApi extends BaseApi {
public void updateProvider(final String provider, final int accountId) {
executorService.execute(() -> userAccountsDao.updateProvider(provider, accountId));
}
public void updateProxyAuthCredentials(
final int accountId, final String username, final String password) {
executorService.execute(
() -> userAccountsDao.updateProxyAuthCredentials(username, password, accountId));
}
}

View File

@@ -84,4 +84,8 @@ public interface UserAccountsDao {
@Query("UPDATE UserAccounts SET provider = :provider WHERE accountId = :accountId")
void updateProvider(String provider, int accountId);
@Query(
"UPDATE UserAccounts SET proxyAuthUsername = :username, proxyAuthPassword = :password WHERE accountId = :accountId")
void updateProxyAuthCredentials(String username, String password, int accountId);
}

View File

@@ -21,7 +21,7 @@ import org.mian.gitnex.database.models.UserAccount;
*/
@Database(
entities = {Repository.class, UserAccount.class, Notes.class, AppSettings.class},
version = 11,
version = 12,
exportSchema = false)
public abstract class GitnexDatabase extends RoomDatabase {
@@ -119,6 +119,17 @@ public abstract class GitnexDatabase extends RoomDatabase {
}
};
private static final Migration MIGRATION_11_12 =
new Migration(11, 12) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL(
"ALTER TABLE 'userAccounts' ADD COLUMN 'proxyAuthUsername' TEXT");
database.execSQL(
"ALTER TABLE 'userAccounts' ADD COLUMN 'proxyAuthPassword' TEXT");
}
};
private static volatile GitnexDatabase gitnexDatabase;
public static GitnexDatabase getDatabaseInstance(Context context) {
@@ -141,7 +152,8 @@ public abstract class GitnexDatabase extends RoomDatabase {
MIGRATION_7_8,
MIGRATION_8_9,
MIGRATION_9_10,
MIGRATION_10_11)
MIGRATION_10_11,
MIGRATION_11_12)
.build();
}
}

View File

@@ -25,6 +25,8 @@ public class UserAccount implements Serializable {
private int maxAttachmentsSize;
private int maxNumberOfAttachments;
private String provider;
@Nullable private String proxyAuthUsername;
@Nullable private String proxyAuthPassword;
public int getAccountId() {
return accountId;
@@ -121,4 +123,20 @@ public class UserAccount implements Serializable {
public void setProvider(String provider) {
this.provider = provider;
}
@Nullable public String getProxyAuthUsername() {
return proxyAuthUsername;
}
public void setProxyAuthUsername(@Nullable String username) {
this.proxyAuthUsername = username;
}
@Nullable public String getProxyAuthPassword() {
return proxyAuthPassword;
}
public void setProxyAuthPassword(@Nullable String password) {
this.proxyAuthPassword = password;
}
}

View File

@@ -196,7 +196,9 @@ public class AppUtil {
"next",
"nvmrc",
"lock",
"vue"
"vue",
"zig",
"zon"
},
FileType.TEXT);
extensions.put(new String[] {"ttf", "otf", "woff", "woff2", "ttc", "eot"}, FileType.FONT);

View File

@@ -128,6 +128,8 @@ public class FileIcon {
extensionIcons.put("nvmrc", R.drawable.ic_node_js);
extensionIcons.put("license", R.drawable.ic_license);
extensionIcons.put("vue", R.drawable.ic_vue);
extensionIcons.put("zig", R.drawable.ic_file_zig);
extensionIcons.put("zon", R.drawable.ic_file_zig);
}
public static int getIconResource(String fileName, String type) {

View File

@@ -207,6 +207,7 @@ public class LanguageColor {
colors.put("sed", R.color.sed);
colors.put("xBase", R.color.x_base);
colors.put("D", R.color.wiki);
colors.put("Zig", R.color.zig);
}
public static int languageColor(String key) {

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="32" android:viewportWidth="32" android:width="24dp">
<path android:fillColor="#f7a41d" android:pathData="M5.733,19.731l0,-7.467l2.8,0l0,-3.733l-6.533,0l0,14.933l3.547,0l3.36,-3.733l-3.174,0z"/>
<path android:fillColor="#f7a41d" android:pathData="M26.453,8.531l-3.36,3.733l3.174,0l0,7.467l-2.8,0l0,3.733l6.533,0l0,-14.933l-3.547,0z"/>
<path android:fillColor="#f7a41d" android:pathData="M26.875,6.707l-6.362,1.824l-11.046,0l0,3.733l7.38,0l-11.732,13.029l6.382,-1.829l11.036,0l0,-3.733l-7.385,0l11.727,-13.024z"/>
</vector>

View File

@@ -202,6 +202,22 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/setup_proxy_auth"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen8dp"
android:layout_marginBottom="@dimen/dimen8dp"
android:text="@string/setup_proxy_auth"
app:icon="@drawable/ic_lock"
app:iconGravity="textStart"
android:textStyle="bold"
app:iconTint="?attr/materialCardBackgroundColor"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/login_button"
android:textColor="?attr/materialCardBackgroundColor" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView

View File

@@ -56,8 +56,6 @@
app:backgroundTint="?attr/navigationBarColor"
app:menu="@menu/bottom_nav_menu"
app:labelVisibilityMode="labeled"
app:itemIconTint="?attr/iconsColor"
app:itemTextColor="?attr/primaryTextColor"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />

View File

@@ -0,0 +1,73 @@
<?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/dimen24dp">
<com.google.android.material.textview.MaterialTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/dimen16dp"
android:text="@string/proxy_auth_title"
android:textAppearance="?attr/textAppearanceHeadlineSmall" />
<com.google.android.material.textview.MaterialTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/dimen16dp"
android:text="@string/proxy_auth_description"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?android:attr/textColorSecondary" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/proxyUsernameLayout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/dimen16dp"
android:hint="@string/proxy_username_hint"
app:endIconMode="clear_text"
app:endIconTint="?attr/iconsColor"
app:hintTextColor="?attr/hintColor"
app:startIconDrawable="@drawable/ic_person"
app:startIconTint="?attr/iconsColor">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/proxyUsername"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPersonName"
android:maxLines="1"
android:textColor="?attr/inputTextColor"
android:textColorHint="?attr/hintColor" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/proxyPasswordLayout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/dimen24dp"
android:hint="@string/proxy_password_hint"
app:endIconMode="password_toggle"
app:endIconTint="?attr/iconsColor"
app:hintTextColor="?attr/hintColor"
app:startIconDrawable="@drawable/ic_lock"
app:startIconTint="?attr/iconsColor">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/proxyPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:maxLines="1"
android:textColor="?attr/inputTextColor"
android:textColorHint="?attr/hintColor" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

View File

@@ -7,20 +7,36 @@
android:orientation="vertical"
android:padding="@dimen/dimen16dp">
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
style="@style/Widget.MaterialComponents.LinearProgressIndicator"
app:indicatorColor="?attr/progressIndicatorColor"
android:visibility="gone"/>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:maxHeight="@dimen/dimen320dp">
<com.google.android.flexbox.FlexboxLayout
android:id="@+id/labelsContainer"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:flexWrap="wrap"
app:justifyContent="flex_start"
app:alignItems="flex_start"
android:padding="@dimen/dimen4dp"/>
android:orientation="vertical">
<com.google.android.flexbox.FlexboxLayout
android:id="@+id/labelsContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:flexWrap="wrap"
app:justifyContent="flex_start"
app:alignItems="flex_start"
android:padding="@dimen/dimen4dp"/>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@@ -44,6 +44,7 @@
android:layout_height="@dimen/dimen96dp"
style="?attr/materialCardViewFilledStyle"
android:layout_gravity="center_horizontal"
android:backgroundTint="@android:color/transparent"
app:cardElevation="@dimen/dimen0dp"
android:layout_marginTop="@dimen/dimen12dp"
android:layout_marginBottom="@dimen/dimen12dp"

View File

@@ -2,6 +2,6 @@
<resources>
<string name="versionLow" translatable="false">1.24</string>
<string name="versionHigh" translatable="false">14.0.0</string>
<string name="versionHigh" translatable="false">15.0.0</string>
</resources>

View File

@@ -163,4 +163,5 @@
<color name="zep">#118f9e</color>
<color name="sed">#64b970</color>
<color name="x_base">#403a40</color>
<color name="zig">#EC915C</color>
</resources>

View File

@@ -1057,4 +1057,16 @@
<string name="invalidTopicName">Invalid topic name</string>
<string name="http_git">HTTP Git</string>
</resources>
<string name="proxy_auth_title">Reverse Proxy Credentials</string>
<string name="proxy_auth_description">If your Gitea/Forgejo is behind a reverse proxy with HTTP Basic Authentication, enter the credentials here.\n\nGitNex sends these in the \'X-Proxy-Auth: Basic …\' header. Administrators needs to configure the proxy to read from this header.</string>
<string name="proxy_username_hint">Proxy Username</string>
<string name="proxy_password_hint">Proxy Password</string>
<string name="setup_proxy_auth">Setup Proxy Authorization</string>
<string name="save_proxy_creds">Save Proxy Credentials</string>
<string name="skip_proxy_creds">Skip</string>
<string name="clear_proxy_creds">Clear Credentials</string>
<string name="proxy_creds_saved">Proxy credentials saved</string>
<string name="proxy_creds_cleared">Proxy credentials cleared</string>
<string name="procy_creds_required_msg">Please provide both username and password or leave both empty</string>
</resources>

View File

@@ -1,23 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<changelog>
<release version="11.0.0" versioncode="1100">
<release version="12.0.0-dev" versioncode="1195">
<type name="🎉 Features 🎉">
<change>Custom URL scheme: gitnex:// (details in README.md)</change>
<change>Repository topics</change>
<change>Add or delete repository topics</change>
<change>View global repository settings as an instance admin</change>
<change>Under development</change>
</type>
<type name="🚀 Improvements 🚀">
<change>Added Vue language to file icons</change>
<change>Under development</change>
</type>
<type name="🐛 Bug Fixes 🐛">
<change>Fixed number formatting in activity logs</change>
<change>Fixed translation issue in general settings screen</change>
<change>Fixed crash after swiping the app away</change>
<change>Fixed My Issues filter bug</change>
<change>Potential fix for repeating notifications</change>
<change>Fixed pull request info when opened from a notification</change>
<change>Under development</change>
</type>
</release>