diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/ContentUriRequestBody.java b/mastodon/src/main/java/org/joinmastodon/android/api/ContentUriRequestBody.java index 3cdf9a78..507ad861 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/ContentUriRequestBody.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/ContentUriRequestBody.java @@ -45,10 +45,16 @@ public class ContentUriRequestBody extends RequestBody{ @Override public void writeTo(BufferedSink sink) throws IOException{ - try(Source source=Okio.source(MastodonApp.context.getContentResolver().openInputStream(uri))){ - BufferedSink wrappedSink=Okio.buffer(new CountingSink(sink)); - wrappedSink.writeAll(source); - wrappedSink.flush(); + if(progressListener!=null){ + try(Source source=Okio.source(MastodonApp.context.getContentResolver().openInputStream(uri))){ + BufferedSink wrappedSink=Okio.buffer(new CountingSink(sink)); + wrappedSink.writeAll(source); + wrappedSink.flush(); + } + }else{ + try(Source source=Okio.source(MastodonApp.context.getContentResolver().openInputStream(uri))){ + sink.writeAll(source); + } } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIRequest.java b/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIRequest.java index c3bc2845..55af2088 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIRequest.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIRequest.java @@ -1,5 +1,8 @@ package org.joinmastodon.android.api; +import android.app.Activity; +import android.app.ProgressDialog; +import android.content.DialogInterface; import android.net.Uri; import android.util.Pair; @@ -17,6 +20,7 @@ import java.util.List; import java.util.Map; import androidx.annotation.CallSuper; +import androidx.annotation.StringRes; import me.grishka.appkit.api.APIRequest; import me.grishka.appkit.api.Callback; import okhttp3.Call; @@ -36,6 +40,7 @@ public abstract class MastodonAPIRequest extends APIRequest{ Token token; boolean canceled; Map headers; + private ProgressDialog progressDialog; public MastodonAPIRequest(HttpMethod method, String path, Class respClass){ this.path=path; @@ -82,6 +87,17 @@ public abstract class MastodonAPIRequest extends APIRequest{ return this; } + public MastodonAPIRequest wrapProgress(Activity activity, @StringRes int message, boolean cancelable){ + progressDialog=new ProgressDialog(activity); + progressDialog.setMessage(activity.getString(message)); + progressDialog.setCancelable(cancelable); + if(cancelable){ + progressDialog.setOnCancelListener(dialog->cancel()); + } + progressDialog.show(); + return this; + } + protected void setRequestBody(Object body){ requestBody=body; } @@ -149,10 +165,18 @@ public abstract class MastodonAPIRequest extends APIRequest{ invokeSuccessCallback(resp); } + @Override + protected void onRequestDone(){ + if(progressDialog!=null){ + progressDialog.dismiss(); + } + } + public enum HttpMethod{ GET, POST, PUT, - DELETE + DELETE, + PATCH } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/UpdateAccountCredentials.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/UpdateAccountCredentials.java new file mode 100644 index 00000000..73bb1885 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/UpdateAccountCredentials.java @@ -0,0 +1,55 @@ +package org.joinmastodon.android.api.requests.accounts; + +import android.net.Uri; + +import org.joinmastodon.android.api.ContentUriRequestBody; +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.model.Account; +import org.joinmastodon.android.model.AccountField; +import org.joinmastodon.android.ui.utils.UiUtils; + +import java.util.List; + +import okhttp3.MultipartBody; +import okhttp3.RequestBody; + +public class UpdateAccountCredentials extends MastodonAPIRequest{ + private String displayName, bio; + private Uri avatar, cover; + private List fields; + + public UpdateAccountCredentials(String displayName, String bio, Uri avatar, Uri cover, List fields){ + super(HttpMethod.PATCH, "/accounts/update_credentials", Account.class); + this.displayName=displayName; + this.bio=bio; + this.avatar=avatar; + this.cover=cover; + this.fields=fields; + } + + @Override + public RequestBody getRequestBody(){ + MultipartBody.Builder bldr=new MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("display_name", displayName) + .addFormDataPart("note", bio); + + if(avatar!=null){ + bldr.addFormDataPart("avatar", UiUtils.getFileName(avatar), new ContentUriRequestBody(avatar, null)); + } + if(cover!=null){ + bldr.addFormDataPart("header", UiUtils.getFileName(cover), new ContentUriRequestBody(cover, null)); + } + if(fields.isEmpty()){ + bldr.addFormDataPart("fields_attributes[0][name]", "").addFormDataPart("fields_attributes[0][value]", ""); + }else{ + int i=0; + for(AccountField field:fields){ + bldr.addFormDataPart("fields_attributes["+i+"][name]", field.name).addFormDataPart("fields_attributes["+i+"][value]", field.value); + i++; + } + } + + return bldr.build(); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/UploadAttachment.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/UploadAttachment.java index a534652c..af96bf0d 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/UploadAttachment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/UploadAttachment.java @@ -9,6 +9,7 @@ import org.joinmastodon.android.api.ContentUriRequestBody; import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.api.ProgressListener; import org.joinmastodon.android.model.Attachment; +import org.joinmastodon.android.ui.utils.UiUtils; import okhttp3.MultipartBody; import okhttp3.RequestBody; @@ -29,16 +30,9 @@ public class UploadAttachment extends MastodonAPIRequest{ @Override public RequestBody getRequestBody(){ - String fileName; - try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null)){ - cursor.moveToFirst(); - fileName=cursor.getString(0); - } - if(fileName==null) - fileName=uri.getLastPathSegment(); return new MultipartBody.Builder() .setType(MultipartBody.FORM) - .addFormDataPart("file", fileName, new ContentUriRequestBody(uri, progressListener)) + .addFormDataPart("file", UiUtils.getFileName(uri), new ContentUriRequestBody(uri, progressListener)) .build(); } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java index 3c6ae3f5..172788bf 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java @@ -311,6 +311,13 @@ public class AccountSessionManager{ return r==null ? Collections.emptyList() : r; } + public void updateAccountInfo(String id, Account account){ + AccountSession session=getAccount(id); + session.self=account; + session.infoLastUpdated=System.currentTimeMillis(); + writeAccountsFile(); + } + private static class SessionsStorageWrapper{ public List accounts; } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java index 45447502..0a6fd8fa 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java @@ -24,12 +24,13 @@ import androidx.annotation.Nullable; import me.grishka.appkit.FragmentStackActivity; import me.grishka.appkit.fragments.AppKitFragment; import me.grishka.appkit.fragments.LoaderFragment; +import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.imageloader.ViewImageLoader; import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; import me.grishka.appkit.utils.V; import me.grishka.appkit.views.FragmentRootLinearLayout; -public class HomeFragment extends AppKitFragment{ +public class HomeFragment extends AppKitFragment implements OnBackPressedListener{ private FragmentRootLinearLayout content; private HomeTimelineFragment homeTimelineFragment; private NotificationsFragment notificationsFragment; @@ -155,4 +156,11 @@ public class HomeFragment extends AppKitFragment{ currentTab=tab; ((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this); } + + @Override + public boolean onBackPressed(){ + if(currentTab==R.id.tab_profile) + return profileFragment.onBackPressed(); + return false; + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileAboutFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileAboutFragment.java index b732afc1..c8ca4835 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileAboutFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileAboutFragment.java @@ -1,6 +1,7 @@ package org.joinmastodon.android.fragments; import android.app.Fragment; +import android.content.Context; import android.graphics.Canvas; import android.graphics.Outline; import android.graphics.Paint; @@ -11,10 +12,12 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewOutlineProvider; +import android.widget.EditText; import android.widget.TextView; import org.joinmastodon.android.R; import org.joinmastodon.android.model.AccountField; +import org.joinmastodon.android.ui.utils.SimpleTextWatcher; import org.joinmastodon.android.ui.views.LinkedTextView; import java.util.Collections; @@ -22,21 +25,32 @@ import java.util.List; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import me.grishka.appkit.imageloader.ListImageLoaderWrapper; import me.grishka.appkit.utils.BindableViewHolder; +import me.grishka.appkit.utils.CubicBezierInterpolator; import me.grishka.appkit.utils.V; import me.grishka.appkit.views.UsableRecyclerView; public class ProfileAboutFragment extends Fragment{ + private static final int MAX_FIELDS=4; + public UsableRecyclerView list; private List fields=Collections.emptyList(); private AboutAdapter adapter; private Paint dividerPaint=new Paint(); + private boolean isInEditMode; + private ItemTouchHelper dragHelper=new ItemTouchHelper(new ReorderCallback()); + private RecyclerView.ViewHolder draggedViewHolder; public void setFields(List fields){ this.fields=fields; + if(isInEditMode){ + isInEditMode=false; + dragHelper.attachToRecyclerView(null); + } if(adapter!=null) adapter.notifyDataSetChanged(); } @@ -60,7 +74,8 @@ public class ProfileAboutFragment extends Fragment{ for(int i=0;i{ + public void enterEditMode(List editableFields){ + isInEditMode=true; + fields=editableFields; + adapter.notifyDataSetChanged(); + dragHelper.attachToRecyclerView(list); + } + + public List getFields(){ + return fields; + } + + private class AboutAdapter extends UsableRecyclerView.Adapter{ public AboutAdapter(){ super(null); } @NonNull @Override - public AboutViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ - return new AboutViewHolder(); + public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ + return switch(viewType){ + case 0 -> new AboutViewHolder(); + case 1 -> new EditableAboutViewHolder(); + case 2 -> new AddRowViewHolder(); + default -> throw new IllegalStateException("Unexpected value: "+viewType); + }; } @Override - public void onBindViewHolder(AboutViewHolder holder, int position){ - holder.bind(fields.get(position)); + public void onBindViewHolder(BaseViewHolder holder, int position){ + if(position{ - private TextView title; - private LinkedTextView value; + private abstract class BaseViewHolder extends BindableViewHolder{ private ShapeDrawable background=new ShapeDrawable(); - public AboutViewHolder(){ - super(getActivity(), R.layout.item_profile_about, list); - title=findViewById(R.id.title); - value=findViewById(R.id.value); + public BaseViewHolder(int layout){ + super(getActivity(), layout, list); background.getPaint().setColor(getResources().getColor(R.color.gray_50)); itemView.setBackground(background); } @Override public void onBind(AccountField item){ - title.setText(item.name); - value.setText(item.parsedValue); - boolean first=getAbsoluteAdapterPosition()==0, last=getAbsoluteAdapterPosition()==fields.size()-1; + boolean first=getAbsoluteAdapterPosition()==0, last=getAbsoluteAdapterPosition()==adapter.getItemCount()-1; float radius=V.dp(10); float[] rad=new float[8]; if(first) @@ -120,4 +163,117 @@ public class ProfileAboutFragment extends Fragment{ itemView.invalidateOutline(); } } + + private class AboutViewHolder extends BaseViewHolder{ + private TextView title; + private LinkedTextView value; + + public AboutViewHolder(){ + super(R.layout.item_profile_about); + title=findViewById(R.id.title); + value=findViewById(R.id.value); + } + + @Override + public void onBind(AccountField item){ + super.onBind(item); + title.setText(item.name); + value.setText(item.parsedValue); + } + } + + private class EditableAboutViewHolder extends BaseViewHolder{ + private EditText title; + private EditText value; + + public EditableAboutViewHolder(){ + super(R.layout.item_profile_about_editable); + title=findViewById(R.id.title); + value=findViewById(R.id.value); + findViewById(R.id.dragger_thingy).setOnLongClickListener(v->{ + dragHelper.startDrag(this); + return true; + }); + title.addTextChangedListener(new SimpleTextWatcher(e->item.name=e.toString())); + value.addTextChangedListener(new SimpleTextWatcher(e->item.value=e.toString())); + } + + @Override + public void onBind(AccountField item){ + super.onBind(item); + title.setText(item.name); + value.setText(item.value); + } + } + + private class AddRowViewHolder extends BaseViewHolder implements UsableRecyclerView.Clickable{ + public AddRowViewHolder(){ + super(R.layout.item_profile_about_add_row); + } + + @Override + public void onClick(){ + fields.add(new AccountField()); + if(fields.size()==MAX_FIELDS){ // replace this row with new row + adapter.notifyItemChanged(fields.size()-1); + }else{ + adapter.notifyItemInserted(fields.size()-1); + rebind(); + } + } + } + + private class ReorderCallback extends ItemTouchHelper.SimpleCallback{ + public ReorderCallback(){ + super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0); + } + + @Override + public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target){ + if(target instanceof AddRowViewHolder) + return false; + int fromPosition=viewHolder.getAbsoluteAdapterPosition(); + int toPosition=target.getAbsoluteAdapterPosition(); + if (fromPositiontoPosition;i--) { + Collections.swap(fields, i, i-1); + } + } + adapter.notifyItemMoved(fromPosition, toPosition); + ((BindableViewHolder)viewHolder).rebind(); + ((BindableViewHolder)target).rebind(); + return true; + } + + @Override + public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction){ + + } + + @Override + public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState){ + super.onSelectedChanged(viewHolder, actionState); + if(actionState==ItemTouchHelper.ACTION_STATE_DRAG){ + viewHolder.itemView.setTag(R.id.item_touch_helper_previous_elevation, viewHolder.itemView.getElevation()); // prevents the default behavior of changing elevation in onDraw() + viewHolder.itemView.animate().translationZ(V.dp(1)).setDuration(200).setInterpolator(CubicBezierInterpolator.DEFAULT).start(); + draggedViewHolder=viewHolder; + } + } + + @Override + public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder){ + super.clearView(recyclerView, viewHolder); + viewHolder.itemView.animate().translationZ(0).setDuration(100).setInterpolator(CubicBezierInterpolator.DEFAULT).start(); + draggedViewHolder=null; + } + + @Override + public boolean isLongPressDragEnabled(){ + return false; + } + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java index 724916d1..b8b26134 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java @@ -1,12 +1,18 @@ package org.joinmastodon.android.fragments; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; import android.app.Activity; import android.app.Fragment; import android.content.Intent; import android.content.res.Configuration; import android.graphics.Outline; +import android.graphics.drawable.Drawable; +import android.net.Uri; import android.os.Bundle; -import android.util.Log; +import android.view.Gravity; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -17,14 +23,18 @@ import android.view.ViewOutlineProvider; import android.view.ViewTreeObserver; import android.view.WindowInsets; import android.widget.Button; +import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ImageView; +import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toolbar; import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships; import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses; +import org.joinmastodon.android.api.requests.accounts.GetOwnAccount; +import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials; import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.AccountField; @@ -38,7 +48,6 @@ import org.joinmastodon.android.ui.views.CoverImageView; import org.joinmastodon.android.ui.views.NestedRecyclerScrollView; import org.parceler.Parcels; -import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; @@ -55,11 +64,15 @@ import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.fragments.BaseRecyclerFragment; import me.grishka.appkit.fragments.LoaderFragment; +import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.imageloader.ViewImageLoader; import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; +import me.grishka.appkit.utils.CubicBezierInterpolator; import me.grishka.appkit.utils.V; -public class ProfileFragment extends LoaderFragment{ +public class ProfileFragment extends LoaderFragment implements OnBackPressedListener{ + private static final int AVATAR_RESULT=722; + private static final int COVER_RESULT=343; private ImageView avatar; private CoverImageView cover; @@ -74,6 +87,8 @@ public class ProfileFragment extends LoaderFragment{ private SwipeRefreshLayout refreshLayout; private CoverOverlayGradientDrawable coverGradient=new CoverOverlayGradientDrawable(); private float titleTransY; + private View postsBtn, followersBtn, followingBtn; + private EditText nameEdit, bioEdit; private Account account; private String accountID; @@ -82,6 +97,9 @@ public class ProfileFragment extends LoaderFragment{ private boolean isOwnProfile; private ArrayList fields=new ArrayList<>(); + private boolean isInEditMode; + private Uri editNewAvatar, editNewCover; + public ProfileFragment(){ super(R.layout.loader_fragment_overlay_toolbar); } @@ -107,15 +125,20 @@ public class ProfileFragment extends LoaderFragment{ bio=content.findViewById(R.id.bio); followersCount=content.findViewById(R.id.followers_count); followersLabel=content.findViewById(R.id.followers_label); + followersBtn=content.findViewById(R.id.followers_btn); followingCount=content.findViewById(R.id.following_count); followingLabel=content.findViewById(R.id.following_label); + followingBtn=content.findViewById(R.id.following_btn); postsCount=content.findViewById(R.id.posts_count); postsLabel=content.findViewById(R.id.posts_label); + postsBtn=content.findViewById(R.id.posts_btn); actionButton=content.findViewById(R.id.profile_action_btn); pager=content.findViewById(R.id.pager); scrollView=content.findViewById(R.id.scroller); tabbar=content.findViewById(R.id.tabbar); refreshLayout=content.findViewById(R.id.refresh_layout); + nameEdit=content.findViewById(R.id.name_edit); + bioEdit=content.findViewById(R.id.bio_edit); avatar.setOutlineProvider(new ViewOutlineProvider(){ @Override @@ -174,6 +197,10 @@ public class ProfileFragment extends LoaderFragment{ } }); + actionButton.setOnClickListener(this::onActionButtonClick); + avatar.setOnClickListener(this::onAvatarClick); + cover.setOnClickListener(this::onCoverClick); + return sizeWrapper; } @@ -287,6 +314,19 @@ public class ProfileFragment extends LoaderFragment{ @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){ + if(isOwnProfile && isInEditMode){ + Button cancelButton=new Button(getActivity(), null, 0, R.style.Widget_Mastodon_Button_Secondary); + cancelButton.setText(R.string.cancel); + cancelButton.setOnClickListener(v->exitEditMode()); + FrameLayout wrap=new FrameLayout(getActivity()); + wrap.addView(cancelButton, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.TOP|Gravity.LEFT)); + wrap.setPadding(V.dp(16), V.dp(4), V.dp(16), V.dp(8)); + wrap.setClipToPadding(false); + MenuItem item=menu.add(R.string.cancel); + item.setActionView(wrap); + item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + return; + } if(relationship==null) return; inflater.inflate(R.menu.profile, menu); @@ -383,6 +423,179 @@ public class ProfileFragment extends LoaderFragment{ return getFragmentForPage(pager.getCurrentItem()).getView().findViewById(R.id.list); } + private void onActionButtonClick(View v){ + if(isOwnProfile){ + if(!isInEditMode) + loadAccountInfoAndEnterEditMode(); + else + saveAndExitEditMode(); + } + } + + private void loadAccountInfoAndEnterEditMode(){ + new GetOwnAccount() + .setCallback(new Callback<>(){ + @Override + public void onSuccess(Account result){ + enterEditMode(result); + } + + @Override + public void onError(ErrorResponse error){ + error.showToast(getActivity()); + } + }) + .wrapProgress(getActivity(), R.string.loading, true) + .exec(accountID); + } + + private void enterEditMode(Account account){ + if(isInEditMode) + throw new IllegalStateException(); + isInEditMode=true; + invalidateOptionsMenu(); + pager.setUserInputEnabled(false); + actionButton.setText(R.string.done); + pager.setCurrentItem(3); + ArrayList animators=new ArrayList<>(); + for(int i=0;i<3;i++){ + animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, .3f)); + tabbar.getTabAt(i).view.setEnabled(false); + } + Drawable overlay=getResources().getDrawable(R.drawable.edit_avatar_overlay).mutate(); + avatar.setForeground(overlay); + animators.add(ObjectAnimator.ofInt(overlay, "alpha", 0, 255)); + + nameEdit.setVisibility(View.VISIBLE); + nameEdit.setText(account.displayName); + RelativeLayout.LayoutParams lp=(RelativeLayout.LayoutParams) username.getLayoutParams(); + lp.addRule(RelativeLayout.BELOW, R.id.name_edit); + username.getParent().requestLayout(); + animators.add(ObjectAnimator.ofFloat(nameEdit, View.ALPHA, 0f, 1f)); + + bioEdit.setVisibility(View.VISIBLE); + bioEdit.setText(account.source.note); + animators.add(ObjectAnimator.ofFloat(bioEdit, View.ALPHA, 0f, 1f)); + animators.add(ObjectAnimator.ofFloat(bio, View.ALPHA, 0f)); + + animators.add(ObjectAnimator.ofFloat(postsBtn, View.ALPHA, .3f)); + animators.add(ObjectAnimator.ofFloat(followersBtn, View.ALPHA, .3f)); + animators.add(ObjectAnimator.ofFloat(followingBtn, View.ALPHA, .3f)); + + AnimatorSet set=new AnimatorSet(); + set.playTogether(animators); + set.setDuration(300); + set.setInterpolator(CubicBezierInterpolator.DEFAULT); + set.start(); + + aboutFragment.enterEditMode(account.source.fields); + } + + private void exitEditMode(){ + if(!isInEditMode) + throw new IllegalStateException(); + isInEditMode=false; + + invalidateOptionsMenu(); + ArrayList animators=new ArrayList<>(); + actionButton.setText(R.string.edit_profile); + for(int i=0;i<3;i++){ + animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, 1f)); + } + animators.add(ObjectAnimator.ofInt(avatar.getForeground(), "alpha", 0)); + animators.add(ObjectAnimator.ofFloat(nameEdit, View.ALPHA, 0f)); + animators.add(ObjectAnimator.ofFloat(bioEdit, View.ALPHA, 0f)); + animators.add(ObjectAnimator.ofFloat(bio, View.ALPHA, 1f)); + animators.add(ObjectAnimator.ofFloat(postsBtn, View.ALPHA, 1f)); + animators.add(ObjectAnimator.ofFloat(followersBtn, View.ALPHA, 1f)); + animators.add(ObjectAnimator.ofFloat(followingBtn, View.ALPHA, 1f)); + + AnimatorSet set=new AnimatorSet(); + set.playTogether(animators); + set.setDuration(200); + set.setInterpolator(CubicBezierInterpolator.DEFAULT); + set.addListener(new AnimatorListenerAdapter(){ + @Override + public void onAnimationEnd(Animator animation){ + for(int i=0;i<3;i++){ + tabbar.getTabAt(i).view.setEnabled(true); + } + pager.setUserInputEnabled(true); + nameEdit.setVisibility(View.GONE); + bioEdit.setVisibility(View.GONE); + RelativeLayout.LayoutParams lp=(RelativeLayout.LayoutParams) username.getLayoutParams(); + lp.addRule(RelativeLayout.BELOW, R.id.name); + username.getParent().requestLayout(); + avatar.setForeground(null); + } + }); + set.start(); + + bindHeaderView(); + } + + private void saveAndExitEditMode(){ + if(!isInEditMode) + throw new IllegalStateException(); + new UpdateAccountCredentials(nameEdit.getText().toString(), bioEdit.getText().toString(), editNewAvatar, editNewCover, aboutFragment.getFields()) + .setCallback(new Callback<>(){ + @Override + public void onSuccess(Account result){ + account=result; + AccountSessionManager.getInstance().updateAccountInfo(accountID, account); + exitEditMode(); + } + + @Override + public void onError(ErrorResponse error){ + error.showToast(getActivity()); + } + }) + .wrapProgress(getActivity(), R.string.saving, false) + .exec(accountID); + } + + @Override + public boolean onBackPressed(){ + if(isInEditMode){ + exitEditMode(); + return true; + } + return false; + } + + private void onAvatarClick(View v){ + if(isInEditMode){ + startImagePicker(AVATAR_RESULT); + } + } + + private void onCoverClick(View v){ + if(isInEditMode){ + startImagePicker(COVER_RESULT); + } + } + + private void startImagePicker(int requestCode){ + Intent intent=new Intent(Intent.ACTION_GET_CONTENT); + intent.setType("image/*"); + intent.addCategory(Intent.CATEGORY_OPENABLE); + startActivityForResult(intent, requestCode); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data){ + if(resultCode==Activity.RESULT_OK){ + if(requestCode==AVATAR_RESULT){ + editNewAvatar=data.getData(); + ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(editNewAvatar, V.dp(100), V.dp(100))); + }else if(requestCode==COVER_RESULT){ + editNewCover=data.getData(); + ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(editNewCover, V.dp(1000), V.dp(1000))); + } + } + } + private class ProfilePagerAdapter extends RecyclerView.Adapter{ @NonNull @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/SimpleTextWatcher.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/SimpleTextWatcher.java new file mode 100644 index 00000000..b30988d2 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/SimpleTextWatcher.java @@ -0,0 +1,31 @@ +package org.joinmastodon.android.ui.utils; + +import android.text.Editable; +import android.text.TextWatcher; + +import java.util.function.Consumer; + +import androidx.annotation.NonNull; + +public class SimpleTextWatcher implements TextWatcher{ + private final Consumer delegate; + + public SimpleTextWatcher(@NonNull Consumer delegate){ + this.delegate=delegate; + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after){ + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count){ + + } + + @Override + public void afterTextChanged(Editable s){ + delegate.accept(s); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java index e075b06c..aead0945 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java @@ -3,13 +3,16 @@ package org.joinmastodon.android.ui.utils; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.ColorStateList; +import android.database.Cursor; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Handler; import android.os.Looper; +import android.provider.OpenableColumns; import android.util.Log; import android.widget.TextView; +import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.R; import java.time.Instant; @@ -80,4 +83,14 @@ public class UiUtils{ public static int lerp(int startValue, int endValue, float fraction) { return startValue + Math.round(fraction * (endValue - startValue)); } + + public static String getFileName(Uri uri){ + try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null)){ + cursor.moveToFirst(); + String name=cursor.getString(0); + if(name!=null) + return name; + } + return uri.getLastPathSegment(); + } } diff --git a/mastodon/src/main/res/drawable/bg_button_secondary.xml b/mastodon/src/main/res/drawable/bg_button_secondary.xml new file mode 100644 index 00000000..32caacae --- /dev/null +++ b/mastodon/src/main/res/drawable/bg_button_secondary.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/bg_profile_field_edit_text.xml b/mastodon/src/main/res/drawable/bg_profile_field_edit_text.xml new file mode 100644 index 00000000..cc86207e --- /dev/null +++ b/mastodon/src/main/res/drawable/bg_profile_field_edit_text.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/edit_avatar_overlay.xml b/mastodon/src/main/res/drawable/edit_avatar_overlay.xml new file mode 100644 index 00000000..5ce6af70 --- /dev/null +++ b/mastodon/src/main/res/drawable/edit_avatar_overlay.xml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/edit_text_border.xml b/mastodon/src/main/res/drawable/edit_text_border.xml new file mode 100644 index 00000000..9d16da94 --- /dev/null +++ b/mastodon/src/main/res/drawable/edit_text_border.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/ic_fluent_add_circle_24_regular.xml b/mastodon/src/main/res/drawable/ic_fluent_add_circle_24_regular.xml new file mode 100644 index 00000000..1050d935 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_add_circle_24_regular.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_fluent_image_edit_24_regular.xml b/mastodon/src/main/res/drawable/ic_fluent_image_edit_24_regular.xml new file mode 100644 index 00000000..9be92cb2 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_image_edit_24_regular.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_fluent_re_order_dots_vertical_24_regular.xml b/mastodon/src/main/res/drawable/ic_fluent_re_order_dots_vertical_24_regular.xml new file mode 100644 index 00000000..97296bad --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_re_order_dots_vertical_24_regular.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/layout/fragment_profile.xml b/mastodon/src/main/res/layout/fragment_profile.xml index 32117c49..155a8a3a 100644 --- a/mastodon/src/main/res/layout/fragment_profile.xml +++ b/mastodon/src/main/res/layout/fragment_profile.xml @@ -45,6 +45,7 @@ android:layout_alignParentStart="true" android:layout_marginStart="16dp" android:layout_marginTop="-38dp" + android:scaleType="centerCrop" tools:src="#0f0" /> + android:padding="4dp"> + + tools:text="123" /> + + tools:text="following" />