Compose design + media upload

This commit is contained in:
Grishka 2022-02-04 13:50:19 +03:00
parent 20d3a62747
commit cc06715aa6
24 changed files with 668 additions and 85 deletions

View File

@ -0,0 +1,72 @@
package org.joinmastodon.android.api;
import android.database.Cursor;
import android.net.Uri;
import android.os.SystemClock;
import android.provider.OpenableColumns;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.IOException;
import okhttp3.MediaType;
import okhttp3.RequestBody;
import okio.Buffer;
import okio.BufferedSink;
import okio.ForwardingSink;
import okio.Okio;
import okio.Sink;
import okio.Source;
public class ContentUriRequestBody extends RequestBody{
private final Uri uri;
private final long length;
private ProgressListener progressListener;
public ContentUriRequestBody(Uri uri, ProgressListener progressListener){
this.uri=uri;
this.progressListener=progressListener;
try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.SIZE}, null, null, null)){
cursor.moveToFirst();
length=cursor.getInt(0);
}
}
@Override
public MediaType contentType(){
return MediaType.get(MastodonApp.context.getContentResolver().getType(uri));
}
@Override
public long contentLength() throws IOException{
return length;
}
@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();
}
}
private class CountingSink extends ForwardingSink{
private long bytesWritten=0;
private long lastCallbackTime;
public CountingSink(Sink delegate){
super(delegate);
}
@Override
public void write(Buffer source, long byteCount) throws IOException{
super.write(source, byteCount);
bytesWritten+=byteCount;
if(SystemClock.uptimeMillis()-lastCallbackTime>=100L || bytesWritten==length){
lastCallbackTime=SystemClock.uptimeMillis();
UiUtils.runOnUiThread(()->progressListener.onProgress(bytesWritten, length));
}
}
}
}

View File

@ -0,0 +1,5 @@
package org.joinmastodon.android.api;
public interface ProgressListener{
void onProgress(long transferred, long total);
}

View File

@ -0,0 +1,44 @@
package org.joinmastodon.android.api.requests.statuses;
import android.database.Cursor;
import android.net.Uri;
import android.provider.OpenableColumns;
import org.joinmastodon.android.MastodonApp;
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 okhttp3.MultipartBody;
import okhttp3.RequestBody;
public class UploadAttachment extends MastodonAPIRequest<Attachment>{
private Uri uri;
private ProgressListener progressListener;
public UploadAttachment(Uri uri){
super(HttpMethod.POST, "/media", Attachment.class);
this.uri=uri;
}
public UploadAttachment setProgressListener(ProgressListener progressListener){
this.progressListener=progressListener;
return this;
}
@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))
.build();
}
}

View File

