Compose: language selection

This commit is contained in:
Grishka 2023-05-12 22:21:21 +03:00
parent 89501271ce
commit 15883f2138
9 changed files with 512 additions and 32 deletions

View File

@ -1,19 +1,29 @@
package org.joinmastodon.android.api.session; package org.joinmastodon.android.api.session;
import android.util.Log;
import org.joinmastodon.android.api.CacheController; import org.joinmastodon.android.api.CacheController;
import org.joinmastodon.android.api.MastodonAPIController; import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.PushSubscriptionManager; import org.joinmastodon.android.api.PushSubscriptionManager;
import org.joinmastodon.android.api.StatusInteractionController; import org.joinmastodon.android.api.StatusInteractionController;
import org.joinmastodon.android.api.requests.accounts.GetPreferences;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Application; import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.PushSubscription; import org.joinmastodon.android.model.PushSubscription;
import org.joinmastodon.android.model.Token; import org.joinmastodon.android.model.Token;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.Consumer;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
public class AccountSession{ public class AccountSession{
private static final String TAG="AccountSession";
public Token token; public Token token;
public Account self; public Account self;
public String domain; public String domain;
@ -29,6 +39,7 @@ public class AccountSession{
public List<Filter> wordFilters=new ArrayList<>(); public List<Filter> wordFilters=new ArrayList<>();
public String pushAccountID; public String pushAccountID;
public AccountActivationInfo activationInfo; public AccountActivationInfo activationInfo;
public Preferences preferences;
private transient MastodonAPIController apiController; private transient MastodonAPIController apiController;
private transient StatusInteractionController statusInteractionController; private transient StatusInteractionController statusInteractionController;
private transient CacheController cacheController; private transient CacheController cacheController;
@ -77,4 +88,22 @@ public class AccountSession{
public String getFullUsername(){ public String getFullUsername(){
return '@'+self.username+'@'+domain; return '@'+self.username+'@'+domain;
} }
public void reloadPreferences(Consumer<Preferences> callback){
new GetPreferences()
.setCallback(new Callback<>(){
@Override
public void onSuccess(Preferences result){
preferences=result;
callback.accept(result);
AccountSessionManager.getInstance().writeAccountsFile();
}
@Override
public void onError(ErrorResponse error){
Log.w(TAG, "Failed to load preferences for account "+getID()+": "+error);
}
})
.exec(getID());
}
} }

View File

@ -73,6 +73,7 @@ import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher; import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewcontrollers.ComposeAutocompleteViewController; import org.joinmastodon.android.ui.viewcontrollers.ComposeAutocompleteViewController;
import org.joinmastodon.android.ui.viewcontrollers.ComposeLanguageAlertViewController;
import org.joinmastodon.android.ui.viewcontrollers.ComposeMediaViewController; import org.joinmastodon.android.ui.viewcontrollers.ComposeMediaViewController;
import org.joinmastodon.android.ui.viewcontrollers.ComposePollViewController; import org.joinmastodon.android.ui.viewcontrollers.ComposePollViewController;
import org.joinmastodon.android.ui.views.ComposeEditText; import org.joinmastodon.android.ui.views.ComposeEditText;
@ -122,7 +123,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private String accountID; private String accountID;
private int charCount, charLimit, trimmedCharCount; private int charCount, charLimit, trimmedCharCount;
private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn; private ImageButton mediaBtn, pollBtn, emojiBtn, spoilerBtn, languageBtn;
private TextView replyText; private TextView replyText;
private Button visibilityBtn; private Button visibilityBtn;
private LinearLayout bottomBar; private LinearLayout bottomBar;
@ -142,6 +143,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private StatusPrivacy statusVisibility=StatusPrivacy.PUBLIC; private StatusPrivacy statusVisibility=StatusPrivacy.PUBLIC;
private ComposeAutocompleteSpan currentAutocompleteSpan; private ComposeAutocompleteSpan currentAutocompleteSpan;
private FrameLayout mainEditTextWrap; private FrameLayout mainEditTextWrap;
private ComposeLanguageAlertViewController.SelectedOption postLang;
private ComposeAutocompleteViewController autocompleteViewController; private ComposeAutocompleteViewController autocompleteViewController;
private ComposePollViewController pollViewController=new ComposePollViewController(this); private ComposePollViewController pollViewController=new ComposePollViewController(this);
@ -190,9 +192,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
else else
charLimit=500; charLimit=500;
if(editingStatus==null)
loadDefaultStatusVisibility(savedInstanceState);
setTitle(editingStatus==null ? R.string.new_post : R.string.edit_post); setTitle(editingStatus==null ? R.string.new_post : R.string.edit_post);
if(savedInstanceState!=null)
postLang=Parcels.unwrap(savedInstanceState.getParcelable("postLang"));
} }
@Override @Override
@ -251,12 +253,14 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
emojiBtn=view.findViewById(R.id.btn_emoji); emojiBtn=view.findViewById(R.id.btn_emoji);
spoilerBtn=view.findViewById(R.id.btn_spoiler); spoilerBtn=view.findViewById(R.id.btn_spoiler);
visibilityBtn=view.findViewById(R.id.btn_visibility); visibilityBtn=view.findViewById(R.id.btn_visibility);
languageBtn=view.findViewById(R.id.btn_language);
replyText=view.findViewById(R.id.reply_text); replyText=view.findViewById(R.id.reply_text);
mediaBtn.setOnClickListener(v->openFilePicker()); mediaBtn.setOnClickListener(v->openFilePicker());
pollBtn.setOnClickListener(v->togglePoll()); pollBtn.setOnClickListener(v->togglePoll());
emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText)); emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText));
spoilerBtn.setOnClickListener(v->toggleSpoiler()); spoilerBtn.setOnClickListener(v->toggleSpoiler());
languageBtn.setOnClickListener(v->showLanguageAlert());
visibilityBtn.setOnClickListener(this::onVisibilityClick); visibilityBtn.setOnClickListener(this::onVisibilityClick);
Drawable arrow=getResources().getDrawable(R.drawable.ic_baseline_arrow_drop_down_18, getActivity().getTheme()).mutate(); Drawable arrow=getResources().getDrawable(R.drawable.ic_baseline_arrow_drop_down_18, getActivity().getTheme()).mutate();
arrow.setTint(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurface)); arrow.setTint(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurface));
@ -343,6 +347,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
mediaViewController.onSaveInstanceState(outState); mediaViewController.onSaveInstanceState(outState);
outState.putBoolean("hasSpoiler", hasSpoiler); outState.putBoolean("hasSpoiler", hasSpoiler);
outState.putSerializable("visibility", statusVisibility); outState.putSerializable("visibility", statusVisibility);
outState.putParcelable("postLang", Parcels.wrap(postLang));
if(currentAutocompleteSpan!=null){ if(currentAutocompleteSpan!=null){
Editable e=mainEditText.getText(); Editable e=mainEditText.getText();
outState.putInt("autocompleteStart", e.getSpanStart(currentAutocompleteSpan)); outState.putInt("autocompleteStart", e.getSpanStart(currentAutocompleteSpan));
@ -358,6 +363,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
@Override @Override
public void onViewCreated(View view, Bundle savedInstanceState){ public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState); super.onViewCreated(view, savedInstanceState);
if(editingStatus==null)
loadDefaultStatusVisibility(savedInstanceState);
contentView.setSizeListener(emojiKeyboard::onContentViewSizeChanged); contentView.setSizeListener(emojiKeyboard::onContentViewSizeChanged);
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class); InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
mainEditText.requestFocus(); mainEditText.requestFocus();
@ -650,6 +657,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(hasSpoiler && spoilerEdit.length()>0){ if(hasSpoiler && spoilerEdit.length()>0){
req.spoilerText=spoilerEdit.getText().toString(); req.spoilerText=spoilerEdit.getText().toString();
} }
if(postLang!=null){
req.language=postLang.locale.toLanguageTag();
}
if(uuid==null) if(uuid==null)
uuid=UUID.randomUUID().toString(); uuid=UUID.randomUUID().toString();
@ -867,45 +877,43 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
menu.show(); menu.show();
} }
private void loadDefaultStatusVisibility(Bundle savedInstanceState) { private void loadDefaultStatusVisibility(Bundle savedInstanceState){
if(getArguments().containsKey("replyTo")){ if(getArguments().containsKey("replyTo")){
replyTo=Parcels.unwrap(getArguments().getParcelable("replyTo")); replyTo=Parcels.unwrap(getArguments().getParcelable("replyTo"));
statusVisibility = replyTo.visibility; statusVisibility=replyTo.visibility;
} }
// A saved privacy setting from a previous compose session wins over the reply visibility // A saved privacy setting from a previous compose session wins over the reply visibility
if(savedInstanceState !=null){ if(savedInstanceState!=null){
statusVisibility = (StatusPrivacy) savedInstanceState.getSerializable("visibility"); statusVisibility=(StatusPrivacy) savedInstanceState.getSerializable("visibility");
} }
new GetPreferences() Preferences prevPrefs=AccountSessionManager.getInstance().getAccount(accountID).preferences;
.setCallback(new Callback<>(){ if(prevPrefs!=null){
@Override applyPreferencesForPostVisibility(prevPrefs, savedInstanceState);
public void onSuccess(Preferences result){ }
// Only override the reply visibility if our preference is more private AccountSessionManager.getInstance().getAccount(accountID).reloadPreferences(prefs->{
if (result.postingDefaultVisibility.isLessVisibleThan(statusVisibility)) { applyPreferencesForPostVisibility(prefs, savedInstanceState);
// Map unlisted from the API onto public, because we don't have unlisted in the UI });
statusVisibility = switch (result.postingDefaultVisibility) { }
case PUBLIC, UNLISTED -> StatusPrivacy.PUBLIC;
case PRIVATE -> StatusPrivacy.PRIVATE;
case DIRECT -> StatusPrivacy.DIRECT;
};
}
// A saved privacy setting from a previous compose session wins over all private void applyPreferencesForPostVisibility(Preferences prefs, Bundle savedInstanceState){
if(savedInstanceState !=null){ // Only override the reply visibility if our preference is more private
statusVisibility = (StatusPrivacy) savedInstanceState.getSerializable("visibility"); if(prefs.postingDefaultVisibility.isLessVisibleThan(statusVisibility)){
} // Map unlisted from the API onto public, because we don't have unlisted in the UI
statusVisibility=switch(prefs.postingDefaultVisibility){
case PUBLIC, UNLISTED -> StatusPrivacy.PUBLIC;
case PRIVATE -> StatusPrivacy.PRIVATE;
case DIRECT -> StatusPrivacy.DIRECT;
};
}
updateVisibilityIcon (); // A saved privacy setting from a previous compose session wins over all
} if(savedInstanceState!=null){
statusVisibility=(StatusPrivacy) savedInstanceState.getSerializable("visibility");
}
@Override updateVisibilityIcon();
public void onError(ErrorResponse error){
Log.w(TAG, "Unable to get user preferences to set default post privacy");
}
})
.exec(accountID);
} }
private void updateVisibilityIcon(){ private void updateVisibilityIcon(){
@ -1037,4 +1045,19 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
public void addFakeMediaAttachment(Uri uri, String description){ public void addFakeMediaAttachment(Uri uri, String description){
mediaViewController.addFakeMediaAttachment(uri, description); mediaViewController.addFakeMediaAttachment(uri, description);
} }
private void showLanguageAlert(){
Preferences prefs=AccountSessionManager.getInstance().getAccount(accountID).preferences;
ComposeLanguageAlertViewController vc=new ComposeLanguageAlertViewController(getActivity(), prefs!=null ? prefs.postingDefaultLanguage : null, postLang, mainEditText.getText().toString());
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.language)
.setView(vc.getView())
.setPositiveButton(R.string.ok, (dialog, which)->setPostLanguage(vc.getSelectedOption()))
.setNegativeButton(R.string.cancel, null)
.show();
}
private void setPostLanguage(ComposeLanguageAlertViewController.SelectedOption language){
postLang=language;
}
} }

