Update user avatar (#1349)

Closes #552

Reviewed-on: https://codeberg.org/gitnex/GitNex/pulls/1349
Co-authored-by: M M Arif <mmarif@swatian.com>
Co-committed-by: M M Arif <mmarif@swatian.com>
This commit is contained in:
M M Arif 2024-04-07 10:15:22 +00:00 committed by M M Arif
parent 7c435e4f34
commit 254779a324
10 changed files with 233 additions and 50 deletions

View File

@ -8,8 +8,8 @@ android {
applicationId "org.mian.gitnex" applicationId "org.mian.gitnex"
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 34 targetSdkVersion 34
versionCode 540 versionCode 545
versionName "5.4.0" versionName "5.5.0-dev"
multiDexEnabled true multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
compileSdk 34 compileSdk 34
@ -71,9 +71,9 @@ dependencies {
implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.google.code.gson:gson:2.10.1'
implementation "com.squareup.picasso:picasso:2.71828" implementation "com.squareup.picasso:picasso:2.71828"
implementation 'com.github.ramseth001:TextDrawable:1.1.3' implementation 'com.github.ramseth001:TextDrawable:1.1.3'
implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:retrofit:2.11.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.11.0'
implementation 'com.squareup.retrofit2:converter-scalars:2.9.0' implementation 'com.squareup.retrofit2:converter-scalars:2.11.0'
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.12' implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.12'
implementation 'org.ocpsoft.prettytime:prettytime:5.0.7.Final' implementation 'org.ocpsoft.prettytime:prettytime:5.0.7.Final'
implementation "com.github.skydoves:colorpickerview:2.3.0" implementation "com.github.skydoves:colorpickerview:2.3.0"

View File

@ -54,6 +54,7 @@ import org.mian.gitnex.fragments.RepositoriesFragment;
import org.mian.gitnex.fragments.SettingsFragment; import org.mian.gitnex.fragments.SettingsFragment;
import org.mian.gitnex.fragments.StarredRepositoriesFragment; import org.mian.gitnex.fragments.StarredRepositoriesFragment;
import org.mian.gitnex.fragments.WatchedRepositoriesFragment; import org.mian.gitnex.fragments.WatchedRepositoriesFragment;
import org.mian.gitnex.fragments.profile.DetailFragment;
import org.mian.gitnex.helpers.AlertDialogs; import org.mian.gitnex.helpers.AlertDialogs;
import org.mian.gitnex.helpers.AppDatabaseSettings; import org.mian.gitnex.helpers.AppDatabaseSettings;
import org.mian.gitnex.helpers.AppUtil; import org.mian.gitnex.helpers.AppUtil;
@ -298,10 +299,6 @@ public class MainActivity extends BaseActivity
.setVisible(false); .setVisible(false);
} }
if (getAccount().requiresVersion("1.14.0")) {
navigationView.getMenu().findItem(R.id.nav_my_issues).setVisible(true);
}
if (getAccount().requiresVersion("1.20.0")) { if (getAccount().requiresVersion("1.20.0")) {
navigationView.getMenu().findItem(R.id.nav_dashboard).setVisible(true); navigationView.getMenu().findItem(R.id.nav_dashboard).setVisible(true);
} }
@ -586,6 +583,10 @@ public class MainActivity extends BaseActivity
this.overridePendingTransition(0, 0); this.overridePendingTransition(0, 0);
refActivity = false; refActivity = false;
} }
if (DetailFragment.refProfile) {
loadUserInfo();
DetailFragment.refProfile = false;
}
} }
public void setActionBarTitle(String title) { public void setActionBarTitle(String title) {

View File

@ -71,7 +71,7 @@ public class AttachmentsAdapter extends RecyclerView.Adapter<AttachmentsAdapter.
} }
} }
class ViewHolder extends RecyclerView.ViewHolder { public class ViewHolder extends RecyclerView.ViewHolder {
public TextView filename; public TextView filename;
public ImageView delete; public ImageView delete;

View File

@ -34,7 +34,6 @@ import org.mian.gitnex.helpers.ColorInverter;
import org.mian.gitnex.helpers.LabelWidthCalculator; import org.mian.gitnex.helpers.LabelWidthCalculator;
import org.mian.gitnex.helpers.RoundedTransformation; import org.mian.gitnex.helpers.RoundedTransformation;
import org.mian.gitnex.helpers.TimeHelper; import org.mian.gitnex.helpers.TimeHelper;
import org.mian.gitnex.helpers.TinyDB;
import org.mian.gitnex.helpers.contexts.IssueContext; import org.mian.gitnex.helpers.contexts.IssueContext;
import org.mian.gitnex.helpers.contexts.RepositoryContext; import org.mian.gitnex.helpers.contexts.RepositoryContext;
@ -44,7 +43,6 @@ import org.mian.gitnex.helpers.contexts.RepositoryContext;
public class ExploreIssuesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { public class ExploreIssuesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private final Context context; private final Context context;
private final TinyDB tinyDb;
private List<Issue> searchedList; private List<Issue> searchedList;
private OnLoadMoreListener loadMoreListener; private OnLoadMoreListener loadMoreListener;
private boolean isLoading = false, isMoreDataAvailable = true; private boolean isLoading = false, isMoreDataAvailable = true;
@ -52,7 +50,6 @@ public class ExploreIssuesAdapter extends RecyclerView.Adapter<RecyclerView.View
public ExploreIssuesAdapter(List<Issue> dataList, Context ctx) { public ExploreIssuesAdapter(List<Issue> dataList, Context ctx) {
this.context = ctx; this.context = ctx;
this.searchedList = dataList; this.searchedList = dataList;
this.tinyDb = TinyDB.getInstance(context);
} }
@NonNull @Override @NonNull @Override

View File

@ -1,18 +1,28 @@
package org.mian.gitnex.fragments.profile; package org.mian.gitnex.fragments.profile;
import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.util.Locale; import java.util.Locale;
import okhttp3.ResponseBody; import okhttp3.ResponseBody;
import org.gitnex.tea4j.v2.models.Repository; import org.gitnex.tea4j.v2.models.Repository;
import org.gitnex.tea4j.v2.models.UpdateUserAvatarOption;
import org.gitnex.tea4j.v2.models.User; import org.gitnex.tea4j.v2.models.User;
import org.gitnex.tea4j.v2.models.UserSettings; import org.gitnex.tea4j.v2.models.UserSettings;
import org.gitnex.tea4j.v2.models.UserSettingsOptions; import org.gitnex.tea4j.v2.models.UserSettingsOptions;
@ -21,6 +31,7 @@ import org.mian.gitnex.activities.BaseActivity;
import org.mian.gitnex.activities.ProfileActivity; import org.mian.gitnex.activities.ProfileActivity;
import org.mian.gitnex.clients.PicassoService; import org.mian.gitnex.clients.PicassoService;
import org.mian.gitnex.clients.RetrofitClient; import org.mian.gitnex.clients.RetrofitClient;
import org.mian.gitnex.databinding.CustomEditAvatarDialogBinding;
import org.mian.gitnex.databinding.CustomEditProfileBinding; import org.mian.gitnex.databinding.CustomEditProfileBinding;
import org.mian.gitnex.databinding.FragmentProfileDetailBinding; import org.mian.gitnex.databinding.FragmentProfileDetailBinding;
import org.mian.gitnex.helpers.AlertDialogs; import org.mian.gitnex.helpers.AlertDialogs;
@ -29,7 +40,6 @@ import org.mian.gitnex.helpers.ClickListener;
import org.mian.gitnex.helpers.Markdown; import org.mian.gitnex.helpers.Markdown;
import org.mian.gitnex.helpers.RoundedTransformation; import org.mian.gitnex.helpers.RoundedTransformation;
import org.mian.gitnex.helpers.TimeHelper; import org.mian.gitnex.helpers.TimeHelper;
import org.mian.gitnex.helpers.TinyDB;
import org.mian.gitnex.helpers.Toasty; import org.mian.gitnex.helpers.Toasty;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.Callback; import retrofit2.Callback;
@ -41,13 +51,17 @@ public class DetailFragment extends Fragment {
private static final String usernameBundle = ""; private static final String usernameBundle = "";
Locale locale; Locale locale;
TinyDB tinyDb;
private Context context; private Context context;
private FragmentProfileDetailBinding binding; private FragmentProfileDetailBinding binding;
private String username; private String username;
private CustomEditProfileBinding customEditProfileBinding; private CustomEditProfileBinding customEditProfileBinding;
private CustomEditAvatarDialogBinding customEditAvatarDialogBinding;
private MaterialAlertDialogBuilder materialAlertDialogBuilder; private MaterialAlertDialogBuilder materialAlertDialogBuilder;
private AlertDialog dialogEditSettings; private AlertDialog dialogEditSettings;
private AlertDialog dialogEditAvatar;
private int imgRadius;
private static Uri avatarUri = null;
public static boolean refProfile = false;
public DetailFragment() {} public DetailFragment() {}
@ -73,8 +87,8 @@ public class DetailFragment extends Fragment {
binding = FragmentProfileDetailBinding.inflate(inflater, container, false); binding = FragmentProfileDetailBinding.inflate(inflater, container, false);
context = getContext(); context = getContext();
tinyDb = TinyDB.getInstance(context);
locale = getResources().getConfiguration().locale; locale = getResources().getConfiguration().locale;
imgRadius = AppUtil.getPixelsFromDensity(context, 3);
getProfileDetail(username); getProfileDetail(username);
getProfileRepository(username); getProfileRepository(username);
@ -94,21 +108,121 @@ public class DetailFragment extends Fragment {
((ProfileActivity) requireActivity()).viewPager.setCurrentItem(2)); ((ProfileActivity) requireActivity()).viewPager.setCurrentItem(2));
if (username.equals(((BaseActivity) context).getAccount().getAccount().getUserName())) { if (username.equals(((BaseActivity) context).getAccount().getAccount().getUserName())) {
binding.editProfile.setVisibility(View.VISIBLE); binding.metaProfile.setVisibility(View.VISIBLE);
} else { } else {
binding.editProfile.setVisibility(View.GONE); binding.metaProfile.setVisibility(View.GONE);
} }
binding.editProfile.setOnClickListener( binding.updateProfile.setOnClickListener(
editProfileSettings -> { editProfileSettings -> {
customEditProfileBinding = customEditProfileBinding =
CustomEditProfileBinding.inflate(LayoutInflater.from(context)); CustomEditProfileBinding.inflate(LayoutInflater.from(context));
showEditProfileDialog(); showEditProfileDialog();
}); });
binding.updateAvatar.setOnClickListener(updateAvatar -> openFileAttachment());
return binding.getRoot(); return binding.getRoot();
} }
public void onDestroy() {
avatarUri = null;
super.onDestroy();
}
ActivityResultLauncher<Intent> activityForAvatarUpdate =
registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK) {
Intent data = result.getData();
assert data != null;
avatarUri = data.getData();
if (avatarUri != null) {
customEditAvatarDialogBinding =
CustomEditAvatarDialogBinding.inflate(
LayoutInflater.from(context));
showUpdateAvatarDialog();
}
}
});
private void openFileAttachment() {
String[] mimeTypes = {"image/webp", "image/gif", "image/jpg", "image/jpeg", "image/png"};
Intent data = new Intent(Intent.ACTION_GET_CONTENT);
data.addCategory(Intent.CATEGORY_OPENABLE);
data.setType("image/*");
data.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
Intent intent = Intent.createChooser(data, "Choose an image");
activityForAvatarUpdate.launch(intent);
}
private void showUpdateAvatarDialog() {
View view = customEditAvatarDialogBinding.getRoot();
materialAlertDialogBuilder.setView(view);
PicassoService.getInstance(context)
.get()
.load(avatarUri)
.transform(new RoundedTransformation(imgRadius, 0))
.placeholder(R.drawable.loader_animated)
.resize(180, 180)
.centerCrop()
.into(customEditAvatarDialogBinding.userAvatar);
customEditAvatarDialogBinding.save.setOnClickListener(
saveUserAvatar -> saveUserAvatar(avatarUri));
dialogEditAvatar = materialAlertDialogBuilder.show();
}
private void saveUserAvatar(Uri avatar) {
InputStream imageStream = null;
try {
imageStream = context.getContentResolver().openInputStream(avatar);
} catch (FileNotFoundException e) {
e.getMessage();
}
Bitmap selectedImage = BitmapFactory.decodeStream(imageStream);
String encodedString = AppUtil.imageEncodeToBase64(selectedImage);
UpdateUserAvatarOption updateUserAvatarOption = new UpdateUserAvatarOption();
updateUserAvatarOption.setImage(encodedString);
Call<Void> saveUserAvatar =
RetrofitClient.getApiInterface(context).userUpdateAvatar(updateUserAvatarOption);
saveUserAvatar.enqueue(
new Callback<>() {
@Override
public void onResponse(
@NonNull Call<Void> call, @NonNull retrofit2.Response<Void> response) {
if (response.code() == 204) {
dialogEditAvatar.dismiss();
getProfileDetail(username);
Toasty.success(context, getString(R.string.profileUpdate));
refProfile = true;
} else {
Toasty.error(context, getString(R.string.genericError));
}
}
@Override
public void onFailure(@NonNull Call<Void> call, @NonNull Throwable t) {
Toasty.error(context, getString(R.string.genericServerResponseError));
}
});
}
private void showEditProfileDialog() { private void showEditProfileDialog() {
View view = customEditProfileBinding.getRoot(); View view = customEditProfileBinding.getRoot();
@ -160,7 +274,8 @@ public class DetailFragment extends Fragment {
dialogEditSettings.dismiss(); dialogEditSettings.dismiss();
getProfileDetail(username); getProfileDetail(username);
Toasty.success(context, getString(R.string.settingsSave)); Toasty.success(context, getString(R.string.profileUpdate));
refProfile = true;
} else { } else {
Toasty.error(context, getString(R.string.genericError)); Toasty.error(context, getString(R.string.genericError));
@ -246,8 +361,6 @@ public class DetailFragment extends Fragment {
? response.body().getEmail() ? response.body().getEmail()
: ""; : "";
int imgRadius = AppUtil.getPixelsFromDensity(context, 3);
binding.userFullName.setText(username); binding.userFullName.setText(username);
binding.userLogin.setText( binding.userLogin.setText(
getString( getString(

View File

@ -12,6 +12,7 @@ import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo; import android.content.pm.ResolveInfo;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Typeface; import android.graphics.Typeface;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
@ -24,6 +25,7 @@ import androidx.annotation.ColorInt;
import androidx.browser.customtabs.CustomTabColorSchemeParams; import androidx.browser.customtabs.CustomTabColorSchemeParams;
import androidx.browser.customtabs.CustomTabsIntent; import androidx.browser.customtabs.CustomTabsIntent;
import androidx.core.content.pm.PackageInfoCompat; import androidx.core.content.pm.PackageInfoCompat;
import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
@ -413,6 +415,15 @@ public class AppUtil {
return base64Str; return base64Str;
} }
public static String imageEncodeToBase64(Bitmap image) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
image.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream);
byte[] bytes = byteArrayOutputStream.toByteArray();
return Base64.encodeToString(bytes, Base64.DEFAULT);
}
public static String decodeBase64(String str) { public static String decodeBase64(String str) {
String base64Str = str; String base64Str = str;

View File

@ -0,0 +1,59 @@
<?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/dimen8dp">
<androidx.core.widget.NestedScrollView
android:id="@+id/mainView"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/dimen16dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical">
<com.google.android.material.card.MaterialCardView
android:layout_width="@dimen/dimen140dp"
android:layout_height="@dimen/dimen140dp"
style="?attr/materialCardViewFilledStyle"
android:layout_marginBottom="@dimen/dimen8dp"
app:cardElevation="@dimen/dimen0dp"
app:cardCornerRadius="@dimen/dimen48dp">
<ImageView
android:id="@+id/userAvatar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/generalImgContentText"
android:src="@mipmap/app_logo_round" />
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/save"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen54dp"
android:layout_marginTop="@dimen/dimen16dp"
android:text="@string/saveButton"
android:textColor="?attr/materialCardBackgroundColor"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View File

@ -91,17 +91,35 @@
android:textIsSelectable="true" android:textIsSelectable="true"
android:textSize="@dimen/dimen14sp" /> android:textSize="@dimen/dimen14sp" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/editProfile" android:id="@+id/metaProfile"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/editSettings" android:visibility="gone">
android:textColor="?attr/materialCardBackgroundColor"
android:backgroundTint="?attr/fabColor" <Button
app:iconTint="?attr/materialCardBackgroundColor" android:id="@+id/update_avatar"
app:icon="@drawable/ic_edit" style="?attr/materialButtonToggleGroupStyle"
android:textSize="14sp" android:layout_width="wrap_content"
android:visibility="gone" /> android:layout_height="wrap_content"
app:iconTint="?attr/materialCardBackgroundColor"
app:icon="@drawable/ic_person"
android:textColor="?attr/materialCardBackgroundColor"
android:textSize="@dimen/dimen14sp"
android:text="@string/userAvatar" />
<Button
android:id="@+id/update_profile"
style="?attr/materialButtonToggleGroupStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:iconTint="?attr/materialCardBackgroundColor"
app:icon="@drawable/ic_edit"
android:textColor="?attr/materialCardBackgroundColor"
android:textSize="@dimen/dimen14sp"
android:text="@string/navProfile" />
</com.google.android.material.button.MaterialButtonToggleGroup>
</LinearLayout> </LinearLayout>

View File

@ -360,6 +360,7 @@
<string name="editSettings">Edit Profile</string> <string name="editSettings">Edit Profile</string>
<string name="hideActivity">Hide Activity from profile page</string> <string name="hideActivity">Hide Activity from profile page</string>
<string name="hideEmail">Hide Email</string> <string name="hideEmail">Hide Email</string>
<string name="profileUpdate">Profile updated</string>
<!-- profile section --> <!-- profile section -->
<!-- account settings --> <!-- account settings -->

View File

@ -1,25 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<changelog> <changelog>
<release version="5.4.0" versioncode="540"> <release version="5.5.0-dev" versioncode="545">
<change>New: Restore/import app data</change> <change>Under development</change>
<change>New: Backup/export app data</change>
<change>New: Delete email from account</change>
<change>New: Update profile settings</change>
<change>New: Add SSH key to account</change>
<change>Improvement: Open specific URL in deep links</change>
<change>Improvement: Show open in browser with other buttons in deep links</change>
<change>Improvement: Show pinned messages in timeline</change>
<change>Improvement: Hide Comment button if issue is locked</change>
<change>Improvement: New languages and translation updates</change>
<change>Bugfix: Fix crash on dynamic snackbar colors for older Android versions</change>
<change>Bugfix: Fix missing text after links in issue/pr</change>
<change>Bugfix: Fix emoji parsing in code blocks in issue/pr</change>
<change>Bugfix: Fix crash on null objects in timeline from API</change>
<change>Bugfix: Fix not seeing labels for repo admin when code option is disabled</change>
<change>Bugfix: Fix Task list in Markdown</change>
<change>Bugfix: Fix other issues related to Markdown</change>
<change>Bugfix: Fix crash on images with no path</change>
</release> </release>
</changelog> </changelog>