@ -2,12 +2,19 @@ package org.joinmastodon.android.fragments;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.ClipData;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Outline;
import android.icu.text.BreakIterator;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@ -16,9 +23,13 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.twitter.twittertext.Regex;
@ -26,31 +37,40 @@ import com.twitter.twittertext.TwitterTextEmojiRegex;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.ProgressListener;
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
import org.joinmastodon.android.api.requests.statuses.UploadAttachment;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.EmojiCategory;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.CustomEmojiPopupKeyboard;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.PopupKeyboard;
import org.joinmastodon.android.ui.views.SizeListenerLinearLayout;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.OnBackPressedListener;
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 ComposeFragment extends ToolbarFragment{
public class ComposeFragment extends ToolbarFragment implements OnBackPressedListener{
private static final int MEDIA_RESULT=717;
private static final Pattern MENTION_PATTERN=Pattern.compile("(^|[^\\/\\w])@(([a-z0-9_]+)@[a-z0-9\\.\\-]+[a-z0-9]+)", Pattern.CASE_INSENSITIVE);
@ -81,10 +101,14 @@ public class ComposeFragment extends ToolbarFragment{
private EditText mainEditText;
private TextView charCounter;
private String accountID;
private int charCount, charLimit;
private int charCount, charLimit, trimmedCharCount;
private MenuItem publishButton;
private ImageButton emojiBtn;
private Button publishButton;
private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, visibilityBtn;
private LinearLayout attachmentsView;
private ArrayList<DraftMediaAttachment> queuedAttachments=new ArrayList<>(), failedAttachments=new ArrayList<>(), attachments=new ArrayList<>();
private DraftMediaAttachment uploadingAttachment;
private List<EmojiCategory> customEmojis;
private CustomEmojiPopupKeyboard emojiKeyboard;
@ -127,7 +151,13 @@ public class ComposeFragment extends ToolbarFragment{
selfAvatar.setOutlineProvider(roundCornersOutline);
selfAvatar.setClipToOutline(true);
mediaBtn=view.findViewById(R.id.btn_media);
pollBtn=view.findViewById(R.id.btn_poll);
emojiBtn=view.findViewById(R.id.btn_emoji);
spoilerBtn=view.findViewById(R.id.btn_spoiler);
visibilityBtn=view.findViewById(R.id.btn_visibility);
mediaBtn.setOnClickListener(v->openFilePicker());
emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText));
emojiKeyboard.setOnIconChangedListener(new PopupKeyboard.OnIconChangeListener(){
@Override
@ -138,6 +168,9 @@ public class ComposeFragment extends ToolbarFragment{
contentView=(SizeListenerLinearLayout) view;
contentView.addView(emojiKeyboard.getView());
emojiKeyboard.getView().setElevation(V.dp(2));
attachmentsView=view.findViewById(R.id.attachments);
return view;
}
@ -173,35 +206,26 @@ public class ComposeFragment extends ToolbarFragment{
updateCharCounter(s);
}
});
updateToolbar();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
publishButton=menu.add("TOOT!");
publishButton.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
publishButton=new Button(getActivity());
publishButton.setText(R.string.publish);
publishButton.setOnClickListener(this::onPublishClick);
FrameLayout wrap=new FrameLayout(getActivity());
wrap.addView(publishButton, 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);
updatePublishButtonState();
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
String text=mainEditText.getText().toString();
CreateStatus.Request req=new CreateStatus.Request();
req.status=text;
String uuid=UUID.randomUUID().toString();
new CreateStatus(req, uuid)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Status result){
Nav.finish(ComposeFragment.this);
E.post(new StatusCreatedEvent(result));
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.exec(accountID);
return true;
}
@ -209,6 +233,7 @@ public class ComposeFragment extends ToolbarFragment{
public void onConfigurationChanged(Configuration newConfig){
super.onConfigurationChanged(newConfig);
emojiKeyboard.onConfigurationChanged();
updateToolbar();
}
@SuppressLint("NewApi")
@ -225,14 +250,200 @@ public class ComposeFragment extends ToolbarFragment{
}
charCounter.setText(String.valueOf(charLimit-charCount));
trimmedCharCount=text.toString().trim().length();
updatePublishButtonState();
}
private void updatePublishButtonState(){
publishButton.setEnabled(charCount>0 && charCount<=charLimit);
publishButton.setEnabled((trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit && uploadingAttachment==null && failedAttachments.isEmpty() && queuedAttachments.isEmpty());
}
private void onCustomEmojiClick(Emoji emoji){
mainEditText.getText().replace(mainEditText.getSelectionStart(), mainEditText.getSelectionEnd(), ':'+emoji.shortcode+':');
}
private void updateToolbar(){
getToolbar().setNavigationIcon(R.drawable.ic_fluent_dismiss_24_regular);
}
private void onPublishClick(View v){
publish();
}
private void publish(){
String text=mainEditText.getText().toString();
CreateStatus.Request req=new CreateStatus.Request();
req.status=text;
if(!attachments.isEmpty()){
req.mediaIds=attachments.stream().map(a->a.serverAttachment.id).collect(Collectors.toList());
}
String uuid=UUID.randomUUID().toString();
ProgressDialog progress=new ProgressDialog(getActivity());
progress.setMessage(getString(R.string.publishing));
progress.setCancelable(false);
progress.show();
new CreateStatus(req, uuid)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Status result){
progress.dismiss();
Nav.finish(ComposeFragment.this);
E.post(new StatusCreatedEvent(result));
}
@Override
public void onError(ErrorResponse error){
progress.dismiss();
error.showToast(getActivity());
}
})
.exec(accountID);
}
private boolean hasDraft(){
return mainEditText.length()>0;
}
@Override
public boolean onBackPressed(){
if(emojiKeyboard.isVisible()){
emojiKeyboard.hide();
return true;
}
if(hasDraft()){
confirmDiscardDraftAndFinish();
return true;
}
return false;
}
@Override
public void onToolbarNavigationClick(){
if(hasDraft()){
confirmDiscardDraftAndFinish();
}else{
super.onToolbarNavigationClick();
}
}
private void confirmDiscardDraftAndFinish(){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.discard_draft)
.setPositiveButton(R.string.discard, (dialog, which)->Nav.finish(this))
.setNegativeButton(R.string.cancel, null)
.show();
}
private void openFilePicker(){
Intent intent=new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.setType("*/*");
intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{"image/*", "video/*"});
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
startActivityForResult(intent, MEDIA_RESULT);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data){
if(requestCode==MEDIA_RESULT && resultCode==Activity.RESULT_OK){
Uri single=data.getData();
if(single!=null){
addMediaAttachment(single);
}else{
ClipData clipData=data.getClipData();
for(int i=0;i<clipData.getItemCount();i++){
addMediaAttachment(clipData.getItemAt(i).getUri());
}
}
}
}
private void addMediaAttachment(Uri uri){
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);
if(uploadingAttachment==null){
uploadMediaAttachment(draft);
}else{
queuedAttachments.add(draft);
}
updatePublishButtonState();
}
private void uploadMediaAttachment(DraftMediaAttachment attachment){
if(uploadingAttachment!=null)
throw new IllegalStateException("there is already an attachment being uploaded");
uploadingAttachment=attachment;
attachment.uploadRequest=(UploadAttachment) new UploadAttachment(attachment.uri)
.setProgressListener(new ProgressListener(){
@Override
public void onProgress(long transferred, long total){
int progress=Math.round(transferred/(float)total*attachment.progressBar.getMax());
if(Build.VERSION.SDK_INT>=24)
attachment.progressBar.setProgress(progress, true);
else
attachment.progressBar.setProgress(progress);
}
})
.setCallback(new Callback<>(){
@Override
public void onSuccess(Attachment result){
attachment.serverAttachment=result;
attachment.uploadRequest=null;
uploadingAttachment=null;
attachments.add(attachment);
attachment.progressBar.setVisibility(View.GONE);
if(!queuedAttachments.isEmpty())
uploadMediaAttachment(queuedAttachments.remove(0));
updatePublishButtonState();
}
@Override
public void onError(ErrorResponse error){
attachment.uploadRequest=null;
uploadingAttachment=null;
failedAttachments.add(attachment);
error.showToast(getActivity());
// TODO show the error state in the attachment view
if(!queuedAttachments.isEmpty())
uploadMediaAttachment(queuedAttachments.remove(0));
}
})
.exec(accountID);
}
private void onRemoveMediaAttachmentClick(View v){
DraftMediaAttachment att=(DraftMediaAttachment) v.getTag();
if(att==uploadingAttachment){
att.uploadRequest.cancel();
if(!queuedAttachments.isEmpty())
uploadMediaAttachment(queuedAttachments.remove(0));
}else{
attachments.remove(att);
queuedAttachments.remove(att);
failedAttachments.remove(att);
}
attachmentsView.removeView(att.view);
updatePublishButtonState();
}
private static class DraftMediaAttachment{
public Attachment serverAttachment;
public Uri uri;
public UploadAttachment uploadRequest;
public View view;
public ProgressBar progressBar;
}
}