View File

@ -0,0 +1,333 @@
package org.joinmastodon.android.ui.viewcontrollers;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.os.Build;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.textclassifier.TextClassificationManager;
import android.view.textclassifier.TextLanguage;
import android.widget.Checkable;
import android.widget.CheckedTextView;
import android.widget.RadioButton;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.CheckableLinearLayout;
import org.parceler.Parcel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class ComposeLanguageAlertViewController{
private Context context;
private UsableRecyclerView list;
private List<LocaleInfo> allLocales;
private List<SpecialLocaleInfo> specialLocales=new ArrayList<>();
private int selectedIndex=0;
private Locale selectedLocale;
public ComposeLanguageAlertViewController(Context context, String preferred, SelectedOption previouslySelected, String postText){
this.context=context;
allLocales=Arrays.stream(Locale.getAvailableLocales())
.map(Locale::getLanguage)
.distinct()
.map(code->{
Locale l=Locale.forLanguageTag(code);
String name=l.getDisplayLanguage(Locale.getDefault());
return new LocaleInfo(l, capitalizeLanguageName(name));
})
.sorted(Comparator.comparing(a->a.displayName))
.collect(Collectors.toList());
if(!TextUtils.isEmpty(preferred)){
Locale l=Locale.forLanguageTag(preferred);
SpecialLocaleInfo pref=new SpecialLocaleInfo();
pref.locale=l;
pref.displayName=capitalizeLanguageName(l.getDisplayLanguage(Locale.getDefault()));
pref.title=context.getString(R.string.language_default);
specialLocales.add(pref);
}
Locale def=Locale.forLanguageTag(Locale.getDefault().getLanguage());
if(!def.getLanguage().equals(preferred)){
SpecialLocaleInfo d=new SpecialLocaleInfo();
d.locale=def;
d.displayName=capitalizeLanguageName(def.getDisplayName());
d.title=context.getString(R.string.language_system);
specialLocales.add(d);
}
if(Build.VERSION.SDK_INT>=29 && !TextUtils.isEmpty(postText)){
SpecialLocaleInfo detected=new SpecialLocaleInfo();
detected.displayName=context.getString(R.string.language_detecting);
detected.enabled=false;
specialLocales.add(detected);
detectLanguage(detected, postText);
}
if(previouslySelected!=null){
if((previouslySelected.index<specialLocales.size() && Objects.equals(previouslySelected.locale, specialLocales.get(previouslySelected.index).locale)) ||
(previouslySelected.index<specialLocales.size()+allLocales.size() && Objects.equals(previouslySelected.locale, allLocales.get(previouslySelected.index-specialLocales.size()).locale))){
selectedIndex=previouslySelected.index;
selectedLocale=previouslySelected.locale;
}
}else{
selectedLocale=specialLocales.get(0).locale;
}
list=new UsableRecyclerView(context);
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
adapter.addAdapter(new SpecialLanguagesAdapter());
adapter.addAdapter(new AllLocalesAdapter());
list.setAdapter(adapter);
list.setLayoutManager(new LinearLayoutManager(context));
list.addItemDecoration(new DividerItemDecoration(context, R.attr.colorM3OutlineVariant, 1, 16, 16, vh->vh.getAbsoluteAdapterPosition()==specialLocales.size()-1));
list.addItemDecoration(new RecyclerView.ItemDecoration(){
private Paint paint=new Paint();
{
paint.setColor(UiUtils.getThemeColor(context, R.attr.colorM3OutlineVariant));
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(V.dp(1));
}
@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
if(parent.canScrollVertically(1)){
float y=parent.getHeight()-paint.getStrokeWidth()/2f;
c.drawLine(0, y, parent.getWidth(), y, paint);
}
if(parent.canScrollVertically(-1)){
float y=paint.getStrokeWidth()/2f;
c.drawLine(0, y, parent.getWidth(), y, paint);
}
}
});
if(previouslySelected!=null && selectedIndex>0){
list.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
list.getViewTreeObserver().removeOnPreDrawListener(this);
if(list.findViewHolderForAdapterPosition(selectedIndex)==null)
list.scrollToPosition(selectedIndex);
return true;
}
});
}
}
@RequiresApi(api = Build.VERSION_CODES.Q)
private void detectLanguage(SpecialLocaleInfo info, String text){
MastodonAPIController.runInBackground(()->{
TextLanguage lang=context.getSystemService(TextClassificationManager.class).getTextClassifier().detectLanguage(new TextLanguage.Request.Builder(text).build());
list.post(()->{
SpecialLanguageViewHolder holder=(SpecialLanguageViewHolder) list.findViewHolderForAdapterPosition(specialLocales.indexOf(info));
if(lang.getLocaleHypothesisCount()==0 || lang.getConfidenceScore(lang.getLocale(0))<0.75f){
info.displayName=context.getString(R.string.language_cant_detect);
}else{
Locale locale=lang.getLocale(0).toLocale();
info.locale=locale;
info.displayName=capitalizeLanguageName(locale.getDisplayName(Locale.getDefault()));
info.title=context.getString(R.string.language_detected);
info.enabled=true;
if(holder!=null)
UiUtils.beginLayoutTransition(holder.view);
}
if(holder!=null)
holder.rebind();
});
});
}
public View getView(){
return list;
}
// Needed because in some languages (e.g. Slavic ones) these names returned by the system start with a lowercase letter
private String capitalizeLanguageName(String name){
return name.substring(0, 1).toUpperCase(Locale.getDefault())+name.substring(1);
}
public SelectedOption getSelectedOption(){
return new SelectedOption(selectedIndex, selectedLocale);
}
private void selectItem(int index){
if(index==selectedIndex)
return;
if(selectedIndex!=-1){
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(selectedIndex);
if(holder!=null && holder.itemView instanceof Checkable checkable)
checkable.setChecked(false);
}
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(index);
if(holder!=null && holder.itemView instanceof Checkable checkable)
checkable.setChecked(true);
selectedIndex=index;
}
private class AllLocalesAdapter extends RecyclerView.Adapter<SimpleLanguageViewHolder>{
@NonNull
@Override
public SimpleLanguageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new SimpleLanguageViewHolder();
}
@Override
public void onBindViewHolder(@NonNull SimpleLanguageViewHolder holder, int position){
holder.bind(allLocales.get(position));
}
@Override
public int getItemCount(){
return allLocales.size();
}
@Override
public int getItemViewType(int position){
return 1;
}
}
private class SimpleLanguageViewHolder extends BindableViewHolder<LocaleInfo> implements UsableRecyclerView.Clickable{
private final CheckedTextView text;
public SimpleLanguageViewHolder(){
super(context, R.layout.item_alert_single_choice_1line, list);
text=(CheckedTextView) itemView;
text.setCompoundDrawablesRelativeWithIntrinsicBounds(new RadioButton(context).getButtonDrawable(), null, null, null);
}
@Override
public void onBind(LocaleInfo item){
text.setText(item.displayName);
text.setChecked(selectedIndex==getAbsoluteAdapterPosition());
}
@Override
public void onClick(){
selectItem(getAbsoluteAdapterPosition());
selectedLocale=item.locale;
}
}
private class SpecialLanguagesAdapter extends RecyclerView.Adapter<SpecialLanguageViewHolder>{
@NonNull
@Override
public SpecialLanguageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new SpecialLanguageViewHolder();
}
@Override
public void onBindViewHolder(@NonNull SpecialLanguageViewHolder holder, int position){
holder.bind(specialLocales.get(position));
}
@Override
public int getItemCount(){
return specialLocales.size();
}
@Override
public int getItemViewType(int position){
return 2;
}
}
private class SpecialLanguageViewHolder extends BindableViewHolder<SpecialLocaleInfo> implements UsableRecyclerView.DisableableClickable{
private final TextView text, title;
private final CheckableLinearLayout view;
public SpecialLanguageViewHolder(){
super(context, R.layout.item_alert_single_choice_2lines, list);
text=findViewById(R.id.text);
title=findViewById(R.id.title);
view=((CheckableLinearLayout) itemView);
findViewById(R.id.radiobutton).setBackground(new RadioButton(context).getButtonDrawable());
}
@Override
public void onBind(SpecialLocaleInfo item){
text.setText(item.displayName);
if(!TextUtils.isEmpty(item.title)){
title.setVisibility(View.VISIBLE);
title.setText(item.title);
}else{
title.setVisibility(View.GONE);
}
text.setEnabled(item.enabled);
view.setEnabled(item.enabled);
view.setChecked(selectedIndex==getAbsoluteAdapterPosition());
}
@Override
public void onClick(){
selectItem(getAbsoluteAdapterPosition());
selectedLocale=item.locale;
}
@Override
public boolean isEnabled(){
return item.enabled;
}
}
private static class LocaleInfo{
public final Locale locale;
public final String displayName;
private LocaleInfo(Locale locale, String displayName){
this.locale=locale;
this.displayName=displayName;
}
}
private static class SpecialLocaleInfo{
public Locale locale;
public String displayName;
public String title;
public boolean enabled=true;
}
@Parcel
public static class SelectedOption{
public int index;
public Locale locale;
public SelectedOption(){}
public SelectedOption(int index, Locale locale){
this.index=index;
this.locale=locale;
}
}
}

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="?colorM3OnSurface" android:state_enabled="true"/>
<item android:color="?colorM3Secondary"/>
</selector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,22Q9.95,22 8.125,21.212Q6.3,20.425 4.938,19.062Q3.575,17.7 2.788,15.875Q2,14.05 2,12Q2,9.925 2.788,8.113Q3.575,6.3 4.938,4.938Q6.3,3.575 8.125,2.787Q9.95,2 12,2Q14.075,2 15.887,2.787Q17.7,3.575 19.062,4.938Q20.425,6.3 21.212,8.113Q22,9.925 22,12Q22,14.05 21.212,15.875Q20.425,17.7 19.062,19.062Q17.7,20.425 15.887,21.212Q14.075,22 12,22ZM15.95,8H18.9Q18.175,6.75 17.087,5.825Q16,4.9 14.6,4.45Q15.05,5.275 15.388,6.162Q15.725,7.05 15.95,8ZM10.1,8H13.9Q13.6,6.9 13.125,5.925Q12.65,4.95 12,4.05Q11.35,4.95 10.875,5.925Q10.4,6.9 10.1,8ZM4.25,14H7.65Q7.575,13.5 7.537,13.012Q7.5,12.525 7.5,12Q7.5,11.475 7.537,10.988Q7.575,10.5 7.65,10H4.25Q4.125,10.5 4.062,10.988Q4,11.475 4,12Q4,12.525 4.062,13.012Q4.125,13.5 4.25,14ZM9.4,19.55Q8.95,18.725 8.613,17.837Q8.275,16.95 8.05,16H5.1Q5.825,17.25 6.912,18.175Q8,19.1 9.4,19.55ZM5.1,8H8.05Q8.275,7.05 8.613,6.162Q8.95,5.275 9.4,4.45Q8,4.9 6.912,5.825Q5.825,6.75 5.1,8ZM12,19.95Q12.65,19.05 13.125,18.075Q13.6,17.1 13.9,16H10.1Q10.4,17.1 10.875,18.075Q11.35,19.05 12,19.95ZM9.65,14H14.35Q14.425,13.5 14.463,13.012Q14.5,12.525 14.5,12Q14.5,11.475 14.463,10.988Q14.425,10.5 14.35,10H9.65Q9.575,10.5 9.538,10.988Q9.5,11.475 9.5,12Q9.5,12.525 9.538,13.012Q9.575,13.5 9.65,14ZM14.6,19.55Q16,19.1 17.087,18.175Q18.175,17.25 18.9,16H15.95Q15.725,16.95 15.388,17.837Q15.05,18.725 14.6,19.55ZM16.35,14H19.75Q19.875,13.5 19.938,13.012Q20,12.525 20,12Q20,11.475 19.938,10.988Q19.875,10.5 19.75,10H16.35Q16.425,10.5 16.462,10.988Q16.5,11.475 16.5,12Q16.5,12.525 16.462,13.012Q16.425,13.5 16.35,14Z"/>
</vector>

