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 2f69fb24e..55937183a 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java @@ -1,26 +1,21 @@ package org.joinmastodon.android.fragments; -import static android.content.Context.CLIPBOARD_SERVICE; - 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.ClipData; -import android.content.ClipboardManager; -import android.content.Context; import android.content.Intent; import android.content.res.Configuration; import android.graphics.Outline; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.RoundRectShape; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.os.VibrationEffect; -import android.os.Vibrator; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.style.ImageSpan; @@ -38,11 +33,9 @@ import android.widget.Button; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.RelativeLayout; import android.widget.TextView; -import android.widget.Toast; import android.widget.Toolbar; import org.joinmastodon.android.GlobalUserPreferences; @@ -61,6 +54,7 @@ import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.AccountField; import org.joinmastodon.android.model.Attachment; import org.joinmastodon.android.model.Relationship; +import org.joinmastodon.android.ui.BetterItemAnimator; import org.joinmastodon.android.ui.SimpleViewHolder; import org.joinmastodon.android.ui.SingleImagePhotoViewerListener; import org.joinmastodon.android.ui.drawables.CoverOverlayGradientDrawable; @@ -69,8 +63,10 @@ import org.joinmastodon.android.ui.tabs.TabLayout; import org.joinmastodon.android.ui.tabs.TabLayoutMediator; import org.joinmastodon.android.ui.text.CustomEmojiSpan; import org.joinmastodon.android.ui.text.HtmlParser; +import org.joinmastodon.android.ui.utils.SimpleTextWatcher; import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.views.CoverImageView; +import org.joinmastodon.android.ui.views.LinkedTextView; import org.joinmastodon.android.ui.views.NestedRecyclerScrollView; import org.joinmastodon.android.ui.views.ProgressBarButton; import org.parceler.Parcels; @@ -84,6 +80,9 @@ import java.util.Collections; 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 androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import androidx.viewpager2.widget.ViewPager2; @@ -94,10 +93,17 @@ import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.fragments.BaseRecyclerFragment; import me.grishka.appkit.fragments.LoaderFragment; import me.grishka.appkit.fragments.OnBackPressedListener; +import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter; +import me.grishka.appkit.imageloader.ImageLoaderViewHolder; +import me.grishka.appkit.imageloader.ListImageLoaderWrapper; +import me.grishka.appkit.imageloader.RecyclerViewDelegate; import me.grishka.appkit.imageloader.ViewImageLoader; +import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; +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 ProfileFragment extends LoaderFragment implements OnBackPressedListener, ScrollableToTop{ private static final int AVATAR_RESULT=722; @@ -139,6 +145,16 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList private PhotoViewer currentPhotoViewer; private boolean editModeLoading; + private static final int MAX_FIELDS=4; + + // from ProfileAboutFragment + public UsableRecyclerView list; + private List metadataListData=Collections.emptyList(); + private MetadataAdapter adapter; + private ItemTouchHelper dragHelper=new ItemTouchHelper(new ReorderCallback()); + private RecyclerView.ViewHolder draggedViewHolder; + private ListImageLoaderWrapper imgLoader; + public ProfileFragment(){ super(R.layout.loader_fragment_overlay_toolbar); } @@ -206,6 +222,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList notifyProgress=content.findViewById(R.id.notify_progress); fab=content.findViewById(R.id.fab); followsYouView=content.findViewById(R.id.follows_you); + list=content.findViewById(R.id.metadata); avatar.setOutlineProvider(new ViewOutlineProvider(){ @Override @@ -303,6 +320,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList return true; }); + // from ProfileAboutFragment + list.setItemAnimator(new BetterItemAnimator()); + list.setDrawSelectorOnTop(true); + list.setLayoutManager(new LinearLayoutManager(getActivity())); + imgLoader=new ListImageLoaderWrapper(getActivity(), list, new RecyclerViewDelegate(list), null); + list.setAdapter(adapter=new MetadataAdapter()); + list.setClipToPadding(false); + return sizeWrapper; } @@ -359,7 +384,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList pinnedPostsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.PINNED, false); mediaFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.MEDIA, false); aboutFragment=new ProfileAboutFragment(); - aboutFragment.setFields(fields); + setFields(fields); } pager.getAdapter().notifyDataSetChanged(); super.dataLoaded(); @@ -519,9 +544,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList fields.add(field); } - if(aboutFragment!=null){ - aboutFragment.setFields(fields); - } + setFields(fields); } private void updateToolbar(){ @@ -829,6 +852,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList set.start(); aboutFragment.enterEditMode(account.source.fields); + + metadataListData=account.source.fields; + adapter.notifyDataSetChanged(); + dragHelper.attachToRecyclerView(list); } private void exitEditMode(){ @@ -1049,4 +1076,228 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList return position; } } + + // from ProfileAboutFragment + public void setFields(ArrayList fields){ + metadataListData=fields; + if (isInEditMode) { + isInEditMode=false; + dragHelper.attachToRecyclerView(null); + } + if (adapter != null) adapter.notifyDataSetChanged(); + if (aboutFragment != null) aboutFragment.setFields(fields); + } + + private class MetadataAdapter extends UsableRecyclerView.Adapter implements ImageLoaderRecyclerAdapter { + public MetadataAdapter(){ + super(imgLoader); + } + + @NonNull + @Override + 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(BaseViewHolder holder, int position){ + if(position { + public BaseViewHolder(int layout){ + super(getActivity(), layout, list); + } + } + + private class AboutViewHolder extends BaseViewHolder implements ImageLoaderViewHolder { + 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){ + title.setText(item.parsedName); + value.setText(item.parsedValue); + if(item.verifiedAt!=null){ + int textColor=UiUtils.isDarkTheme() ? 0xFF89bb9c : 0xFF5b8e63; + value.setTextColor(textColor); + value.setLinkTextColor(textColor); + Drawable check=getResources().getDrawable(R.drawable.ic_fluent_checkmark_24_regular, getActivity().getTheme()).mutate(); + check.setTint(textColor); + value.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, check, null); + }else{ + value.setTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary)); + value.setLinkTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.colorAccent)); + value.setCompoundDrawables(null, null, null, null); + } + } + + @Override + public void setImage(int index, Drawable image){ + CustomEmojiSpan span=index>=item.nameEmojis.length ? item.valueEmojis[index-item.nameEmojis.length] : item.nameEmojis[index]; + span.setDrawable(image); + title.invalidate(); + value.invalidate(); + } + + @Override + public void clearImage(int index){ + setImage(index, null); + } + } + + 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())); + findViewById(R.id.remove_row_btn).setOnClickListener(this::onRemoveRowClick); + } + + @Override + public void onBind(AccountField item){ + title.setText(item.name); + value.setText(item.value); + } + + private void onRemoveRowClick(View v){ + int pos=getAbsoluteAdapterPosition(); + metadataListData.remove(pos); + adapter.notifyItemRemoved(pos); + for(int i=0;itoPosition;i--) { + Collections.swap(metadataListData, 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/res/layout/fragment_profile.xml b/mastodon/src/main/res/layout/fragment_profile.xml index 3be922fb9..d063a1848 100644 --- a/mastodon/src/main/res/layout/fragment_profile.xml +++ b/mastodon/src/main/res/layout/fragment_profile.xml @@ -303,9 +303,15 @@ android:ellipsize="middle" tools:text="followers"/> - + + + android:paddingHorizontal="16dp"> \ No newline at end of file diff --git a/mastodon/src/main/res/layout/item_profile_about_editable.xml b/mastodon/src/main/res/layout/item_profile_about_editable.xml index d0b6acb7e..37e78b1f3 100644 --- a/mastodon/src/main/res/layout/item_profile_about_editable.xml +++ b/mastodon/src/main/res/layout/item_profile_about_editable.xml @@ -4,9 +4,7 @@ android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="?colorBackgroundLight" - android:elevation="2dp" - android:outlineProvider="background"> + android:layout_marginBottom="4dp"> @@ -41,7 +40,9 @@ android:layout_below="@id/title" android:layout_toStartOf="@id/dragger_thingy" android:layout_toEndOf="@id/remove_row_btn" - android:layout_marginBottom="16dp" + android:minHeight="28dp" + android:paddingBottom="3dp" + android:gravity="bottom" android:textAppearance="@style/m3_body_large" android:background="@drawable/bg_profile_field_edit_text" android:hint="@string/field_content" @@ -55,7 +56,6 @@ android:layout_alignParentEnd="true" android:layout_alignParentTop="true" android:layout_alignBottom="@id/value" - android:layout_marginBottom="-16dp" android:scaleType="center" android:contentDescription="@string/reorder" android:src="@drawable/ic_fluent_re_order_dots_vertical_24_regular"/>