View File

@ -0,0 +1,31 @@
package org.joinmastodon.android.ui;
import android.app.AlertDialog;
import android.content.Context;
import android.view.View;
import android.widget.Button;
import me.grishka.appkit.utils.V;
public class M3AlertDialogBuilder extends AlertDialog.Builder{
public M3AlertDialogBuilder(Context context){
super(context);
}
public M3AlertDialogBuilder(Context context, int themeResId){
super(context, themeResId);
}
@Override
public AlertDialog create(){
AlertDialog alert=super.create();
alert.create();
Button btn=alert.getButton(AlertDialog.BUTTON_POSITIVE);
if(btn!=null){
View buttonBar=(View) btn.getParent();
buttonBar.setPadding(V.dp(16), V.dp(24), V.dp(16), V.dp(24));
((View)buttonBar.getParent()).setPadding(0, 0, 0, 0);
}
return alert;
}
}

View File

@ -4,6 +4,8 @@ import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.widget.TextView;
@ -15,6 +17,8 @@ import androidx.annotation.ColorRes;
import androidx.browser.customtabs.CustomTabsIntent;
public class UiUtils{
private static Handler mainHandler=new Handler(Looper.getMainLooper());
private UiUtils(){}
public static void launchWebBrowser(Context context, String url){
@ -56,4 +60,8 @@ public class UiUtils{
}
textView.setCompoundDrawablesRelative(drawables[0], drawables[1], drawables[2], drawables[3]);
}
public static void runOnUiThread(Runnable runnable){
mainHandler.post(runnable);
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/gray_800" android:state_enabled="true"/>
<item android:color="@color/gray_800_alpha50"/>
</selector>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android" android:inset="16dp">
<shape>
<solid android:color="?android:attr/colorBackground"/>
<corners android:radius="16dp"/>
</shape>
</inset>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="?android:attr/colorControlHighlight">
<item android:id="@android:id/mask">
<shape>
<solid android:color="#000"/>
<corners android:radius="100dp"/>
<padding android:top="10dp" android:bottom="10dp" android:left="12dp" android:right="12dp"/>
</shape>
</item>
</ripple>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<ripple android:color="@color/highlight_over_dark">
<item>
<shape>
<solid android:color="@color/button_bg"/>
<corners android:radius="10dp"/>
<padding android:left="16dp" android:right="16dp" android:top="8dp" android:bottom="8dp"/>
</shape>
</item>
</ripple>
</item>
</selector>

View File

@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M22 11.998c0-5.523-4.477-10-10-10s-10 4.477-10 10c0 1.643 0.397 3.23 1.145 4.65l-1.116 4.289c-0.037 0.14-0.037 0.288 0 0.428 0.118 0.454 0.582 0.727 1.036 0.608l4.29-1.117c1.42 0.746 3.005 1.142 4.645 1.142 5.523 0 10-4.477 10-10zM12 6.5c0.414 0 0.75 0.335 0.75 0.75v6.25c0 0.414-0.336 0.75-0.75 0.75s-0.75-0.336-0.75-0.75V7.25c0-0.415 0.336-0.75 0.75-0.75zm1 9.997c0 0.553-0.448 1-1 1s-1-0.447-1-1c0-0.552 0.448-1 1-1s1 0.448 1 1z" 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="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M12 6.5c0.414 0 0.75 0.336 0.75 0.75v6.25c0 0.414-0.336 0.75-0.75 0.75s-0.75-0.336-0.75-0.75V7.25c0-0.414 0.336-0.75 0.75-0.75zm0 10.998c0.552 0 1-0.448 1-1 0-0.553-0.448-1-1-1s-1 0.447-1 1c0 0.552 0.448 1 1 1zM12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10c-1.618 0-3.182-0.385-4.587-1.112l-3.826 1.067c-0.665 0.186-1.354-0.202-1.54-0.867-0.062-0.22-0.062-0.453 0-0.673l1.068-3.823C2.386 15.186 2 13.62 2 12 2 6.477 6.477 2 12 2zm0 1.5c-4.694 0-8.5 3.806-8.5 8.5 0 1.47 0.373 2.883 1.073 4.137l0.15 0.27-1.112 3.984 3.986-1.112 0.27 0.15C9.12 20.13 10.532 20.5 12 20.5c4.694 0 8.5-3.806 8.5-8.5S16.694 3.5 12 3.5z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!--~ Copyright (c) 2022. ~ Microsoft Corporation. All rights reserved.-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_fluent_chat_warning_24_filled" android:state_activated="true"/>
<item android:drawable="@drawable/ic_fluent_chat_warning_24_filled" android:state_checked="true"/>
<item android:drawable="@drawable/ic_fluent_chat_warning_24_filled" android:state_selected="true"/>
<item android:drawable="@drawable/ic_fluent_chat_warning_24_regular"/>
</selector>

View File

@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M4.397 4.554L4.47 4.47c0.266-0.267 0.683-0.29 0.976-0.073L5.53 4.47 12 10.939l6.47-6.47c0.293-0.292 0.767-0.292 1.06 0 0.293 0.294 0.293 0.768 0 1.061L13.061 12l6.47 6.47c0.266 0.266 0.29 0.683 0.072 0.976L19.53 19.53c-0.266 0.267-0.683 0.29-0.976 0.073L18.47 19.53 12 13.061l-6.47 6.47c-0.293 0.292-0.767 0.292-1.06 0-0.293-0.294-0.293-0.768 0-1.061L10.939 12l-6.47-6.47C4.204 5.264 4.18 4.847 4.398 4.554L4.47 4.47 4.397 4.554z" 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="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M17.75 3C19.545 3 21 4.455 21 6.25v11.5c0 1.795-1.455 3.25-3.25 3.25H6.25C4.455 21 3 19.545 3 17.75V6.25C3 4.455 4.455 3 6.25 3h11.5zm0.58 16.401l-5.805-5.686c-0.265-0.26-0.675-0.283-0.966-0.071l-0.084 0.07-5.807 5.687C5.85 19.465 6.046 19.5 6.25 19.5h11.5c0.203 0 0.399-0.035 0.58-0.099l-5.805-5.686L18.33 19.4zM17.75 4.5H6.25C5.284 4.5 4.5 5.284 4.5 6.25v11.5c0 0.208 0.036 0.408 0.103 0.594l5.823-5.701c0.833-0.816 2.142-0.854 3.02-0.116l0.128 0.116 5.822 5.702c0.067-0.186 0.104-0.386 0.104-0.595V6.25c0-0.966-0.784-1.75-1.75-1.75zm-2.498 2c1.244 0 2.252 1.008 2.252 2.252 0 1.244-1.008 2.252-2.252 2.252-1.244 0-2.252-1.008-2.252-2.252C13 7.508 14.008 6.5 15.252 6.5zm0 1.5C14.837 8 14.5 8.337 14.5 8.752s0.337 0.752 0.752 0.752 0.752-0.336 0.752-0.752C16.004 8.337 15.667 8 15.252 8z" 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="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M14.75 15c0.966 0 1.75 0.784 1.75 1.75l-0.001 0.962c0.117 2.19-1.511 3.297-4.432 3.297-2.91 0-4.567-1.09-4.567-3.259v-1C7.5 15.784 8.284 15 9.25 15h5.5zm0 1.5h-5.5C9.112 16.5 9 16.612 9 16.75v1c0 1.176 0.887 1.759 3.067 1.759 2.168 0 2.995-0.564 2.933-1.757V16.75c0-0.138-0.112-0.25-0.25-0.25zm-11-6.5h4.376C8.044 10.32 8 10.655 8 11c0 0.17 0.01 0.336 0.03 0.5H3.75c-0.138 0-0.25 0.112-0.25 0.25v1c0 1.176 0.887 1.759 3.067 1.759 0.462 0 0.863-0.026 1.207-0.077-0.565 0.358-0.989 0.917-1.173 1.576L6.567 16.01C3.657 16.009 2 14.919 2 12.75v-1C2 10.784 2.784 10 3.75 10zm16.5 0c0.966 0 1.75 0.784 1.75 1.75l-0.001 0.962c0.117 2.19-1.511 3.297-4.432 3.297l-0.169-0.002c-0.189-0.677-0.631-1.248-1.218-1.606 0.387 0.072 0.847 0.108 1.387 0.108 2.168 0 2.995-0.564 2.933-1.757V11.75c0-0.138-0.112-0.25-0.25-0.25h-4.28C15.99 11.335 16 11.17 16 11c0-0.345-0.044-0.68-0.126-1h4.376zM12 8c1.657 0 3 1.343 3 3s-1.343 3-3 3-3-1.343-3-3 1.343-3 3-3zm0 1.5c-0.828 0-1.5 0.672-1.5 1.5s0.672 1.5 1.5 1.5 1.5-0.672 1.5-1.5-0.672-1.5-1.5-1.5zM6.5 3c1.657 0 3 1.343 3 3s-1.343 3-3 3-3-1.343-3-3 1.343-3 3-3zm11 0c1.657 0 3 1.343 3 3s-1.343 3-3 3-3-1.343-3-3 1.343-3 3-3zm-11 1.5C5.672 4.5 5 5.172 5 6s0.672 1.5 1.5 1.5S8 6.828 8 6 7.328 4.5 6.5 4.5zm11 0C16.672 4.5 16 5.172 16 6s0.672 1.5 1.5 1.5S19 6.828 19 6s-0.672-1.5-1.5-1.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="25dp" android:height="24dp" android:viewportWidth="25" android:viewportHeight="24">
<path android:pathData="M12.168 2c1.52 0 2.752 1.232 2.752 2.752V19.25c0 1.52-1.232 2.752-2.752 2.752-1.52 0-2.752-1.232-2.752-2.752V4.752C9.416 3.232 10.648 2 12.168 2zm7 5c1.52 0 2.752 1.232 2.752 2.752v9.498c0 1.52-1.232 2.752-2.752 2.752-1.52 0-2.752-1.232-2.752-2.752V9.752c0-1.52 1.232-2.752 2.752-2.752zm-14 5c1.52 0 2.752 1.232 2.752 2.752v4.498c0 1.52-1.232 2.752-2.752 2.752-1.52 0-2.752-1.232-2.752-2.752v-4.498c0-1.52 1.232-2.752 2.752-2.752z" 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="25dp" android:height="24dp" android:viewportWidth="25" android:viewportHeight="24">
<path android:pathData="M12.168 2c1.52 0 2.752 1.232 2.752 2.752V19.25c0 1.52-1.232 2.752-2.752 2.752-1.52 0-2.752-1.232-2.752-2.752V4.752C9.416 3.232 10.648 2 12.168 2zm7 5c1.52 0 2.752 1.232 2.752 2.752v9.498c0 1.52-1.232 2.752-2.752 2.752-1.52 0-2.752-1.232-2.752-2.752V9.752c0-1.52 1.232-2.752 2.752-2.752zm-14 5c1.52 0 2.752 1.232 2.752 2.752v4.498c0 1.52-1.232 2.752-2.752 2.752-1.52 0-2.752-1.232-2.752-2.752v-4.498c0-1.52 1.232-2.752 2.752-2.752zm7-8.5c-0.691 0-1.252 0.56-1.252 1.252V19.25c0 0.692 0.56 1.252 1.252 1.252 0.691 0 1.252-0.56 1.252-1.252V4.752c0-0.691-0.56-1.252-1.252-1.252zm7 5c-0.691 0-1.252 0.56-1.252 1.252v9.498c0 0.692 0.56 1.252 1.252 1.252 0.691 0 1.252-0.56 1.252-1.252V9.752c0-0.692-0.56-1.252-1.252-1.252zm-14 5c-0.692 0-1.252 0.56-1.252 1.252v4.498c0 0.692 0.56 1.252 1.252 1.252 0.691 0 1.252-0.56 1.252-1.252v-4.498c0-0.692-0.56-1.252-1.252-1.252z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!--~ Copyright (c) 2022. ~ Microsoft Corporation. All rights reserved.-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_fluent_poll_24_filled" android:state_activated="true"/>
<item android:drawable="@drawable/ic_fluent_poll_24_filled" android:state_checked="true"/>
<item android:drawable="@drawable/ic_fluent_poll_24_filled" android:state_selected="true"/>
<item android:drawable="@drawable/ic_fluent_poll_24_regular"/>
</selector>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="250dp"
android:layout_height="150dp">
<ImageView
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"/>
<ProgressBar
android:id="@+id/progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
style="?android:progressBarStyleHorizontal"/>
</FrameLayout>

View File

@ -5,6 +5,17 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -46,8 +57,7 @@
<EditText
android:id="@+id/toot_text"
android:layout_width="match_parent"
android:layout_height="0px"
android:layout_weight="1"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
@ -57,10 +67,14 @@
android:background="@null"
android:inputType="textMultiLine|textCapSentences"/>
<View
<LinearLayout
android:id="@+id/attachments"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#20000000"/>
android:layout_height="wrap_content"
android:orientation="vertical"/>
</LinearLayout>
</ScrollView>
<LinearLayout
android:layout_width="match_parent"
@ -68,18 +82,58 @@
android:orientation="horizontal"
android:gravity="center_vertical"
android:background="@color/gray_25"
android:elevation="2dp"
android:outlineProvider="bounds"
android:paddingLeft="16dp"
android:paddingRight="16dp">
android:paddingRight="16dp"
android:layoutDirection="locale">
<ImageButton
android:id="@+id/btn_media"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="24dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:padding="0px"
android:src="@drawable/ic_fluent_image_24_regular"/>
<ImageButton
android:id="@+id/btn_poll"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="24dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:padding="0px"
android:src="@drawable/ic_fluent_poll_24_selector"/>
<ImageButton
android:id="@+id/btn_emoji"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="24dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:padding="0px"
android:src="@drawable/ic_fluent_emoji_24_selector"/>
<View
<ImageButton
android:id="@+id/btn_spoiler"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="24dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:padding="0px"
android:src="@drawable/ic_fluent_chat_warning_24_selector"/>
<ImageButton
android:id="@+id/btn_visibility"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="24dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:padding="0px"
android:src="@drawable/ic_fluent_people_community_24_regular"/>
<Space
android:layout_width="0px"
android:layout_height="1px"
android:layout_weight="1"/>

View File

@ -12,10 +12,13 @@
<color name="gray_25">#FCFCFD</color>
<color name="gray_50t">#CCF9FAFB</color>
<color name="gray_50">#F9FAFB</color>
<color name="gray_100">#F2F4F7</color>
<color name="gray_800">#282C37</color>
<color name="gray_500">#667085</color>
<color name="gray_800_alpha50">#80282C37</color>
<color name="text_primary">@color/gray_800</color>
<color name="text_secondary">@color/gray_500</color>
<color name="secondary">#E9EDF2</color>
@ -23,6 +26,7 @@
<color name="text_secondary_alpha50">#80667085</color>
<color name="actionbar_bg">#FAFBFC</color>
<color name="navigation_bar_bg">#000</color>
<color name="highlight_over_dark">#80FFFFFF</color>
<color name="favorite_selected">#FF9F0A</color>
<color name="boost_selected">#79BD9A</color>

View File

@ -28,4 +28,9 @@
<string name="share_toot_title">Share toot</string>
<string name="settings">Settings</string>
<string name="publish">Publish</string>
<string name="discard_draft">Discard draft?</string>
<string name="discard">Discard</string>
<string name="cancel">Cancel</string>
<string name="publishing">Your toot is being tooted</string>
</resources>

View File

@ -1,17 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.Mastodon" parent="Theme.AppKit.Light">
<!-- needed to disable scrim on API 29+ -->
<item name="android:enforceNavigationBarContrast">false</item>
<item name="android:enforceStatusBarContrast">false</item>
<item name="android:enforceNavigationBarContrast" tools:ignore="NewApi">false</item>
<item name="android:enforceStatusBarContrast" tools:ignore="NewApi">false</item>
<item name="android:windowLightStatusBar">true</item>
<item name="android:windowLightNavigationBar">true</item>
<item name="android:windowLightNavigationBar" tools:ignore="NewApi">true</item>
<item name="android:windowBackground">@color/white</item>
<item name="android:statusBarColor">@color/actionbar_bg</item>
<item name="android:navigationBarColor">@color/navigation_bar_bg</item>
<item name="android:colorAccent">@color/gray_800</item>
<item name="android:colorPrimary">@color/gray_800</item>
<item name="android:colorBackground">@color/gray_100</item>
<item name="android:actionBarTheme">@style/Theme.Mastodon.Toolbar</item>
<item name="android:buttonStyle">@style/Widget.Mastodon.Button</item>
<item name="android:alertDialogTheme">@style/Theme.Mastodon.Dialog.Alert</item>
</style>
<style name="Theme.Mastodon.Toolbar" parent="android:ThemeOverlay.Material.ActionBar">
@ -20,6 +23,53 @@
<item name="android:textColorSecondary">@color/gray_800</item>
</style>
<style name="Widget.Mastodon.Button" parent="android:Widget.Material.Button">
<item name="android:textAllCaps">false</item>
<item name="android:background">@drawable/bg_button</item>
<item name="android:textAppearance">@style/m3_label_large</item>
<item name="android:textColor">@color/gray_50</item>
<item name="android:minHeight">36dp</item>
<item name="android:minWidth">0px</item>
</style>
<style name="Theme.Mastodon.Dialog.Alert" parent="android:Theme.Material.Light.Dialog.Alert">
<item name="android:windowTitleStyle">@style/alert_title</item>
<item name="android:dialogPreferredPadding">24dp</item>
<item name="android:windowBackground">@drawable/bg_alert</item>
<item name="android:colorBackground">@color/gray_100</item>
<item name="android:buttonBarStyle">@style/Widget.Mastodon.ButtonBar</item>
<item name="android:buttonBarButtonStyle">@style/Widget.Mastodon.ButtonBarButton</item>
</style>
<style name="Widget.Mastodon.ButtonBar" parent="android:Widget.Material.Light.ButtonBar.AlertDialog">
<!-- <item name="android:layout_marginEnd">4dp</item>-->
<!-- <item name="android:layout_marginStart">12dp</item>-->
<!-- <item name="android:layout_marginTop">20dp</item>-->
<!-- <item name="android:layout_marginBottom">20dp</item>-->
<!-- <item name="android:paddingEnd">4dp</item>-->
<!-- <item name="android:paddingStart">12dp</item>-->
<!-- <item name="android:paddingTop">20dp</item>-->
<!-- <item name="android:paddingBottom">20dp</item>-->
</style>
<style name="Widget.Mastodon.ButtonBarButton" parent="android:Widget.Material.Button.Borderless">
<item name="android:textAllCaps">false</item>
<item name="android:layout_marginEnd">8dp</item>
<item name="android:minHeight">40dp</item>
<item name="android:minWidth">0px</item>
<item name="android:background">@drawable/bg_alert_button</item>
<item name="android:textAppearance">@style/m3_label_large</item>
<item name="android:textColor">@color/text_primary</item>
</style>
<style name="alert_title">
<item name="android:textColor">@color/text_primary</item>
<item name="android:textSize">24dp</item>
<item name="android:minHeight">38dp</item>
<item name="android:gravity">bottom</item>
</style>
<style name="m3_body_large">
<item name="android:textSize">16dp</item>
<item name="android:textColor">@color/text_primary</item>