Better toot layouts, char counter in compose

This commit is contained in:
Grishka
2022-02-01 08:56:13 +03:00
parent a4a514d37a
commit b9bdf7caec
33 changed files with 2744 additions and 140 deletions

View File

@@ -1,6 +1,8 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
@@ -9,8 +11,10 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.DisplayItemsParent;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.PhotoStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
@@ -26,6 +30,7 @@ import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends BaseRecyclerFragment<T> implements PhotoViewerHost{
@@ -204,6 +209,26 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
currentPhotoViewer.offsetView(-dx, -dy);
}
});
list.addItemDecoration(new RecyclerView.ItemDecoration(){
private Paint paint=new Paint();
{
paint.setColor(0xFFD0D5DD);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(V.dp(1));
}
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i);
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
if(holder instanceof FooterStatusDisplayItem.Holder){
float y=child.getY()+child.getHeight()-V.dp(.5f);
c.drawLine(child.getX(), y, child.getX()+child.getWidth(), y, paint);
}
}
}
});
}
protected int getMainAdapterOffset(){

View File

@@ -0,0 +1,191 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.graphics.Outline;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
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.ViewOutlineProvider;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import com.twitter.twittertext.Regex;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
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.Status;
import java.text.BreakIterator;
import java.util.UUID;
import java.util.regex.Pattern;
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 ComposeFragment extends ToolbarFragment{
private static final Pattern MENTION_PATTERN=Pattern.compile("(^|[^\\/\\w])@(([a-z0-9_]+)@[a-z0-9\\.\\-]+[a-z0-9]+)", Pattern.CASE_INSENSITIVE);
private static final String VALID_URL_PATTERN_STRING =
"(" + // $1 total match
"(" + Regex.URL_VALID_PRECEDING_CHARS + ")" + // $2 Preceding character
"(" + // $3 URL
"(https?://)" + // $4 Protocol (optional)
"(" + Regex.URL_VALID_DOMAIN + ")" + // $5 Domain(s)
"(?::(" + Regex.URL_VALID_PORT_NUMBER + "))?" + // $6 Port number (optional)
"(/" +
Regex.URL_VALID_PATH + "*+" +
")?" + // $7 URL Path and anchor
"(\\?" + Regex.URL_VALID_URL_QUERY_CHARS + "*" + // $8 Query String
Regex.URL_VALID_URL_QUERY_ENDING_CHARS + ")?" +
")" +
")";
private static final Pattern URL_PATTERN=Pattern.compile(VALID_URL_PATTERN_STRING, Pattern.CASE_INSENSITIVE);
private final BreakIterator breakIterator=BreakIterator.getCharacterInstance();
private TextView selfName, selfUsername;
private ImageView selfAvatar;
private Account self;
private String instanceDomain;
private EditText mainEditText;
private TextView charCounter;
private String accountID;
private int charCount, charLimit;
private MenuItem publishButton;
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setHasOptionsMenu(true);
accountID=getArguments().getString("account");
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
charLimit=session.tootCharLimit;
if(charLimit==0)
charLimit=500;
self=session.self;
instanceDomain=session.domain;
}
@Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
View view=inflater.inflate(R.layout.fragment_compose, container, false);
mainEditText=view.findViewById(R.id.toot_text);
charCounter=view.findViewById(R.id.char_counter);
charCounter.setText(String.valueOf(charLimit));
selfName=view.findViewById(R.id.name);
selfUsername=view.findViewById(R.id.username);
selfAvatar=view.findViewById(R.id.avatar);
selfName.setText(self.displayName);
selfUsername.setText('@'+self.username+'@'+instanceDomain);
ViewImageLoader.load(selfAvatar, null, new UrlImageLoaderRequest(self.avatar));
ViewOutlineProvider roundCornersOutline=new ViewOutlineProvider(){
@Override
public void getOutline(View view, Outline outline){
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), V.dp(12));
}
};
selfAvatar.setOutlineProvider(roundCornersOutline);
selfAvatar.setClipToOutline(true);
return view;
}
@Override
public void onResume(){
super.onResume();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
view.postDelayed(()->{
mainEditText.requestFocus();
imm.showSoftInput(mainEditText, 0);
}, 100);
mainEditText.addTextChangedListener(new TextWatcher(){
@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){
updateCharCounter(s);
}
});
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
publishButton=menu.add("TOOT!");
publishButton.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;
}
private void updateCharCounter(CharSequence text){
String countableText=MENTION_PATTERN.matcher(URL_PATTERN.matcher(text).replaceAll("$2xxxxxxxxxxxxxxxxxxxxxxx")).replaceAll("$1@$3");
breakIterator.setText(countableText);
charCount=0;
while(breakIterator.next()!=BreakIterator.DONE){
charCount++;
}
charCounter.setText(String.valueOf(charLimit-charCount));
updatePublishButtonState();
}
private void updatePublishButtonState(){
publishButton.setEnabled(charCount>0 && charCount<=charLimit);
}
}

View File

@@ -1,88 +0,0 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.os.Bundle;
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.EditText;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.CreateStatus;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.model.Status;
import java.util.UUID;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.ToolbarFragment;
public class CreateTootFragment extends ToolbarFragment{
private EditText mainEditText;
private String accountID;
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setHasOptionsMenu(true);
accountID=getArguments().getString("account");
}
@Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
View view=inflater.inflate(R.layout.fragment_new_toot, container, false);
mainEditText=view.findViewById(R.id.toot_text);
return view;
}
@Override
public void onResume(){
super.onResume();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
view.postDelayed(()->{
mainEditText.requestFocus();
imm.showSoftInput(mainEditText, 0);
}, 100);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
menu.add("TOOT!").setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
}
@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(CreateTootFragment.this);
E.post(new StatusCreatedEvent(result));
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.exec(accountID);
return true;
}
}

View File

@@ -49,4 +49,9 @@ public class HomeFragment extends AppKitFragment{
super.onHiddenChanged(hidden);
homeTimelineFragment.onHiddenChanged(hidden);
}
@Override
public boolean wantsLightStatusBar(){
return true;
}
}

View File

@@ -5,6 +5,8 @@ import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageButton;
import com.squareup.otto.Subscribe;
@@ -23,6 +25,11 @@ import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
public class HomeTimelineFragment extends StatusListFragment{
private ImageButton fab;
public HomeTimelineFragment(){
setListLayoutId(R.layout.recycler_fragment_with_fab);
}
@Override
public void onAttach(Activity activity){
@@ -44,6 +51,13 @@ public class HomeTimelineFragment extends StatusListFragment{
.exec(accountID);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
fab.setOnClickListener(this::onFabClick);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.home, menu);
@@ -55,7 +69,7 @@ public class HomeTimelineFragment extends StatusListFragment{
args.putString("account", accountID);
int id=item.getItemId();
if(id==R.id.new_toot){
Nav.go(getActivity(), CreateTootFragment.class, args);
Nav.go(getActivity(), ComposeFragment.class, args);
}else if(id==R.id.notifications){
Nav.go(getActivity(), NotificationsFragment.class, args);
}else if(id==R.id.my_profile){
@@ -81,4 +95,10 @@ public class HomeTimelineFragment extends StatusListFragment{
public void onStatusCreated(StatusCreatedEvent ev){
prependItems(Collections.singletonList(ev.status));
}
private void onFabClick(View v){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), ComposeFragment.class, args);
}
}