Compose things

This commit is contained in:
Grishka 2022-03-15 21:40:52 +03:00
parent b2588fbb6e
commit 8c5d6cd4a6
11 changed files with 532 additions and 33 deletions

View File

@ -10,7 +10,7 @@ android {
applicationId "org.joinmastodon.android"
minSdk 23
targetSdk 31
versionCode 12
versionCode 13
versionName "0.1"
}

View File

@ -0,0 +1,19 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Attachment;
public class UpdateAttachment extends MastodonAPIRequest<Attachment>{
public UpdateAttachment(String id, String description){
super(HttpMethod.PUT, "/media/"+id, Attachment.class);
setRequestBody(new Body(description));
}
private static class Body{
public String description;
public Body(String description){
this.description=description;
}
}
}

View File

@ -12,6 +12,7 @@ import android.icu.text.BreakIterator;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
@ -33,6 +34,7 @@ import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.twitter.twittertext.Regex;
import com.twitter.twittertext.TwitterTextEmojiRegex;
@ -58,8 +60,11 @@ import org.joinmastodon.android.ui.PopupKeyboard;
import org.joinmastodon.android.ui.drawables.SpoilerStripesDrawable;
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.ComposeMediaLayout;
import org.joinmastodon.android.ui.views.ReorderableLinearLayout;
import org.joinmastodon.android.ui.views.SizeListenerLinearLayout;
import org.parceler.Parcel;
import org.parceler.Parcels;
import java.util.ArrayList;
@ -80,7 +85,9 @@ import me.grishka.appkit.utils.V;
public class ComposeFragment extends ToolbarFragment implements OnBackPressedListener{
private static final int MEDIA_RESULT=717;
private static final int IMAGE_DESCRIPTION_RESULT=363;
private static final int MAX_POLL_OPTIONS=4;
private static final int MAX_ATTACHMENTS=4;
private static final Pattern MENTION_PATTERN=Pattern.compile("(^|[^\\/\\w])@(([a-z0-9_]+)@[a-z0-9\\.\\-]+[a-z0-9]+)", Pattern.CASE_INSENSITIVE);
@ -115,7 +122,7 @@ public class ComposeFragment extends ToolbarFragment implements OnBackPressedLis
private Button publishButton;
private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, visibilityBtn;
private LinearLayout attachmentsView;
private ComposeMediaLayout attachmentsView;
private TextView replyText;
private ReorderableLinearLayout pollOptionsView;
private View pollWrap;
@ -124,7 +131,7 @@ public class ComposeFragment extends ToolbarFragment implements OnBackPressedLis
private ArrayList<DraftPollOption> pollOptions=new ArrayList<>();
private ArrayList<DraftMediaAttachment> queuedAttachments=new ArrayList<>(), failedAttachments=new ArrayList<>(), attachments=new ArrayList<>();
private ArrayList<DraftMediaAttachment> queuedAttachments=new ArrayList<>(), failedAttachments=new ArrayList<>(), attachments=new ArrayList<>(), allAttachments=new ArrayList<>();
private DraftMediaAttachment uploadingAttachment;
private List<EmojiCategory> customEmojis;
@ -244,7 +251,20 @@ public class ComposeFragment extends ToolbarFragment implements OnBackPressedLis
spoilerBtn.setSelected(true);
}
// TODO save and restore media attachments (when design is ready)
if(savedInstanceState!=null && savedInstanceState.containsKey("attachments")){
ArrayList<Parcelable> serializedAttachments=savedInstanceState.getParcelableArrayList("attachments");
for(Parcelable a:serializedAttachments){
DraftMediaAttachment att=Parcels.unwrap(a);
attachmentsView.addView(createMediaAttachmentView(att));
attachments.add(att);
}
attachmentsView.setVisibility(View.VISIBLE);
}else if(!allAttachments.isEmpty()){
attachmentsView.setVisibility(View.VISIBLE);
for(DraftMediaAttachment att:allAttachments){
attachmentsView.addView(createMediaAttachmentView(att));
}
}
return view;
}
@ -261,6 +281,13 @@ public class ComposeFragment extends ToolbarFragment implements OnBackPressedLis
outState.putInt("pollDuration", pollDuration);
outState.putString("pollDurationStr", pollDurationStr);
outState.putBoolean("hasSpoiler", hasSpoiler);
if(!attachments.isEmpty()){
ArrayList<Parcelable> serializedAttachments=new ArrayList<>(attachments.size());
for(DraftMediaAttachment att:attachments){
serializedAttachments.add(Parcels.wrap(att));
}
outState.putParcelableArrayList("attachments", serializedAttachments);
}
}
}
@ -473,6 +500,21 @@ public class ComposeFragment extends ToolbarFragment implements OnBackPressedLis
}
}
@Override
public void onFragmentResult(int reqCode, boolean success, Bundle result){
if(reqCode==IMAGE_DESCRIPTION_RESULT && success){
Attachment updated=Parcels.unwrap(result.getParcelable("attachment"));
for(DraftMediaAttachment att:attachments){
if(att.serverAttachment.id.equals(updated.id)){
att.serverAttachment=updated;
att.description=updated.description;
att.descriptionView.setText(att.description);
break;
}
}
}
}
private void confirmDiscardDraftAndFinish(){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.discard_draft)
@ -506,26 +548,61 @@ public class ComposeFragment extends ToolbarFragment implements OnBackPressedLis
}
private void addMediaAttachment(Uri uri){
if(getMediaAttachmentsCount()==MAX_ATTACHMENTS)
return;
pollBtn.setEnabled(false);
View thumb=getActivity().getLayoutInflater().inflate(R.layout.compose_media_thumb, attachmentsView, false);
ImageView img=thumb.findViewById(R.id.thumb);
ViewImageLoader.load(img, null, new UrlImageLoaderRequest(uri, V.dp(250), V.dp(250)));
attachmentsView.addView(thumb);
DraftMediaAttachment draft=new DraftMediaAttachment();
draft.uri=uri;
draft.view=thumb;
draft.progressBar=thumb.findViewById(R.id.progress);
Button btn=thumb.findViewById(R.id.remove_btn);
btn.setTag(draft);
btn.setOnClickListener(this::onRemoveMediaAttachmentClick);
attachmentsView.addView(createMediaAttachmentView(draft));
allAttachments.add(draft);
attachmentsView.setVisibility(View.VISIBLE);
if(uploadingAttachment==null){
uploadMediaAttachment(draft);
}else{
queuedAttachments.add(draft);
}
updatePublishButtonState();
if(getMediaAttachmentsCount()==MAX_ATTACHMENTS)
mediaBtn.setEnabled(false);
}
private View createMediaAttachmentView(DraftMediaAttachment draft){
View thumb=getActivity().getLayoutInflater().inflate(R.layout.compose_media_thumb, attachmentsView, false);
ImageView img=thumb.findViewById(R.id.thumb);
ViewImageLoader.load(img, null, new UrlImageLoaderRequest(draft.uri, V.dp(250), V.dp(250)));
TextView fileName=thumb.findViewById(R.id.file_name);
fileName.setText(UiUtils.getFileName(draft.uri));
draft.view=thumb;
draft.progressBar=thumb.findViewById(R.id.progress);
draft.infoBar=thumb.findViewById(R.id.info_bar);
draft.errorOverlay=thumb.findViewById(R.id.error_overlay);
draft.descriptionView=thumb.findViewById(R.id.description);
ImageButton btn=thumb.findViewById(R.id.remove_btn);
btn.setTag(draft);
btn.setOnClickListener(this::onRemoveMediaAttachmentClick);
btn=thumb.findViewById(R.id.remove_btn2);
btn.setTag(draft);
btn.setOnClickListener(this::onRemoveMediaAttachmentClick);
Button retry=thumb.findViewById(R.id.retry_upload);
retry.setTag(draft);
retry.setOnClickListener(this::onRetryMediaUploadClick);
draft.infoBar.setTag(draft);
draft.infoBar.setOnClickListener(this::onEditMediaDescriptionClick);
if(!TextUtils.isEmpty(draft.description))
draft.descriptionView.setText(draft.description);
if(uploadingAttachment!=draft && !queuedAttachments.contains(draft)){
draft.progressBar.setVisibility(View.GONE);
}
if(failedAttachments.contains(draft)){
draft.infoBar.setVisibility(View.GONE);
draft.errorOverlay.setVisibility(View.VISIBLE);
}
return thumb;
}
private void uploadMediaAttachment(DraftMediaAttachment attachment){
@ -561,8 +638,11 @@ public class ComposeFragment extends ToolbarFragment implements OnBackPressedLis
attachment.uploadRequest=null;
uploadingAttachment=null;
failedAttachments.add(attachment);
error.showToast(getActivity());
// TODO show the error state in the attachment view
// error.showToast(getActivity());
Toast.makeText(getActivity(), R.string.image_upload_failed, Toast.LENGTH_SHORT).show();
V.setVisibilityAnimated(attachment.errorOverlay, View.VISIBLE);
V.setVisibilityAnimated(attachment.infoBar, View.GONE);
if(!queuedAttachments.isEmpty())
uploadMediaAttachment(queuedAttachments.remove(0));
@ -584,9 +664,38 @@ public class ComposeFragment extends ToolbarFragment implements OnBackPressedLis
queuedAttachments.remove(att);
failedAttachments.remove(att);
}
allAttachments.remove(att);
attachmentsView.removeView(att.view);
if(getMediaAttachmentsCount()==0)
attachmentsView.setVisibility(View.GONE);
updatePublishButtonState();
pollBtn.setEnabled(attachments.isEmpty() && queuedAttachments.isEmpty() && failedAttachments.isEmpty() && uploadingAttachment==null);
mediaBtn.setEnabled(true);
}
private void onRetryMediaUploadClick(View v){
DraftMediaAttachment att=(DraftMediaAttachment) v.getTag();
if(failedAttachments.remove(att)){
V.setVisibilityAnimated(att.errorOverlay, View.GONE);
V.setVisibilityAnimated(att.infoBar, View.VISIBLE);
V.setVisibilityAnimated(att.progressBar, View.VISIBLE);
if(uploadingAttachment==null)
uploadMediaAttachment(att);
else
queuedAttachments.add(att);
}
}
private void onEditMediaDescriptionClick(View v){
DraftMediaAttachment att=(DraftMediaAttachment) v.getTag();
if(att.serverAttachment==null)
return;
Bundle args=new Bundle();
args.putString("account", accountID);
args.putString("attachment", att.serverAttachment.id);
args.putParcelable("uri", att.uri);
args.putString("existingDescription", att.description);
Nav.goForResult(getActivity(), ComposeImageDescriptionFragment.class, args, IMAGE_DESCRIPTION_RESULT, this);
}
private void togglePoll(){
@ -680,13 +789,22 @@ public class ComposeFragment extends ToolbarFragment implements OnBackPressedLis
}
}
private static class DraftMediaAttachment{
private int getMediaAttachmentsCount(){
return allAttachments.size();
}
@Parcel
static class DraftMediaAttachment{
public Attachment serverAttachment;
public Uri uri;
public UploadAttachment uploadRequest;
public transient UploadAttachment uploadRequest;
public String description;
public View view;
public ProgressBar progressBar;
public transient View view;
public transient ProgressBar progressBar;
public transient TextView descriptionView;
public transient View errorOverlay;
public transient View infoBar;
}
private static class DraftPollOption{

View File

@ -0,0 +1,113 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.content.res.TypedArray;
import android.net.Uri;
import android.os.Bundle;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.UpdateAttachment;
import org.joinmastodon.android.model.Attachment;
import org.parceler.Parcels;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class ComposeImageDescriptionFragment extends ToolbarFragment{
private String accountID, attachmentID;
private EditText edit;
private Button saveButton;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
attachmentID=getArguments().getString("attachment");
setHasOptionsMenu(true);
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setTitle(R.string.edit_image);
}
@Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
View view=inflater.inflate(R.layout.fragment_image_description, container, false);
edit=view.findViewById(R.id.edit);
ImageView image=view.findViewById(R.id.photo);
Uri uri=getArguments().getParcelable("uri");
ViewImageLoader.load(image, null, new UrlImageLoaderRequest(uri, 1000, 1000));
edit.setText(getArguments().getString("existingDescription"));
return view;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
edit.requestFocus();
view.postDelayed(()->getActivity().getSystemService(InputMethodManager.class).showSoftInput(edit, 0), 100);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
TypedArray ta=getActivity().obtainStyledAttributes(new int[]{R.attr.secondaryButtonStyle});
int buttonStyle=ta.getResourceId(0, 0);
ta.recycle();
saveButton=new Button(getActivity(), null, 0, buttonStyle);
saveButton.setText(R.string.save);
saveButton.setOnClickListener(this::onSaveClick);
FrameLayout wrap=new FrameLayout(getActivity());
wrap.addView(saveButton, 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.publish);
item.setActionView(wrap);
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
return true;
}
private void onSaveClick(View v){
new UpdateAttachment(attachmentID, edit.getText().toString().trim())
.setCallback(new Callback<>(){
@Override
public void onSuccess(Attachment result){
Bundle r=new Bundle();
r.putParcelable("attachment", Parcels.wrap(result));
setResult(true, r);
Nav.finish(ComposeImageDescriptionFragment.this);
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.saving, false)
.exec(accountID);
}
}

View File

@ -0,0 +1,97 @@
package org.joinmastodon.android.ui.views;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import me.grishka.appkit.utils.V;
public class ComposeMediaLayout extends ViewGroup{
private static final int MAX_WIDTH_DP=400;
private static final int GAP_DP=8;
private static final float ASPECT_RATIO=0.5625f;
public ComposeMediaLayout(Context context){
this(context, null);
}
public ComposeMediaLayout(Context context, AttributeSet attrs){
this(context, attrs, 0);
}
public ComposeMediaLayout(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
int mode=MeasureSpec.getMode(widthMeasureSpec);
@SuppressLint("SwitchIntDef")
int width=switch(mode){
case MeasureSpec.AT_MOST -> Math.min(V.dp(MAX_WIDTH_DP), MeasureSpec.getSize(widthMeasureSpec));
case MeasureSpec.EXACTLY -> MeasureSpec.getSize(widthMeasureSpec);
default -> throw new IllegalArgumentException("unsupported measure mode");
};
int height=Math.round(width*ASPECT_RATIO);
setMeasuredDimension(width, height);
// We don't really need this, but some layouts will freak out if you don't measure them
int childWidth, firstChildHeight, otherChildrenHeight=0;
int gap=V.dp(GAP_DP);
switch(getChildCount()){
case 0 -> {
return;
}
case 1 -> {
childWidth=width;
firstChildHeight=height;
}
case 2 -> {
childWidth=(width-gap)/2;
firstChildHeight=otherChildrenHeight=height;
}
case 3 -> {
childWidth=(width-gap)/2;
firstChildHeight=height;
otherChildrenHeight=(height-gap)/2;
}
default -> {
childWidth=(width-gap)/2;
firstChildHeight=otherChildrenHeight=(height-gap)/2;
}
}
for(int i=0;i<getChildCount();i++){
getChildAt(i).measure(childWidth | MeasureSpec.EXACTLY, (i==0 ? firstChildHeight : otherChildrenHeight) | MeasureSpec.EXACTLY);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b){
int gap=V.dp(GAP_DP);
int width=r-l;
int height=b-t;
int halfWidth=(width-gap)/2;
int halfHeight=(height-gap)/2;
switch(getChildCount()){
case 0 -> {}
case 1 -> getChildAt(0).layout(0, 0, width, height);
case 2 -> {
getChildAt(0).layout(0, 0, halfWidth, height);
getChildAt(1).layout(halfWidth+gap, 0, width, height);
}
case 3 -> {
getChildAt(0).layout(0, 0, halfWidth, height);
getChildAt(1).layout(halfWidth+gap, 0, width, halfHeight);
getChildAt(2).layout(halfWidth+gap, halfHeight+gap, width, height);
}
default -> {
getChildAt(0).layout(0, 0, halfWidth, halfHeight);
getChildAt(1).layout(halfWidth+gap, 0, width, halfHeight);
getChildAt(2).layout(0, halfHeight+gap, halfWidth, height);
getChildAt(3).layout(halfWidth+gap, halfHeight+gap, width, height);
}
}
}
}

View File

@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="20dp" android:height="20dp" android:viewportWidth="20" android:viewportHeight="20">
<path android:pathData="M11.5 4c0-0.828-0.672-1.5-1.5-1.5S8.5 3.172 8.5 4h-1c0-1.38 1.12-2.5 2.5-2.5s2.5 1.12 2.5 2.5H17c0.276 0 0.5 0.224 0.5 0.5S17.276 5 17 5h-0.554L15.15 16.23C15.033 17.237 14.179 18 13.163 18H6.837c-1.016 0-1.87-0.762-1.987-1.77L3.553 5H3C2.755 5 2.55 4.823 2.508 4.59L2.5 4.5C2.5 4.224 2.724 4 3 4h8.5zm3.938 1H4.561l1.282 11.115C5.902 16.619 6.33 17 6.837 17h6.326c0.508 0 0.935-0.38 0.993-0.885L15.438 5zM8.5 7.5c0.245 0 0.45 0.155 0.492 0.359L9 7.938v6.125c0 0.24-0.224 0.437-0.5 0.437-0.245 0-0.45-0.155-0.492-0.359L8 14.062V7.939C8 7.696 8.224 7.5 8.5 7.5zm3 0c0.245 0 0.45 0.155 0.492 0.359L12 7.938v6.125c0 0.241-0.224 0.437-0.5 0.437-0.245 0-0.45-0.155-0.492-0.359L11 14.062V7.939C11 7.696 11.224 7.5 11.5 7.5z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="28dp" android:height="28dp" android:viewportWidth="28" android:viewportHeight="28">
<path android:pathData="M14 2.25c2.004 0 3.641 1.572 3.745 3.55L17.75 6h5.5C23.664 6 24 6.336 24 6.75c0 0.38-0.282 0.694-0.648 0.743L23.25 7.5h-1.059l-1.22 15.053C20.813 24.5 19.187 26 17.233 26h-6.466c-1.954 0-3.58-1.5-3.738-3.447L5.808 7.5H4.75c-0.38 0-0.694-0.282-0.743-0.648L4 6.75c0-0.38 0.282-0.694 0.648-0.743L4.75 6h5.5c0-2.071 1.679-3.75 3.75-3.75zm6.687 5.25H7.313l1.211 14.932C8.619 23.6 9.594 24.5 10.767 24.5h6.466c1.172 0 2.148-0.9 2.243-2.068L20.686 7.5zm-8.937 3.75c0.38 0 0.694 0.282 0.743 0.648L12.5 12v8c0 0.414-0.336 0.75-0.75 0.75-0.38 0-0.694-0.282-0.743-0.648L11 20v-8c0-0.414 0.336-0.75 0.75-0.75zm4.5 0c0.38 0 0.694 0.282 0.743 0.648L17 12v8c0 0.414-0.336 0.75-0.75 0.75-0.38 0-0.694-0.282-0.743-0.648L15.5 20v-8c0-0.414 0.336-0.75 0.75-0.75zM14 3.75c-1.19 0-2.166 0.925-2.245 2.096L11.75 6h4.5l-0.005-0.154C16.165 4.676 15.19 3.75 14 3.75z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="250dp"
android:layout_height="150dp">
@ -7,20 +8,93 @@
android:id="@+id/thumb"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"/>
<Button
android:id="@+id/remove_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|right"
android:text="X"/>
android:scaleType="centerCrop"
tools:src="#0f0"/>
<ProgressBar
android:id="@+id/progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_gravity="top"
style="?android:progressBarStyleHorizontal"/>
<RelativeLayout
android:id="@+id/info_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="#f2000000"
android:backgroundTint="?colorWindowBackground">
<ImageButton
android:id="@+id/remove_btn"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="2dp"
android:layout_marginStart="2dp"
android:background="?android:selectableItemBackgroundBorderless"
android:tint="#D92C2C"
android:src="@drawable/ic_fluent_delete_20_regular"/>
<TextView
android:id="@+id/file_name"
android:layout_width="match_parent"
android:layout_height="24dp"
android:textAppearance="@style/m3_body_large"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_toStartOf="@id/remove_btn"
android:gravity="center_vertical"
android:singleLine="true"
android:ellipsize="middle"
tools:text="asdf.jpg"/>
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/file_name"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginBottom="8dp"
android:maxLines="2"
android:ellipsize="end"
android:textAppearance="@style/m3_body_medium"
android:textColor="?android:textColorSecondary"
android:text="@string/add_image_description"/>
</RelativeLayout>
<FrameLayout
android:id="@+id/error_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#cc000000"
android:backgroundTint="?colorWindowBackground"
android:padding="8dp"
android:clipToPadding="false"
android:visibility="gone">
<Button
android:id="@+id/retry_upload"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start|bottom"
style="?secondaryButtonStyle"
android:text="@string/retry_upload"/>
<ImageButton
android:id="@+id/remove_btn2"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_marginBottom="4dp"
android:layout_gravity="end|bottom"
android:background="?android:selectableItemBackgroundBorderless"
android:tint="#D92C2C"
android:src="@drawable/ic_fluent_delete_20_regular"/>
</FrameLayout>
</FrameLayout>

View File

@ -6,7 +6,7 @@
android:layout_height="match_parent">
<ScrollView
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:fillViewport="true">
@ -137,11 +137,12 @@
tools:text="Duration: 7 days"/>
</LinearLayout>
<LinearLayout
<org.joinmastodon.android.ui.views.ComposeMediaLayout
android:id="@+id/attachments"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"/>
android:layout_gravity="center_horizontal"
android:visibility="gone"/>
</LinearLayout>
</ScrollView>

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<org.joinmastodon.android.ui.views.ComposeMediaLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal">
<ImageView
android:id="@+id/photo"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:importantForAccessibility="no"
tools:src="#0f0"/>
</org.joinmastodon.android.ui.views.ComposeMediaLayout>
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="16dp"
android:textAppearance="@style/m3_headline_medium"
android:minHeight="36dp"
android:gravity="center_vertical"
android:text="@string/add_alt_text"/>
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:textAppearance="@style/m3_title_medium"
android:textColor="?android:textColorSecondary"
android:text="@string/alt_text_subtitle"/>
<EditText
android:id="@+id/edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:minHeight="212dp"
android:gravity="top"
android:inputType="textCapSentences|textMultiLine"
android:hint="@string/alt_text_hint"/>
</LinearLayout>
</ScrollView>

View File

@ -205,4 +205,13 @@
<string name="resent_email">Confirmation email sent</string>
<string name="compose_hint">Type or paste what\'s on your mind</string>
<string name="content_warning">Content warning</string>
<string name="add_image_description">Add image description…</string>
<string name="retry_upload">Retry upload</string>
<string name="image_upload_failed">Image failed to upload</string>
<string name="video_upload_failed">Video failed to upload</string>
<string name="edit_image">Edit image</string>
<string name="save">Save</string>
<string name="add_alt_text">Add alt text</string>
<string name="alt_text_subtitle">Alt text describes your photos for people with low or no vision. Try to only include enough detail to understand the context.</string>
<string name="alt_text_hint">e.g. A dog looking around suspiciously with narrowed eyes at the camera.</string>
</resources>