View File

@ -388,6 +388,19 @@
android:tooltipText="@string/content_warning" android:tooltipText="@string/content_warning"
android:src="@drawable/ic_compose_cw"/> android:src="@drawable/ic_compose_cw"/>
<ImageButton
android:id="@+id/btn_language"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_marginEnd="12dp"
android:background="@drawable/bg_compose_button"
android:padding="0px"
android:tint="@color/compose_button"
android:tintMode="src_in"
android:contentDescription="@string/language"
android:tooltipText="@string/language"
android:src="@drawable/ic_language_24px"/>
<Space <Space
android:layout_width="0px" android:layout_width="0px"
android:layout_height="1px" android:layout_height="1px"

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="48dp"
android:paddingEnd="24dp"
android:paddingStart="12dp"
android:drawablePadding="12dp"
android:gravity="center_vertical"
android:textAppearance="@style/m3_body_large"
android:textColor="?colorM3OnSurface"
android:singleLine="true"
android:ellipsize="end"
tools:text="Item text">
</CheckedTextView>

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<org.joinmastodon.android.ui.views.CheckableLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="52dp"
android:orientation="horizontal"
android:paddingStart="12dp"
android:paddingEnd="24dp">
<View
android:id="@+id/radiobutton"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center_vertical"
android:duplicateParentState="true"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="12dp"
android:orientation="vertical"
android:gravity="center_vertical">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="16dp"
android:textAppearance="@style/m3_label_medium"
android:textColor="?colorM3OnSurfaceVariant"
android:singleLine="true"
android:ellipsize="end"
tools:text="Title"/>
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="24dp"
android:textAppearance="@style/m3_body_large"
android:textColor="@color/text_on_surface_disableable"
android:singleLine="true"
android:ellipsize="end"
tools:text="Text"/>
</LinearLayout>
</org.joinmastodon.android.ui.views.CheckableLinearLayout>

View File

@ -484,4 +484,10 @@
<string name="compose_autocomplete_emoji_empty">Browse emoji</string> <string name="compose_autocomplete_emoji_empty">Browse emoji</string>
<string name="compose_autocomplete_users_empty">Find who you\'re looking for</string> <string name="compose_autocomplete_users_empty">Find who you\'re looking for</string>
<string name="no_search_results">Could not find anything for these search terms</string> <string name="no_search_results">Could not find anything for these search terms</string>
<string name="language">Language</string>
<string name="language_default">Default</string>
<string name="language_system">System</string>
<string name="language_detecting">Detecting language</string>
<string name="language_cant_detect">Unable to detect language</string>
<string name="language_detected">Detected</string>
</resources> </resources>