Signup flow redesign

This commit is contained in:
Grishka 2022-12-15 19:41:38 +03:00
parent b79ba71228
commit 166401ea18
45 changed files with 1529 additions and 596 deletions

View File

@ -12,10 +12,7 @@ android {
versionCode 45
versionName "1.1.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs "en", "ar-rSA", "bs-rBA", "ca-rES", "cs-rCZ", "de-rDE", "el-rGR", "es-rES",
"eu-rES", "fi-rFI", "fr-rFR", "gl-rES", "hr-rHR", "hy-rAM", "it-rIT", "iw-rIL",
"ja-rJP", "kab", "ko-rKR", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ru-rRU",
"sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "vi-rVN", "zh-rCN", "zh-rTW"
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "kab", "ko-rKR", "nl-rNL", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "vi-rVN", "zh-rCN", "zh-rTW"
}
buildTypes {
@ -91,4 +88,4 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.4-alpha05'
androidTestImplementation 'androidx.test:runner:1.5.0-alpha02'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0-alpha05'
}
}

View File

@ -64,7 +64,7 @@ public class OAuthActivity extends Activity{
.setCallback(new Callback<>(){
@Override
public void onSuccess(Account account){
AccountSessionManager.getInstance().addAccount(instance, token, account, app, true);
AccountSessionManager.getInstance().addAccount(instance, token, account, app, null);
progress.dismiss();
finish();
// not calling restartMainActivity() here on purpose to have it recreated (notice different flags)

View File

@ -0,0 +1,11 @@
package org.joinmastodon.android.api.session;
public class AccountActivationInfo{
public String email;
public long lastEmailConfirmationResend;
public AccountActivationInfo(String email, long lastEmailConfirmationResend){
this.email=email;
this.lastEmailConfirmationResend=lastEmailConfirmationResend;
}
}

View File

@ -28,17 +28,19 @@ public class AccountSession{
public long filtersLastUpdated;
public List<Filter> wordFilters=new ArrayList<>();
public String pushAccountID;
public AccountActivationInfo activationInfo;
private transient MastodonAPIController apiController;
private transient StatusInteractionController statusInteractionController;
private transient CacheController cacheController;
private transient PushSubscriptionManager pushSubscriptionManager;
AccountSession(Token token, Account self, Application app, String domain, boolean activated){
AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){
this.token=token;
this.self=self;
this.domain=domain;
this.app=app;
this.activated=activated;
this.activationInfo=activationInfo;
infoLastUpdated=System.currentTimeMillis();
}

View File

@ -100,9 +100,9 @@ public class AccountSessionManager{
maybeUpdateShortcuts();
}
public void addAccount(Instance instance, Token token, Account self, Application app, boolean active){
public void addAccount(Instance instance, Token token, Account self, Application app, AccountActivationInfo activationInfo){
instances.put(instance.uri, instance);
AccountSession session=new AccountSession(token, self, app, instance.uri, active);
AccountSession session=new AccountSession(token, self, app, instance.uri, activationInfo==null, activationInfo);
sessions.put(session.getID(), session);
lastActiveAccountID=session.getID();
writeAccountsFile();

View File

@ -5,7 +5,6 @@ import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.SpannableString;
import android.text.style.ImageSpan;
import android.text.style.ReplacementSpan;
import android.view.LayoutInflater;
import android.view.View;
@ -15,17 +14,14 @@ import android.view.WindowInsets;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.onboarding.InstanceCatalogSignupFragment;
import org.joinmastodon.android.fragments.onboarding.InstanceChooserLoginFragment;
import org.joinmastodon.android.ui.InterpolatingMotionEffect;
import org.joinmastodon.android.ui.views.SizeListenerFrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager2.widget.ViewPager2;
import me.grishka.appkit.Nav;
import me.grishka.appkit.fragments.AppKitFragment;

View File

@ -1,8 +1,8 @@
package org.joinmastodon.android.fragments.onboarding;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
@ -12,19 +12,21 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.accounts.ResendConfirmationEmail;
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
import org.joinmastodon.android.api.session.AccountActivationInfo;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.SettingsFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.File;
@ -35,40 +37,50 @@ import me.grishka.appkit.Nav;
import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.utils.V;
public class AccountActivationFragment extends AppKitFragment{
public class AccountActivationFragment extends ToolbarFragment{
private String accountID;
private Button btn, backBtn;
private View buttonBar;
private Button openEmailBtn, resendBtn;
private View contentView;
private Handler uiHandler=new Handler(Looper.getMainLooper());
private Runnable pollRunnable=this::tryGetAccount;
private APIRequest currentRequest;
private Runnable resendTimer=this::updateResendTimer;
private long lastResendTime;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
setTitle(R.string.confirm_email_title);
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
lastResendTime=session.activationInfo!=null ? session.activationInfo.lastEmailConfirmationResend : 0;
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){
public View onCreateContentView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){
View view=inflater.inflate(R.layout.fragment_onboarding_activation, container, false);
btn=view.findViewById(R.id.btn_next);
btn.setOnClickListener(v->onButtonClick());
btn.setOnLongClickListener(v->{
openEmailBtn=view.findViewById(R.id.btn_next);
openEmailBtn.setOnClickListener(this::onOpenEmailClick);
openEmailBtn.setOnLongClickListener(v->{
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), SettingsFragment.class, args);
return true;
});
buttonBar=view.findViewById(R.id.button_bar);
view.findViewById(R.id.btn_back).setOnClickListener(v->onBackButtonClick());
resendBtn=view.findViewById(R.id.btn_resend);
resendBtn.setOnClickListener(this::onResendClick);
TextView text=view.findViewById(R.id.subtitle);
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
text.setText(getString(R.string.confirm_email_subtitle, session.activationInfo!=null ? session.activationInfo.email : "?"));
updateResendTimer();
contentView=view;
return view;
}
@ -80,14 +92,32 @@ public class AccountActivationFragment extends AppKitFragment{
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
getToolbar().setBackground(null);
getToolbar().setElevation(0);
}
@Override
protected boolean canGoBack(){
return true;
}
@Override
public void onToolbarNavigationClick(){
new AccountSwitcherSheet(getActivity()).show();
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
contentView.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
@ -111,7 +141,7 @@ public class AccountActivationFragment extends AppKitFragment{
}
}
private void onButtonClick(){
private void onOpenEmailClick(View v){
try{
startActivity(Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, Intent.CATEGORY_APP_EMAIL).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
}catch(ActivityNotFoundException x){
@ -119,12 +149,21 @@ public class AccountActivationFragment extends AppKitFragment{
}
}
private void onBackButtonClick(){
private void onResendClick(View v){
new ResendConfirmationEmail(null)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Object result){
Toast.makeText(getActivity(), R.string.resent_email, Toast.LENGTH_SHORT).show();
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
if(session.activationInfo==null){
session.activationInfo=new AccountActivationInfo("?", System.currentTimeMillis());
}else{
session.activationInfo.lastEmailConfirmationResend=System.currentTimeMillis();
}
lastResendTime=session.activationInfo.lastEmailConfirmationResend;
AccountSessionManager.getInstance().writeAccountsFile();
updateResendTimer();
}
@Override
@ -152,7 +191,7 @@ public class AccountActivationFragment extends AppKitFragment{
AccountSessionManager mgr=AccountSessionManager.getInstance();
AccountSession session=mgr.getAccount(accountID);
mgr.removeAccount(accountID);
mgr.addAccount(mgr.getInstanceInfo(session.domain), session.token, result, session.app, true);
mgr.addAccount(mgr.getInstanceInfo(session.domain), session.token, result, session.app, null);
String newID=mgr.getLastActiveAccountID();
Bundle args=new Bundle();
args.putString("account", newID);
@ -189,4 +228,25 @@ public class AccountActivationFragment extends AppKitFragment{
})
.exec(accountID);
}
@SuppressLint("DefaultLocale")
private void updateResendTimer(){
long sinceResend=System.currentTimeMillis()-lastResendTime;
if(sinceResend>59_000L){
resendBtn.setText(R.string.resend);
resendBtn.setEnabled(true);
return;
}
int seconds=(int)((60_000L-sinceResend)/1000L);
resendBtn.setText(String.format("%s (%d)", getString(R.string.resend), seconds));
if(resendBtn.isEnabled())
resendBtn.setEnabled(false);
resendBtn.postDelayed(resendTimer, 500);
}
@Override
public void onDestroyView(){
super.onDestroyView();
resendBtn.removeCallbacks(resendTimer);
}
}

View File

@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments.onboarding;
import android.app.Activity;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
@ -15,6 +16,7 @@ import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.jsoup.Jsoup;
@ -33,6 +35,7 @@ import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
@ -46,7 +49,7 @@ import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
public class GoogleMadeMeAddThisFragment extends AppKitFragment{
public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
private UsableRecyclerView list;
private MergeRecyclerAdapter adapter;
private Button btn;
@ -60,6 +63,7 @@ public class GoogleMadeMeAddThisFragment extends AppKitFragment{
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setRetainInstance(true);
setTitle(R.string.privacy_policy_title);
}
@Override
@ -82,37 +86,24 @@ public class GoogleMadeMeAddThisFragment extends AppKitFragment{
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
View view=inflater.inflate(R.layout.fragment_onboarding_rules, container, false);
list=view.findViewById(R.id.list);
list.setLayoutManager(new LinearLayoutManager(getActivity()));
View headerView=inflater.inflate(R.layout.item_list_header, list, false);
TextView title=headerView.findViewById(R.id.title);
TextView subtitle=headerView.findViewById(R.id.subtitle);
headerView.findViewById(R.id.step_counter).setVisibility(View.GONE);
title.setText(R.string.privacy_policy_title);
subtitle.setText(R.string.privacy_policy_subtitle);
View headerView=inflater.inflate(R.layout.item_list_header_simple, list, false);
TextView text=headerView.findViewById(R.id.text);
text.setText(R.string.privacy_policy_subtitle);
adapter=new MergeRecyclerAdapter();
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
adapter.addAdapter(itemsAdapter=new ItemsAdapter());
list.setAdapter(adapter);
list.setSelector(null);
list.addItemDecoration(new RecyclerView.ItemDecoration(){
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
if(parent.getChildViewHolder(view) instanceof ItemViewHolder){
outRect.left=outRect.right=V.dp(18.5f);
outRect.top=V.dp(16);
}
}
});
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3SurfaceVariant, 1, 56, 0, DividerItemDecoration.NOT_FIRST));
btn=view.findViewById(R.id.btn_next);
btn.setOnClickListener(v->onButtonClick());
buttonBar=view.findViewById(R.id.button_bar);
view.findViewById(R.id.btn_back).setOnClickListener(v->Nav.finish(this));
return view;
}
@ -120,7 +111,15 @@ public class GoogleMadeMeAddThisFragment extends AppKitFragment{
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
getToolbar().setBackground(null);
getToolbar().setElevation(0);
}
protected void onButtonClick(){
@ -192,24 +191,17 @@ public class GoogleMadeMeAddThisFragment extends AppKitFragment{
}
private class ItemViewHolder extends BindableViewHolder<Item> implements UsableRecyclerView.Clickable{
private final TextView domain, title;
private final ImageView favicon;
private final TextView title;
public ItemViewHolder(){
super(getActivity(), R.layout.item_privacy_policy_link, list);
domain=findViewById(R.id.domain);
title=findViewById(R.id.title);
favicon=findViewById(R.id.favicon);
itemView.setOutlineProvider(OutlineProviders.roundedRect(10));
itemView.setClipToOutline(true);
title.setPaintFlags(title.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
}
@Override
public void onBind(Item item){
domain.setText(item.domain);
title.setText(item.title);
ViewImageLoader.load(favicon, null, new UrlImageLoaderRequest(item.faviconUrl));
}
@Override

View File

@ -187,6 +187,8 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
}
protected void loadInstanceInfo(String _domain, boolean isFromRedirect){
if(TextUtils.isEmpty(_domain))
return;
String domain=normalizeInstanceDomain(_domain);
Instance cachedInstance=instancesCache.get(domain);
if(cachedInstance!=null){
@ -222,7 +224,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
}
if(Objects.equals(domain, currentSearchQuery) || Objects.equals(currentSearchQuery, redirects.get(domain)) || Objects.equals(currentSearchQuery, redirectsInverse.get(domain))){
boolean found=false;
for(CatalogInstance ci : filteredData){
for(CatalogInstance ci:filteredData){
if(ci.domain.equals(domain) && ci!=fakeInstance){
found=true;
break;

View File

@ -1,39 +1,52 @@
package org.joinmastodon.android.fragments.onboarding;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Bundle;
import android.os.LocaleList;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.Menu;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.inputmethod.InputMethodManager;
import android.widget.HorizontalScrollView;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.RadioButton;
import android.widget.RelativeLayout;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.catalog.GetCatalogCategories;
import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.catalog.CatalogCategory;
import org.joinmastodon.android.model.catalog.CatalogInstance;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.FilterChipView;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Locale;
import java.util.Random;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
@ -42,18 +55,36 @@ import androidx.recyclerview.widget.RecyclerView;
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.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.CubicBezierInterpolator;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
private View headerView;
public class InstanceCatalogSignupFragment extends InstanceCatalogFragment implements OnBackPressedListener{
private MastodonAPIRequest<?> getCategoriesRequest;
private TabLayout categoriesList;
private String currentCategory="all";
private List<CatalogCategory> categories=new ArrayList<>();
private View topBar;
private List<String> languages=Collections.emptyList();
private PopupMenu langFilterMenu, speedFilterMenu;
private SignupSpeedFilter currentSignupSpeedFilter=SignupSpeedFilter.INSTANT;
private String currentLanguage=null;
private boolean searchQueryMode;
private LinearLayout filtersWrap;
private HorizontalScrollView filtersScroll;
private ImageButton backBtn, clearSearchBtn;
private FilterChipView categoryGeneral, categorySpecialInterests;
private List<FilterChipView> regionalFilters;
private CatalogInstance.Region chosenRegion;
private CategoryChoice categoryChoice;
public InstanceCatalogSignupFragment(){
super(R.layout.fragment_onboarding_common, 10);
@ -63,6 +94,12 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
public void onAttach(Context context){
super.onAttach(context);
setRefreshEnabled(false);
setRetainInstance(true);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
loadData();
}
@ -75,6 +112,25 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
if(getActivity()==null)
return;
onDataLoaded(sortInstances(result), false);
if(langFilterMenu!=null){
Menu menu=langFilterMenu.getMenu();
menu.clear();
menu.add(0, 0, 0, R.string.server_filter_any_language);
languages=result.stream().map(i->i.language).distinct().filter(s->s.length()>0).sorted().collect(Collectors.toList());
int i=1;
for(String lang:languages){
Locale locale=Locale.forLanguageTag(lang);
String name=locale.getDisplayLanguage(locale);
if(name.equals(lang))
name=lang.toUpperCase();
else
name=name.substring(0, 1).toUpperCase()+name.substring(1);
menu.add(0, i, 0, name);
i++;
}
}
updateFilteredList();
}
@ -111,14 +167,14 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
}
private void updateCategories(){
categoriesList.removeAllTabs();
for(CatalogCategory cat:categories){
int titleRes=getTitleForCategory(cat.category);
TabLayout.Tab tab=categoriesList.newTab().setText(titleRes!=0 ? getString(titleRes) : cat.category).setCustomView(R.layout.item_instance_category);
ImageView emoji=tab.getCustomView().findViewById(R.id.emoji);
emoji.setImageResource(getEmojiForCategory(cat.category));
categoriesList.addTab(tab);
}
// categoriesList.removeAllTabs();
// for(CatalogCategory cat:categories){
// int titleRes=getTitleForCategory(cat.category);
// TabLayout.Tab tab=categoriesList.newTab().setText(titleRes!=0 ? getString(titleRes) : cat.category).setCustomView(R.layout.item_instance_category);
// ImageView emoji=tab.getCustomView().findViewById(R.id.emoji);
// emoji.setImageResource(getEmojiForCategory(cat.category));
// categoriesList.addTab(tab);
// }
}
@Override
@ -130,27 +186,77 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
@Override
protected RecyclerView.Adapter getAdapter(){
headerView=getActivity().getLayoutInflater().inflate(R.layout.header_onboarding_instance_catalog, list, false);
searchEdit=headerView.findViewById(R.id.search_edit);
categoriesList=headerView.findViewById(R.id.categories_list);
categoriesList.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener(){
@Override
public void onTabSelected(TabLayout.Tab tab){
CatalogCategory category=categories.get(tab.getPosition());
currentCategory=category.category;
updateFilteredList();
}
View headerView=new View(getActivity());
headerView.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1));
@Override
public void onTabUnselected(TabLayout.Tab tab){
}
@Override
public void onTabReselected(TabLayout.Tab tab){
mergeAdapter=new MergeRecyclerAdapter();
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
mergeAdapter.addAdapter(adapter=new InstancesAdapter());
return mergeAdapter;
}
@SuppressLint("ClickableViewAccessibility")
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
backBtn=view.findViewById(R.id.btn_back);
backBtn.setOnClickListener(v->{
if(searchQueryMode){
setSearchQueryMode(false);
}else{
Nav.finish(this);
}
});
clearSearchBtn=view.findViewById(R.id.clear);
clearSearchBtn.setOnClickListener(v->searchEdit.setText(""));
nextButton.setEnabled(true);
list.setItemAnimator(new BetterItemAnimator());
setStatusBarColor(0);
topBar=view.findViewById(R.id.top_bar);
LayerDrawable topBg=(LayerDrawable) topBar.getBackground().mutate();
topBar.setBackground(topBg);
Drawable topOverlay=topBg.findDrawableByLayerId(R.id.color_overlay);
topOverlay.setAlpha(0);
LayerDrawable btmBg=(LayerDrawable) buttonBar.getBackground().mutate();
buttonBar.setBackground(btmBg);
Drawable btmOverlay=btmBg.findDrawableByLayerId(R.id.color_overlay);
btmOverlay.setAlpha(0);
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
private boolean isAtTop=true;
private Animator currentPanelsAnim;
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
boolean newAtTop=recyclerView.getChildCount()==0 || (recyclerView.getChildAdapterPosition(recyclerView.getChildAt(0))==0 && recyclerView.getChildAt(0).getTop()==recyclerView.getPaddingTop());
if(newAtTop!=isAtTop){
isAtTop=newAtTop;
if(currentPanelsAnim!=null)
currentPanelsAnim.cancel();
AnimatorSet set=new AnimatorSet();
set.playTogether(
ObjectAnimator.ofInt(topOverlay, "alpha", isAtTop ? 0 : 20),
ObjectAnimator.ofInt(btmOverlay, "alpha", isAtTop ? 0 : 20),
ObjectAnimator.ofFloat(topBar, View.TRANSLATION_Z, isAtTop ? 0 : V.dp(3)),
ObjectAnimator.ofFloat(buttonBar, View.TRANSLATION_Z, isAtTop ? 0 : V.dp(3))
);
set.setDuration(150);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
currentPanelsAnim=null;
}
});
set.start();
currentPanelsAnim=set;
}
}
});
searchEdit=view.findViewById(R.id.search_edit);
searchEdit.setOnEditorActionListener(this::onSearchEnterPressed);
searchEdit.addTextChangedListener(new TextWatcher(){
@Override
@ -166,42 +272,145 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
@Override
public void afterTextChanged(Editable s){
if((clearSearchBtn.getVisibility()==View.VISIBLE)!=(s.length()>0)){
clearSearchBtn.setVisibility(s.length()>0 ? View.VISIBLE : View.GONE);
}
}
});
searchEdit.setOnFocusChangeListener((v, hasFocus)->{
if(hasFocus && !searchQueryMode){
setSearchQueryMode(true);
}
});
mergeAdapter=new MergeRecyclerAdapter();
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
mergeAdapter.addAdapter(adapter=new InstancesAdapter());
return mergeAdapter;
FilterChipView langFilter=new FilterChipView(getActivity());
langFilter.setDrawableEnd(R.drawable.ic_baseline_arrow_drop_down_18);
langFilter.setText(R.string.server_filter_any_language);
langFilterMenu=new PopupMenu(getContext(), langFilter);
langFilter.setOnTouchListener(langFilterMenu.getDragToOpenListener());
langFilter.setOnClickListener(v->langFilterMenu.show());
filtersWrap=view.findViewById(R.id.filters_container);
filtersScroll=view.findViewById(R.id.filters_scroll);
filtersWrap.addView(langFilter, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
FilterChipView speedFilter=new FilterChipView(getActivity());
speedFilter.setDrawableEnd(R.drawable.ic_baseline_arrow_drop_down_18);
speedFilterMenu=new PopupMenu(getContext(), speedFilter);
speedFilterMenu.getMenu().add(0, 0, 0, R.string.server_filter_any_signup_speed);
speedFilterMenu.getMenu().add(0, 1, 0, R.string.server_filter_instant_signup);
speedFilterMenu.getMenu().add(0, 2, 0, R.string.server_filter_manual_review);
speedFilter.setOnTouchListener(speedFilterMenu.getDragToOpenListener());
speedFilter.setOnClickListener(v->speedFilterMenu.show());
speedFilter.setText(R.string.server_filter_instant_signup);
speedFilter.setSelected(true);
filtersWrap.addView(speedFilter, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
speedFilterMenu.setOnMenuItemClickListener(item->{
speedFilter.setText(item.getTitle());
speedFilter.setSelected(item.getItemId()>0);
currentSignupSpeedFilter=SignupSpeedFilter.values()[item.getItemId()];
updateFilteredList();
return true;
});
langFilterMenu.setOnMenuItemClickListener(item->{
langFilter.setText(item.getTitle());
langFilter.setSelected(item.getItemId()>0);
currentLanguage=item.getItemId()==0 ? null : languages.get(item.getItemId()-1);
updateFilteredList();
return true;
});
View divider=new View(getActivity());
divider.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Outline));
filtersWrap.addView(divider, new LinearLayout.LayoutParams(V.dp(.5f), ViewGroup.LayoutParams.MATCH_PARENT));
categoryGeneral=new FilterChipView(getActivity());
categoryGeneral.setText(R.string.category_general);
categoryGeneral.setTag(CategoryChoice.GENERAL);
categoryGeneral.setOnClickListener(this::onCategoryFilterClick);
filtersWrap.addView(categoryGeneral, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
categorySpecialInterests=new FilterChipView(getActivity());
categorySpecialInterests.setText(R.string.category_special_interests);
categorySpecialInterests.setTag(CategoryChoice.SPECIAL);
categorySpecialInterests.setOnClickListener(this::onCategoryFilterClick);
filtersWrap.addView(categorySpecialInterests, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
regionalFilters=Arrays.stream(CatalogInstance.Region.values()).map(r->{
FilterChipView fv=new FilterChipView(getActivity());
fv.setTag(r);
fv.setText(switch(r){
case EUROPE -> R.string.server_filter_region_europe;
case NORTH_AMERICA -> R.string.server_filter_region_north_america;
case SOUTH_AMERICA -> R.string.server_filter_region_south_america;
case AFRICA -> R.string.server_filter_region_africa;
case ASIA -> R.string.server_filter_region_asia;
case OCEANIA -> R.string.server_filter_region_oceania;
});
fv.setSelected(r==chosenRegion);
fv.setOnClickListener(this::onRegionFilterClick);
filtersWrap.addView(fv, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
return fv;
}).collect(Collectors.toList());
}
private void onRegionFilterClick(View v){
CatalogInstance.Region r=(CatalogInstance.Region) v.getTag();
if(chosenRegion==r){
chosenRegion=null;
v.setSelected(false);
}else{
if(chosenRegion!=null)
filtersWrap.findViewWithTag(chosenRegion).setSelected(false);
chosenRegion=r;
v.setSelected(true);
}
updateFilteredList();
}
private void onCategoryFilterClick(View v){
CategoryChoice c=(CategoryChoice) v.getTag();
if(categoryChoice==c){
categoryChoice=null;
v.setSelected(false);
}else{
if(categoryChoice!=null)
filtersWrap.findViewWithTag(categoryChoice).setSelected(false);
categoryChoice=c;
v.setSelected(true);
}
updateFilteredList();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
view.findViewById(R.id.btn_back).setOnClickListener(v->Nav.finish(this));
list.setItemAnimator(new BetterItemAnimator());
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 16, 16, DividerItemDecoration.NOT_FIRST));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
protected void onNextClick(View v){
if(chosenInstance==null){
String lang=Locale.getDefault().getLanguage();
List<CatalogInstance> instances=data.stream().filter(ci->!ci.approvalRequired && ("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general"))) && (lang.equals(ci.language) || (ci.languages!=null && ci.languages.contains(lang)))).collect(Collectors.toList());
if(instances.isEmpty()){
instances=data.stream().filter(ci->!ci.approvalRequired && ("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general")))).collect(Collectors.toList());
}
if(instances.isEmpty()){
return;
}
chosenInstance=instances.get(new Random().nextInt(instances.size()));
}
super.onNextClick(v);
}
@Override
protected void proceedWithAuthOrSignup(Instance instance){
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
if(isSignup){
if(!instance.registrations){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(R.string.instance_signup_closed)
.setPositiveButton(R.string.ok, null)
.show();
return;
}
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
Nav.go(getActivity(), InstanceRulesFragment.class, args);
}else{
if(!instance.registrations){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(R.string.instance_signup_closed)
.setPositiveButton(R.string.ok, null)
.show();
return;
}
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
Nav.go(getActivity(), InstanceRulesFragment.class, args);
}
// private String getEmojiForCategory(String category){
@ -265,11 +474,29 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
protected void updateFilteredList(){
ArrayList<CatalogInstance> prevData=new ArrayList<>(filteredData);
filteredData.clear();
for(CatalogInstance instance:data){
if(currentCategory.equals("all") || instance.categories.contains(currentCategory)){
if(TextUtils.isEmpty(currentSearchQuery) || instance.domain.contains(currentSearchQuery)){
if(instance.domain.equals(currentSearchQuery) || !isSignup || !instance.approvalRequired)
if(searchQueryMode){
if(!TextUtils.isEmpty(currentSearchQuery)){
for(CatalogInstance instance:data){
if(instance.domain.contains(currentSearchQuery)){
filteredData.add(instance);
}
}
}
}else{
for(CatalogInstance instance:data){
if(categoryChoice==null || categoryChoice.matches(instance.category)){
if(chosenRegion==null || instance.region==chosenRegion){
boolean signupSpeedMatches=switch(currentSignupSpeedFilter){
case ANY -> true;
case INSTANT -> !instance.approvalRequired;
case REVIEWED -> instance.approvalRequired;
};
if(signupSpeedMatches){
if(currentLanguage==null || instance.languages.contains(currentLanguage)){
filteredData.add(instance);
}
}
}
}
}
}
@ -296,8 +523,46 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
}).dispatchUpdatesTo(adapter);
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
topBar.setPadding(0, insets.getSystemWindowInsetTop(), 0, 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
private class InstancesAdapter extends UsableRecyclerView.Adapter<InstanceCatalogSignupFragment.InstanceViewHolder>{
@Override
public boolean onBackPressed(){
if(searchQueryMode){
setSearchQueryMode(false);
return true;
}
return false;
}
private void setSearchQueryMode(boolean enabled){
searchQueryMode=enabled;
RelativeLayout.LayoutParams lp=(RelativeLayout.LayoutParams) searchEdit.getLayoutParams();
if(searchQueryMode){
filtersScroll.setVisibility(View.GONE);
lp.removeRule(RelativeLayout.END_OF);
backBtn.setScaleX(0.83333333f);
backBtn.setScaleY(0.83333333f);
backBtn.setTranslationX(V.dp(8));
searchEdit.setCompoundDrawableTintList(ColorStateList.valueOf(0));
}else{
filtersScroll.setVisibility(View.VISIBLE);
searchEdit.clearFocus();
searchEdit.setText("");
lp.addRule(RelativeLayout.END_OF, R.id.btn_back);
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0);
backBtn.setScaleX(1);
backBtn.setScaleY(1);
backBtn.setTranslationX(0);
searchEdit.setCompoundDrawableTintList(ColorStateList.valueOf(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant)));
}
updateFilteredList();
}
private class InstancesAdapter extends UsableRecyclerView.Adapter<InstanceCatalogSignupFragment.InstanceViewHolder> implements ImageLoaderRecyclerAdapter{
public InstancesAdapter(){
super(imgLoader);
}
@ -323,32 +588,53 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
public int getItemViewType(int position){
return -1;
}
@Override
public int getImageCountForItem(int position){
return filteredData.get(position).thumbnailRequest!=null ? 1 : 0;
}
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
return filteredData.get(position).thumbnailRequest;
}
}
private class InstanceViewHolder extends BindableViewHolder<CatalogInstance> implements UsableRecyclerView.Clickable{
private final TextView title, description, userCount, lang;
private class InstanceViewHolder extends BindableViewHolder<CatalogInstance> implements UsableRecyclerView.DisableableClickable, ImageLoaderViewHolder{
private final TextView title, description;
private final RadioButton radioButton;
private final ImageView thumbnail;
private boolean enabled;
public InstanceViewHolder(){
super(getActivity(), R.layout.item_instance_catalog, list);
title=findViewById(R.id.title);
description=findViewById(R.id.description);
userCount=findViewById(R.id.user_count);
lang=findViewById(R.id.lang);
radioButton=findViewById(R.id.radiobtn);
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N){
UiUtils.fixCompoundDrawableTintOnAndroid6(userCount);
UiUtils.fixCompoundDrawableTintOnAndroid6(lang);
}
thumbnail=findViewById(R.id.image);
}
@Override
public void onBind(CatalogInstance item){
title.setText(item.normalizedDomain);
description.setText(item.description);
userCount.setText(UiUtils.abbreviateNumber(item.totalUsers));
lang.setText(item.language.toUpperCase());
radioButton.setChecked(chosenInstance==item);
if(item.thumbnailRequest==null)
thumbnail.setImageDrawable(null);
Instance realInstance=instancesCache.get(item.normalizedDomain);
float alpha;
if(realInstance!=null && !realInstance.registrations){
alpha=0.38f;
description.setText(R.string.not_accepting_new_members);
enabled=false;
}else{
alpha=1f;
description.setText(item.description);
enabled=true;
}
title.setAlpha(alpha);
description.setAlpha(alpha);
radioButton.setAlpha(alpha);
thumbnail.setAlpha(alpha);
}
@Override
@ -358,10 +644,17 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
if(chosenInstance!=null){
int idx=filteredData.indexOf(chosenInstance);
if(idx!=-1){
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(mergeAdapter.getPositionForAdapter(adapter)+idx);
if(holder instanceof InstanceCatalogSignupFragment.InstanceViewHolder ivh){
ivh.radioButton.setChecked(false);
boolean found=false;
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder.getAbsoluteAdapterPosition()==mergeAdapter.getPositionForAdapter(adapter)+idx && holder instanceof InstanceViewHolder ivh){
ivh.radioButton.setChecked(false);
found=true;
break;
}
}
if(!found)
adapter.notifyItemChanged(idx);
}
}
radioButton.setChecked(true);
@ -370,5 +663,36 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment{
chosenInstance=item;
loadInstanceInfo(chosenInstance.domain, false);
}
@Override
public void setImage(int index, Drawable image){
thumbnail.setImageDrawable(image);
}
@Override
public void clearImage(int index){
setImage(index, null);
}
@Override
public boolean isEnabled(){
return enabled;
}
}
private enum SignupSpeedFilter{
ANY,
INSTANT,
REVIEWED
}
private enum CategoryChoice{
GENERAL,
SPECIAL;
public boolean matches(String category){
boolean isGeneral=(category==null || "general".equals(category));
return (this==GENERAL)==isGeneral;
}
}
}

View File

@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments.onboarding;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
@ -22,14 +23,14 @@ import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class InstanceRulesFragment extends AppKitFragment{
public class InstanceRulesFragment extends ToolbarFragment{
private UsableRecyclerView list;
private MergeRecyclerAdapter adapter;
private Button btn;
@ -47,31 +48,28 @@ public class InstanceRulesFragment extends AppKitFragment{
super.onAttach(activity);
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
instance=Parcels.unwrap(getArguments().getParcelable("instance"));
setTitle(R.string.instance_rules_title);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
View view=inflater.inflate(R.layout.fragment_onboarding_rules, container, false);
list=view.findViewById(R.id.list);
list.setLayoutManager(new LinearLayoutManager(getActivity()));
View headerView=inflater.inflate(R.layout.item_list_header, list, false);
TextView title=headerView.findViewById(R.id.title);
TextView subtitle=headerView.findViewById(R.id.subtitle);
headerView.findViewById(R.id.step_counter).setVisibility(View.GONE);
title.setText(R.string.instance_rules_title);
subtitle.setText(getString(R.string.instance_rules_subtitle, instance.uri));
View headerView=inflater.inflate(R.layout.item_list_header_simple, list, false);
TextView text=headerView.findViewById(R.id.text);
text.setText(getString(R.string.instance_rules_subtitle, instance.uri));
adapter=new MergeRecyclerAdapter();
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
adapter.addAdapter(new ItemsAdapter());
list.setAdapter(adapter);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 16, 16, DividerItemDecoration.NOT_FIRST));
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3SurfaceVariant, 1, 56, 0, DividerItemDecoration.NOT_FIRST));
btn=view.findViewById(R.id.btn_next);
btn.setOnClickListener(v->onButtonClick());
buttonBar=view.findViewById(R.id.button_bar);
view.findViewById(R.id.btn_back).setOnClickListener(v->Nav.finish(this));
return view;
}
@ -79,7 +77,15 @@ public class InstanceRulesFragment extends AppKitFragment{
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
getToolbar().setBackground(null);
getToolbar().setElevation(0);
}
protected void onButtonClick(){
@ -119,23 +125,22 @@ public class InstanceRulesFragment extends AppKitFragment{
}
private class ItemViewHolder extends BindableViewHolder<Instance.Rule>{
private final TextView title, subtitle;
private final ImageView checkbox;
private final TextView text, number;
public ItemViewHolder(){
super(getActivity(), R.layout.item_report_choice, list);
title=findViewById(R.id.title);
subtitle=findViewById(R.id.subtitle);
checkbox=findViewById(R.id.checkbox);
subtitle.setVisibility(View.GONE);
super(getActivity(), R.layout.item_server_rule, list);
text=findViewById(R.id.text);
number=findViewById(R.id.number);
}
@SuppressLint("DefaultLocale")
@Override
public void onBind(Instance.Rule item){
if(item.parsedText==null){
item.parsedText=HtmlParser.parseLinks(item.text);
}
title.setText(item.parsedText);
text.setText(item.parsedText);
number.setText(String.format("%d", getAbsoluteAdapterPosition()));
}
}
}

View File

@ -25,14 +25,15 @@ import org.joinmastodon.android.api.MastodonDetailedErrorResponse;
import org.joinmastodon.android.api.requests.accounts.RegisterAccount;
import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp;
import org.joinmastodon.android.api.requests.oauth.GetOauthToken;
import org.joinmastodon.android.api.session.AccountActivationInfo;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Token;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
import org.parceler.Parcels;
import java.io.File;
@ -49,30 +50,28 @@ import me.grishka.appkit.Nav;
import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.AppKitFragment;
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 SignupFragment extends AppKitFragment{
public class SignupFragment extends ToolbarFragment{
private static final int AVATAR_RESULT=198;
private static final String TAG="SignupFragment";
private Instance instance;
private EditText displayName, username, email, password, reason;
private EditText displayName, username, email, password, passwordConfirm, reason;
private FloatingHintEditTextLayout displayNameWrap, usernameWrap, emailWrap, passwordWrap, passwordConfirmWrap, reasonWrap;
private TextView reasonExplain;
private Button btn;
private View buttonBar;
private TextWatcher buttonStateUpdater=new SimpleTextWatcher(e->updateButtonState());
private ImageView avatar;
private APIRequest currentBackgroundRequest;
private Application apiApplication;
private Token apiToken;
private boolean submitAfterGettingToken;
private ProgressDialog progressDialog;
private Uri avatarUri;
private File avatarFile;
private HashSet<EditText> errorFields=new HashSet<>();
@Override
@ -81,25 +80,30 @@ public class SignupFragment extends AppKitFragment{
setRetainInstance(true);
instance=Parcels.unwrap(getArguments().getParcelable("instance"));
createAppAndGetToken();
setTitle(R.string.signup_title);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){
public View onCreateContentView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){
View view=inflater.inflate(R.layout.fragment_onboarding_signup, container, false);
TextView title=view.findViewById(R.id.title);
TextView domain=view.findViewById(R.id.domain);
displayName=view.findViewById(R.id.display_name);
username=view.findViewById(R.id.username);
email=view.findViewById(R.id.email);
password=view.findViewById(R.id.password);
avatar=view.findViewById(R.id.avatar);
passwordConfirm=view.findViewById(R.id.password_confirm);
reason=view.findViewById(R.id.reason);
reasonExplain=view.findViewById(R.id.reason_explain);
View avaWrap=view.findViewById(R.id.ava_wrap);
title.setText(getString(R.string.signup_title, instance.uri));
displayNameWrap=view.findViewById(R.id.display_name_wrap);
usernameWrap=view.findViewById(R.id.username_wrap);
emailWrap=view.findViewById(R.id.email_wrap);
passwordWrap=view.findViewById(R.id.password_wrap);
passwordConfirmWrap=view.findViewById(R.id.password_confirm_wrap);
reasonWrap=view.findViewById(R.id.reason_wrap);
domain.setText('@'+instance.uri);
username.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@ -114,23 +118,20 @@ public class SignupFragment extends AppKitFragment{
btn=view.findViewById(R.id.btn_next);
btn.setOnClickListener(v->onButtonClick());
buttonBar=view.findViewById(R.id.button_bar);
view.findViewById(R.id.btn_back).setOnClickListener(v->Nav.finish(this));
updateButtonState();
username.addTextChangedListener(buttonStateUpdater);
email.addTextChangedListener(buttonStateUpdater);
password.addTextChangedListener(buttonStateUpdater);
passwordConfirm.addTextChangedListener(buttonStateUpdater);
reason.addTextChangedListener(buttonStateUpdater);
username.addTextChangedListener(new ErrorClearingListener(username));
email.addTextChangedListener(new ErrorClearingListener(email));
password.addTextChangedListener(new ErrorClearingListener(password));
passwordConfirm.addTextChangedListener(new ErrorClearingListener(passwordConfirm));
reason.addTextChangedListener(new ErrorClearingListener(reason));
avaWrap.setOutlineProvider(OutlineProviders.roundedRect(22));
avaWrap.setClipToOutline(true);
avaWrap.setOnClickListener(v->onAvatarClick());
if(!instance.approvalRequired){
reason.setVisibility(View.GONE);
reasonExplain.setVisibility(View.GONE);
@ -142,10 +143,23 @@ public class SignupFragment extends AppKitFragment{
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
getToolbar().setBackground(null);
getToolbar().setElevation(0);
}
private void onButtonClick(){
if(!password.getText().equals(passwordConfirm.getText())){
passwordConfirm.setError(getString(R.string.signup_passwords_dont_match));
passwordConfirmWrap.setErrorState();
return;
}
showProgressDialog();
if(currentBackgroundRequest!=null){
submitAfterGettingToken=true;
@ -160,32 +174,8 @@ public class SignupFragment extends AppKitFragment{
}
}
private void copyAvatar(Runnable onDone){
// Need to copy the avatar from the content provider to somewhere accessible in case the app gets killed between signup and account activation
Activity activity=getActivity();
MastodonAPIController.runInBackground(()->{
String origName=UiUtils.getFileName(avatarUri);
avatarFile=new File(activity.getCacheDir(), System.currentTimeMillis()+origName.substring(origName.lastIndexOf('.')));
try(InputStream in=activity.getContentResolver().openInputStream(avatarUri);
FileOutputStream out=new FileOutputStream(avatarFile)){
byte[] buf=new byte[10240];
int read;
while((read=in.read(buf))>0){
out.write(buf, 0, read);
}
}catch(IOException x){
Log.w(TAG, "copyAvatar: error copying", x);
}
activity.runOnUiThread(onDone);
});
}
private void submit(){
if(avatarUri!=null && (avatarFile==null || !avatarFile.exists())){
copyAvatar(this::actuallySubmit);
}else{
actuallySubmit();
}
actuallySubmit();
}
private void actuallySubmit(){
@ -204,9 +194,7 @@ public class SignupFragment extends AppKitFragment{
fakeAccount.acct=fakeAccount.username=username;
fakeAccount.id="tmp"+System.currentTimeMillis();
fakeAccount.displayName=displayName.getText().toString();
if(avatarFile!=null)
fakeAccount.avatar=avatarFile.getAbsolutePath();
AccountSessionManager.getInstance().addAccount(instance, result, fakeAccount, apiApplication, false);
AccountSessionManager.getInstance().addAccount(instance, result, fakeAccount, apiApplication, new AccountActivationInfo(email, System.currentTimeMillis()));
Bundle args=new Bundle();
args.putString("account", AccountSessionManager.getInstance().getLastActiveAccountID());
Nav.goClearingStack(getActivity(), AccountActivationFragment.class, args);
@ -225,6 +213,7 @@ public class SignupFragment extends AppKitFragment{
continue;
}
field.setError(fieldErrors.get(fieldName).stream().map(err->err.description).collect(Collectors.joining("\n")));
getFieldWrapByName(fieldName).setErrorState();
errorFields.add(field);
if(first){
first=false;
@ -252,6 +241,16 @@ public class SignupFragment extends AppKitFragment{
};
}
private FloatingHintEditTextLayout getFieldWrapByName(String name){
return switch(name){
case "email" -> emailWrap;
case "username" -> usernameWrap;
case "password" -> passwordWrap;
case "reason" -> reasonWrap;
default -> null;
};
}
private void showProgressDialog(){
if(progressDialog==null){
progressDialog=new ProgressDialog(getActivity());
@ -262,7 +261,7 @@ public class SignupFragment extends AppKitFragment{
}
private void updateButtonState(){
btn.setEnabled(username.length()>0 && email.length()>0 && email.getText().toString().contains("@") && password.length()>=8 && (!instance.approvalRequired || reason.length()>0));
btn.setEnabled(username.length()>0 && email.length()>0 && email.getText().toString().contains("@") && password.length()>=8 && passwordConfirm.length()>=8 && (!instance.approvalRequired || reason.length()>0));
}
private void createAppAndGetToken(){
@ -324,20 +323,6 @@ public class SignupFragment extends AppKitFragment{
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data){
if(requestCode==AVATAR_RESULT && resultCode==Activity.RESULT_OK){
avatarUri=data.getData();
if(avatarFile!=null && avatarFile.exists())
avatarFile.delete();
ViewImageLoader.load(avatar, getResources().getDrawable(R.drawable.default_avatar), new UrlImageLoaderRequest(avatarUri, V.dp(100), V.dp(100)));
}
}
private void onAvatarClick(){
startActivityForResult(new Intent(Intent.ACTION_GET_CONTENT).setType("image/*").addCategory(Intent.CATEGORY_OPENABLE), AVATAR_RESULT);
}
private class ErrorClearingListener implements TextWatcher{
public final EditText editText;

View File

@ -1,5 +1,10 @@
package org.joinmastodon.android.model.catalog;
import android.graphics.Region;
import android.text.TextUtils;
import com.google.gson.annotations.SerializedName;
import org.joinmastodon.android.api.AllFieldsAreRequired;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.model.BaseModel;
@ -7,13 +12,17 @@ import org.joinmastodon.android.model.BaseModel;
import java.net.IDN;
import java.util.List;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
@AllFieldsAreRequired
public class CatalogInstance extends BaseModel{
public String domain;
public String version;
public String description;
public List<String> languages;
public String region;
@SerializedName("region")
private String _region;
public List<String> categories;
public String proxiedThumbnail;
public int totalUsers;
@ -22,7 +31,9 @@ public class CatalogInstance extends BaseModel{
public String language;
public String category;
public transient Region region;
public transient String normalizedDomain;
public transient UrlImageLoaderRequest thumbnailRequest;
@Override
public void postprocess() throws ObjectValidationException{
@ -31,6 +42,14 @@ public class CatalogInstance extends BaseModel{
normalizedDomain=IDN.toUnicode(domain);
else
normalizedDomain=domain;
if(!TextUtils.isEmpty(_region)){
try{
region=Region.valueOf(_region.toUpperCase());
}catch(IllegalArgumentException ignore){}
}
if(!TextUtils.isEmpty(proxiedThumbnail)){
thumbnailRequest=new UrlImageLoaderRequest(proxiedThumbnail, 0, V.dp(56));
}
}
@Override
@ -50,4 +69,13 @@ public class CatalogInstance extends BaseModel{
", category='"+category+'\''+
'}';
}
public enum Region{
EUROPE,
NORTH_AMERICA,
SOUTH_AMERICA,
AFRICA,
ASIA,
OCEANIA
}
}

View File

@ -243,7 +243,10 @@ public class AccountSwitcherSheet extends BottomSheet{
public WrappedAccount(AccountSession session){
this.session=session;
req=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? session.self.avatar : session.self.avatarStatic, V.dp(50), V.dp(50));
if(session.self.avatar!=null)
req=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? session.self.avatar : session.self.avatarStatic, V.dp(50), V.dp(50));
else
req=null;
}
}
}

View File

@ -0,0 +1,59 @@
package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.Button;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.utils.UiUtils;
import androidx.annotation.DrawableRes;
import me.grishka.appkit.utils.V;
public class FilterChipView extends Button{
private boolean currentlySelected;
public FilterChipView(Context context){
this(context, null);
}
public FilterChipView(Context context, AttributeSet attrs){
this(context, attrs, 0);
}
public FilterChipView(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
setCompoundDrawablePadding(V.dp(8));
setBackgroundResource(R.drawable.bg_filter_chip);
setTextAppearance(R.style.m3_label_large);
setTextColor(getResources().getColorStateList(R.color.filter_chip_text, context.getTheme()));
setCompoundDrawableTintList(ColorStateList.valueOf(UiUtils.getThemeColor(context, R.attr.colorM3OnSurface)));
updatePadding();
}
@Override
protected void drawableStateChanged(){
super.drawableStateChanged();
if(currentlySelected==isSelected())
return;
currentlySelected=isSelected();
Drawable start=currentlySelected ? getResources().getDrawable(R.drawable.ic_baseline_check_18, getContext().getTheme()) : null;
Drawable end=getCompoundDrawablesRelative()[2];
setCompoundDrawablesRelativeWithIntrinsicBounds(start, null, end, null);
updatePadding();
}
private void updatePadding(){
int vertical=V.dp(6);
Drawable[] drawables=getCompoundDrawablesRelative();
setPaddingRelative(V.dp(drawables[0]==null ? 16 : 8), vertical, V.dp(drawables[2]==null ? 16 : 8), vertical);
}
public void setDrawableEnd(@DrawableRes int drawable){
setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, drawable, 0);
updatePadding();
}
}

View File

@ -5,19 +5,34 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.os.Build;
import android.text.Editable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
@ -28,6 +43,10 @@ public class FloatingHintEditTextLayout extends FrameLayout{
private int offsetY;
private boolean hintVisible;
private Animator currentAnim;
private float animProgress;
private RectF tmpRect=new RectF();
private ColorStateList labelColors, origHintColors;
private boolean errorState;
public FloatingHintEditTextLayout(Context context){
this(context, null);
@ -44,7 +63,9 @@ public class FloatingHintEditTextLayout extends FrameLayout{
TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.FloatingHintEditTextLayout);
labelTextSize=ta.getDimensionPixelSize(R.styleable.FloatingHintEditTextLayout_android_labelTextSize, V.dp(12));
offsetY=ta.getDimensionPixelOffset(R.styleable.FloatingHintEditTextLayout_editTextOffsetY, 0);
labelColors=ta.getColorStateList(R.styleable.FloatingHintEditTextLayout_labelTextColor);
ta.recycle();
setAddStatesFromChildren(true);
}
@Override
@ -58,13 +79,15 @@ public class FloatingHintEditTextLayout extends FrameLayout{
label=new TextView(getContext());
label.setTextSize(TypedValue.COMPLEX_UNIT_PX, labelTextSize);
label.setTextColor(edit.getHintTextColors());
// label.setTextColor(labelColors==null ? edit.getHintTextColors() : labelColors);
origHintColors=edit.getHintTextColors();
label.setText(edit.getHint());
label.setSingleLine();
label.setPivotX(0f);
label.setPivotY(0f);
label.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
LayoutParams lp=new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.START | Gravity.TOP);
lp.setMarginStart(edit.getPaddingStart());
lp.setMarginStart(edit.getPaddingStart()+((LayoutParams)edit.getLayoutParams()).getMarginStart());
addView(label, lp);
hintVisible=edit.getText().length()==0;
@ -75,6 +98,11 @@ public class FloatingHintEditTextLayout extends FrameLayout{
}
private void onTextChanged(Editable text){
if(errorState){
errorState=false;
setForeground(getResources().getDrawable(R.drawable.bg_m3_outlined_text_field));
refreshDrawableState();
}
boolean newHintVisible=text.length()==0;
if(newHintVisible==hintVisible)
return;
@ -83,42 +111,210 @@ public class FloatingHintEditTextLayout extends FrameLayout{
hintVisible=newHintVisible;
label.setAlpha(1);
float scale=edit.getLineHeight()/(float)label.getLineHeight();
float transY=edit.getHeight()/2f-edit.getLineHeight()/2f+(edit.getTop()-label.getTop())-(label.getHeight()/2f-label.getLineHeight()/2f);
AnimatorSet anim=new AnimatorSet();
if(hintVisible){
anim.playTogether(
ObjectAnimator.ofFloat(edit, TRANSLATION_Y, 0),
ObjectAnimator.ofFloat(label, SCALE_X, scale),
ObjectAnimator.ofFloat(label, SCALE_Y, scale),
ObjectAnimator.ofFloat(label, TRANSLATION_Y, transY)
);
edit.setHintTextColor(0);
}else{
label.setScaleX(scale);
label.setScaleY(scale);
label.setTranslationY(transY);
anim.playTogether(
ObjectAnimator.ofFloat(edit, TRANSLATION_Y, offsetY),
ObjectAnimator.ofFloat(label, SCALE_X, 1f),
ObjectAnimator.ofFloat(label, SCALE_Y, 1f),
ObjectAnimator.ofFloat(label, TRANSLATION_Y, 0f)
);
}
anim.setDuration(150);
anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
anim.start();
anim.addListener(new AnimatorListenerAdapter(){
edit.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public void onAnimationEnd(Animator animation){
currentAnim=null;
public boolean onPreDraw(){
edit.getViewTreeObserver().removeOnPreDrawListener(this);
float scale=edit.getLineHeight()/(float)label.getLineHeight();
float transY=edit.getHeight()/2f-edit.getLineHeight()/2f+(edit.getTop()-label.getTop())-(label.getHeight()/2f-label.getLineHeight()/2f);
AnimatorSet anim=new AnimatorSet();
if(hintVisible){
label.setAlpha(0);
edit.setHintTextColor(label.getTextColors());
anim.playTogether(
ObjectAnimator.ofFloat(edit, TRANSLATION_Y, 0),
ObjectAnimator.ofFloat(label, SCALE_X, scale),
ObjectAnimator.ofFloat(label, SCALE_Y, scale),
ObjectAnimator.ofFloat(label, TRANSLATION_Y, transY),
ObjectAnimator.ofFloat(FloatingHintEditTextLayout.this, "animProgress", 0f)
);
edit.setHintTextColor(0);
}else{
label.setScaleX(scale);
label.setScaleY(scale);
label.setTranslationY(transY);
anim.playTogether(
ObjectAnimator.ofFloat(edit, TRANSLATION_Y, offsetY),
ObjectAnimator.ofFloat(label, SCALE_X, 1f),
ObjectAnimator.ofFloat(label, SCALE_Y, 1f),
ObjectAnimator.ofFloat(label, TRANSLATION_Y, 0f),
ObjectAnimator.ofFloat(FloatingHintEditTextLayout.this, "animProgress", 1f)
);
}
anim.setDuration(150);
anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
anim.start();
anim.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
currentAnim=null;
if(hintVisible){
label.setAlpha(0);
edit.setHintTextColor(origHintColors);
}
}
});
currentAnim=anim;
return true;
}
});
currentAnim=anim;
}
@Keep
public void setAnimProgress(float progress){
animProgress=progress;
invalidate();
}
@Keep
public float getAnimProgress(){
return animProgress;
}
@Override
public void onDrawForeground(Canvas canvas){
if(getForeground()!=null && animProgress>0){
canvas.save();
float width=(label.getWidth()+V.dp(8))*animProgress;
float centerX=label.getLeft()+label.getWidth()/2f;
tmpRect.set(centerX-width/2f, label.getTop(), centerX+width/2f, label.getBottom());
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O)
canvas.clipOutRect(tmpRect);
else
canvas.clipRect(tmpRect, Region.Op.DIFFERENCE);
super.onDrawForeground(canvas);
canvas.restore();
}else{
super.onDrawForeground(canvas);
}
}
@Override
public void setForeground(Drawable foreground){
super.setForeground(new PaddedForegroundDrawable(foreground));
}
@Override
public Drawable getForeground(){
if(super.getForeground() instanceof PaddedForegroundDrawable pfd){
return pfd.wrapped;
}
return null;
}
@Override
protected void drawableStateChanged(){
super.drawableStateChanged();
if(label==null || errorState)
return;
ColorStateList color=labelColors==null ? origHintColors : labelColors;
label.setTextColor(color.getColorForState(getDrawableState(), 0xff00ff00));
}
public void setErrorState(){
if(errorState)
return;
errorState=true;
setForeground(getResources().getDrawable(R.drawable.bg_m3_outlined_text_field_error, getContext().getTheme()));
label.setTextColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3Error));
}
private class PaddedForegroundDrawable extends Drawable{
private final Drawable wrapped;
private PaddedForegroundDrawable(Drawable wrapped){
this.wrapped=wrapped;
wrapped.setCallback(new Callback(){
@Override
public void invalidateDrawable(@NonNull Drawable who){
invalidateSelf();
}
@Override
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when){
scheduleSelf(what, when);
}
@Override
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what){
unscheduleSelf(what);
}
});
}
@Override
public void draw(@NonNull Canvas canvas){
wrapped.draw(canvas);
}
@Override
public void setAlpha(int alpha){
wrapped.setAlpha(alpha);
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter){
wrapped.setColorFilter(colorFilter);
}
@Override
public int getOpacity(){
return wrapped.getOpacity();
}
@Override
public boolean setState(@NonNull int[] stateSet){
return wrapped.setState(stateSet);
}
@Override
public int getLayoutDirection(){
return wrapped.getLayoutDirection();
}
@Override
public int getAlpha(){
return wrapped.getAlpha();
}
@Nullable
@Override
public ColorFilter getColorFilter(){
return wrapped.getColorFilter();
}
@Override
public boolean isStateful(){
return wrapped.isStateful();
}
@NonNull
@Override
public int[] getState(){
return wrapped.getState();
}
@NonNull
@Override
public Drawable getCurrent(){
return wrapped.getCurrent();
}
@Override
public void applyTheme(@NonNull Resources.Theme t){
wrapped.applyTheme(t);
}
@Override
public boolean canApplyTheme(){
return wrapped.canApplyTheme();
}
@Override
protected void onBoundsChange(@NonNull Rect bounds){
super.onBoundsChange(bounds);
LayoutParams lp=(LayoutParams) edit.getLayoutParams();
wrapped.setBounds(bounds.left+lp.leftMargin-V.dp(12), bounds.top, bounds.right-lp.rightMargin+V.dp(12), bounds.bottom);
}
}
}

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

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorM3OnSecondaryContainer" android:alpha="0.12"/>
</selector>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorM3OnSurfaceVariant" android:alpha="0.12"/>
</selector>

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="?colorM3Primary" android:state_focused="true"/>
<item android:color="?colorM3OnSurfaceVariant"/>
</selector>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true">
<ripple android:color="@color/m3_on_secondary_container_overlay">
<item>
<shape>
<corners android:radius="8dp"/>
<solid android:color="?colorM3SecondaryContainer"/>
</shape>
</item>
</ripple>
</item>
<item>
<ripple android:color="@color/m3_on_surface_variant_overlay">
<item android:id="@android:id/mask">
<shape>
<corners android:radius="8dp"/>
<solid android:color="#000"/>
</shape>
</item>
<item>
<shape>
<corners android:radius="8dp"/>
<stroke android:color="?colorM3Outline" android:width="1dp"/>
</shape>
</item>
</ripple>
</item>
</selector>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:left="12dp" android:top="12dp" android:right="12dp" android:bottom="12dp">
<selector>
<item android:state_focused="true">
<shape>
<stroke android:color="?colorM3Primary" android:width="2dp"/>
<corners android:radius="4dp"/>
</shape>
</item>
<item>
<shape>
<stroke android:color="?colorM3Outline" android:width="1dp"/>
<corners android:radius="4dp"/>
</shape>
</item>
</selector>
</item>
</layer-list>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:left="12dp" android:top="12dp" android:right="12dp" android:bottom="12dp">
<shape>
<stroke android:color="?colorM3Error" android:width="2dp"/>
<corners android:radius="4dp"/>
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="?colorM3Background"/>
</item>
<item android:id="@+id/color_overlay">
<color android:color="?colorM3Primary"/>
</item>
</layer-list>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<size android:width="8dp"/>
<solid android:color="#00000000"/>
</shape>

View File

@ -0,0 +1,5 @@
<vector android:height="18dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="18dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M7,10l5,5 5,-5z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="18dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="18dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M22,6c0,-1.1 -0.9,-2 -2,-2L4,4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6zM20,6l-8,5 -8,-5h16zM20,18L4,18L4,8l8,5 8,-5v10z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M17,7h-4v2h4c1.65,0 3,1.35 3,3s-1.35,3 -3,3h-4v2h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5zM11,15L7,15c-1.65,0 -3,-1.35 -3,-3s1.35,-3 3,-3h4L11,7L7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-2zM8,11h8v2L8,13z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M2,17h20v2H2V17zM3.15,12.95L4,11.47l0.85,1.48l1.3,-0.75L5.3,10.72H7v-1.5H5.3l0.85,-1.47L4.85,7L4,8.47L3.15,7l-1.3,0.75L2.7,9.22H1v1.5h1.7L1.85,12.2L3.15,12.95zM9.85,12.2l1.3,0.75L12,11.47l0.85,1.48l1.3,-0.75l-0.85,-1.48H15v-1.5h-1.7l0.85,-1.47L12.85,7L12,8.47L11.15,7l-1.3,0.75l0.85,1.47H9v1.5h1.7L9.85,12.2zM23,9.22h-1.7l0.85,-1.47L20.85,7L20,8.47L19.15,7l-1.3,0.75l0.85,1.47H17v1.5h1.7l-0.85,1.48l1.3,0.75L20,11.47l0.85,1.48l1.3,-0.75l-0.85,-1.48H23V9.22z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,6c1.1,0 2,0.9 2,2s-0.9,2 -2,2 -2,-0.9 -2,-2 0.9,-2 2,-2m0,10c2.7,0 5.8,1.29 6,2L6,18c0.23,-0.72 3.31,-2 6,-2m0,-12C9.79,4 8,5.79 8,8s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/>
</vector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="100dp"/>
<solid android:color="#000"/>
</shape>

View File

@ -1,15 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<me.grishka.appkit.views.FragmentRootLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
tools:context=".fragments.onboarding.AccountActivationFragment">
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="?colorBackgroundLight">
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
@ -17,40 +17,67 @@
android:orientation="vertical"
android:clipChildren="false">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_headline_medium"
android:minHeight="36dp"
android:layout_marginTop="32dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="12dp"
android:gravity="center_vertical"
android:text="@string/confirm_email_title"/>
<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:layout_marginStart="56dp"
android:layout_marginEnd="24dp"
android:layout_marginTop="12dp"
android:textAppearance="@style/m3_body_large"
android:textColor="?colorM3OnSurface"
android:text="@string/confirm_email_subtitle"/>
<ImageView
android:layout_width="wrap_content"
android:layout_width="230dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_margin="32dp"
android:layout_marginTop="12dp"
android:importantForAccessibility="no"
android:adjustViewBounds="true"
android:src="@drawable/confirm_email_art"/>
</LinearLayout>
</ScrollView>
<include layout="@layout/button_bar_activation" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_gravity="center_horizontal">
</me.grishka.appkit.views.FragmentRootLinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_label_large"
android:textColor="?colorM3OnSurfaceVariant"
android:text="@string/confirm_email_didnt_get"/>
<Button
android:id="@+id/btn_resend"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="@string/resend"
android:paddingTop="0dp"
android:paddingBottom="0dp"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:fontFeatureSettings="'tnum'"
style="@style/Widget.Mastodon.M3.Button.Text"/>
</LinearLayout>
<Button
android:id="@+id/btn_next"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:minWidth="145dp"
style="@style/Widget.Mastodon.M3.Button.Filled"
android:text="@string/open_email_app" />
</LinearLayout>

View File

@ -5,14 +5,95 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:id="@+id/appkit_loader_root"
xmlns:android="http://schemas.android.com/apk/res/android"
android:background="?colorBackgroundLight">
xmlns:android="http://schemas.android.com/apk/res/android">
<RelativeLayout
android:id="@+id/top_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:background="@drawable/bg_onboarding_panel">
<EditText
android:id="@+id/search_edit"
android:layout_width="match_parent"
android:layout_height="48dp"
android:elevation="0dp"
android:inputType="textFilter|textUri"
android:layout_toEndOf="@+id/btn_back"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:textAppearance="@style/m3_body_large"
android:paddingTop="0dp"
android:paddingBottom="0dp"
android:paddingStart="12dp"
android:paddingEnd="40dp"
android:drawableStart="@drawable/ic_m3_search"
android:drawablePadding="8dp"
android:drawableTint="?colorM3OnSurfaceVariant"
android:gravity="center_vertical"
android:textColorHint="?colorM3OnSurfaceVariant"
android:textColor="?colorM3OnSurfaceVariant"
android:background="@drawable/round_rect"
android:backgroundTint="?colorM3SurfaceVariant"
android:hint="@string/search_communities"/>
<ImageButton
android:id="@+id/btn_back"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="20dp"
android:layout_marginStart="8dp"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:background="?android:selectableItemBackgroundBorderless"
android:tint="?colorM3OnSurface"
android:contentDescription="@string/back"
android:src="@drawable/ic_arrow_back"/>
<ImageButton
android:id="@+id/clear"
android:layout_width="24dp"
android:layout_height="24dp"
android:background="?android:selectableItemBackgroundBorderless"
android:layout_alignEnd="@id/search_edit"
android:layout_alignTop="@id/search_edit"
android:layout_margin="12dp"
android:contentDescription="@string/clear"
android:tint="?colorM3OnSurfaceVariant"
android:visibility="gone"
android:src="@drawable/ic_m3_cancel"/>
<HorizontalScrollView
android:id="@+id/filters_scroll"
android:layout_below="@id/search_edit"
android:layout_width="match_parent"
android:layout_height="48dp"
android:scrollbars="none">
<LinearLayout
android:id="@+id/filters_container"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
android:paddingTop="8dp"
android:paddingRight="16dp"
android:paddingBottom="8dp"
android:paddingLeft="16dp"
android:showDividers="middle"
android:divider="@drawable/empty_8dp">
</LinearLayout>
</HorizontalScrollView>
</RelativeLayout>
<FrameLayout
android:id="@+id/appkit_loader_content"
android:layout_width="match_parent"
android:layout_height="0px"
android:layout_weight="1">
android:layout_weight="1"
android:background="?colorM3Surface">
<include layout="@layout/loading"
android:id="@+id/loading"/>
@ -33,32 +114,29 @@
android:id="@+id/button_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorBackgroundLight"
android:outlineProvider="bounds"
android:orientation="horizontal"
android:elevation="3dp">
android:orientation="vertical"
android:background="@drawable/bg_onboarding_panel">
<Button
android:id="@+id/btn_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:minWidth="145dp"
style="?secondaryLargeButtonStyle"
android:text="@string/back"/>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1"/>
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
android:textAppearance="@style/m3_body_small"
android:textColor="?colorM3OnSurfaceVariant"
android:text="@string/signup_random_server_explain"/>
<Button
android:id="@+id/btn_next"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:minWidth="145dp"
style="?primaryLargeButtonStyle"
style="@style/Widget.Mastodon.M3.Button.Filled"
android:text="@string/next" />
</LinearLayout>

View File

@ -8,39 +8,24 @@
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="?colorBackgroundLight"/>
android:layout_weight="1"/>
<LinearLayout
android:id="@+id/button_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorBackgroundLight"
android:outlineProvider="bounds"
android:orientation="horizontal"
android:elevation="3dp">
<Button
android:id="@+id/btn_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:minWidth="145dp"
style="?secondaryLargeButtonStyle"
android:text="@string/back"/>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1"/>
android:orientation="horizontal">
<Button
android:id="@+id/btn_next"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:minWidth="145dp"
style="?primaryLargeButtonStyle"
style="@style/Widget.Mastodon.M3.Button.Filled"
android:text="@string/i_agree" />
</LinearLayout>

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<me.grishka.appkit.views.FragmentRootLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -8,8 +9,7 @@
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="?colorBackgroundLight">
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
@ -17,68 +17,58 @@
android:orientation="vertical"
android:clipChildren="false">
<TextView
android:id="@+id/title"
<org.joinmastodon.android.ui.views.FloatingHintEditTextLayout
android:id="@+id/display_name_wrap"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:textAppearance="@style/m3_headline_medium"
android:minHeight="36dp"
android:layout_marginTop="32dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:gravity="center_vertical"
tools:text="@string/signup_title"/>
android:layout_height="80dp"
android:paddingTop="4dp"
app:labelTextColor="@color/m3_outlined_text_field_label"
android:foreground="@drawable/bg_m3_outlined_text_field">
<FrameLayout
android:id="@+id/ava_wrap"
android:layout_width="88dp"
android:layout_height="88dp"
android:layout_gravity="center_horizontal"
android:layout_marginBottom="24dp">
<ImageView
android:id="@+id/avatar"
<EditText
android:id="@+id/display_name"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/default_avatar"/>
android:layout_height="56dp"
android:layout_marginStart="56dp"
android:layout_marginEnd="24dp"
android:layout_marginTop="8dp"
android:padding="16dp"
android:background="@null"
android:elevation="0dp"
android:inputType="textPersonName|textCapWords"
android:autofillHints="name"
android:singleLine="true"
android:hint="@string/display_name"/>
<TextView
android:layout_width="match_parent"
android:layout_height="22dp"
android:layout_gravity="bottom"
android:gravity="center"
android:background="@color/gray_800t"
android:textAppearance="@style/m3_label_large"
android:textColor="#eee"
android:text="@string/edit_photo"/>
<View
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="start|top"
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
android:backgroundTint="?colorM3OnSurfaceVariant"
android:background="@drawable/ic_outline_person_24"/>
</FrameLayout>
</org.joinmastodon.android.ui.views.FloatingHintEditTextLayout>
<EditText
android:id="@+id/display_name"
<org.joinmastodon.android.ui.views.FloatingHintEditTextLayout
android:id="@+id/username_wrap"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:inputType="textPersonName|textCapWords"
android:autofillHints="name"
android:singleLine="true"
android:hint="@string/display_name"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="24dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp">
android:layout_height="80dp"
android:paddingTop="4dp"
app:labelTextColor="@color/m3_outlined_text_field_label"
android:foreground="@drawable/bg_m3_outlined_text_field">
<EditText
android:id="@+id/username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="56dp"
android:layout_marginStart="56dp"
android:layout_marginEnd="24dp"
android:layout_marginTop="8dp"
android:padding="16dp"
android:background="@null"
android:elevation="0dp"
android:inputType="textFilter|textNoSuggestions"
android:autofillHints="username"
android:singleLine="true"
@ -89,70 +79,152 @@
android:id="@+id/domain"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:elevation="5dp"
android:layout_gravity="right|center_vertical"
android:layout_marginRight="20dp"
android:paddingLeft="8dp"
android:paddingRight="16dp"
android:textAppearance="@style/m3_title_medium"
android:textAppearance="@style/m3_body_large"
tools:text="\@mastodon.social"/>
</FrameLayout>
</org.joinmastodon.android.ui.views.FloatingHintEditTextLayout>
<EditText
android:id="@+id/email"
<org.joinmastodon.android.ui.views.FloatingHintEditTextLayout
android:id="@+id/email_wrap"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="8dp"
android:inputType="textEmailAddress"
android:autofillHints="emailAddress"
android:singleLine="true"
android:hint="@string/email"/>
android:layout_height="80dp"
android:paddingTop="4dp"
app:labelTextColor="@color/m3_outlined_text_field_label"
android:foreground="@drawable/bg_m3_outlined_text_field">
<EditText
android:id="@+id/password"
<EditText
android:id="@+id/email"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginStart="56dp"
android:layout_marginEnd="24dp"
android:layout_marginTop="8dp"
android:padding="16dp"
android:background="@null"
android:elevation="0dp"
android:inputType="textEmailAddress"
android:autofillHints="emailAddress"
android:singleLine="true"
android:hint="@string/email"/>
<View
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="start|top"
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
android:backgroundTint="?colorM3OnSurfaceVariant"
android:background="@drawable/ic_outline_email_24"/>
</org.joinmastodon.android.ui.views.FloatingHintEditTextLayout>
<org.joinmastodon.android.ui.views.FloatingHintEditTextLayout
android:id="@+id/password_wrap"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:inputType="textPassword"
android:autofillHints="password"
android:singleLine="true"
android:fontFamily="sans-serif"
android:hint="@string/password"/>
android:layout_height="80dp"
android:paddingTop="4dp"
app:labelTextColor="@color/m3_outlined_text_field_label"
android:foreground="@drawable/bg_m3_outlined_text_field">
<EditText
android:id="@+id/password"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginStart="56dp"
android:layout_marginEnd="24dp"
android:layout_marginTop="8dp"
android:padding="16dp"
android:background="@null"
android:elevation="0dp"
android:inputType="textPassword"
android:autofillHints="password"
android:singleLine="true"
android:fontFamily="sans-serif"
android:hint="@string/password"/>
<View
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="start|top"
android:layout_marginStart="16dp"
android:layout_marginTop="12dp"
android:backgroundTint="?colorM3OnSurfaceVariant"
android:background="@drawable/ic_outline_password_24"/>
</org.joinmastodon.android.ui.views.FloatingHintEditTextLayout>
<org.joinmastodon.android.ui.views.FloatingHintEditTextLayout
android:id="@+id/password_confirm_wrap"
android:layout_width="match_parent"
android:layout_height="80dp"
android:paddingTop="4dp"
app:labelTextColor="@color/m3_outlined_text_field_label"
android:foreground="@drawable/bg_m3_outlined_text_field">
<EditText
android:id="@+id/password_confirm"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginStart="56dp"
android:layout_marginEnd="24dp"
android:layout_marginTop="8dp"
android:padding="16dp"
android:background="@null"
android:elevation="0dp"
android:inputType="textPassword"
android:autofillHints="password"
android:singleLine="true"
android:fontFamily="sans-serif"
android:hint="@string/confirm_password"/>
</org.joinmastodon.android.ui.views.FloatingHintEditTextLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="?android:textColorSecondary"
android:layout_marginStart="56dp"
android:layout_marginEnd="20dp"
android:textAppearance="@style/m3_body_small"
android:textColor="?colorM3OnSurfaceVariant"
android:text="@string/password_note"/>
<EditText
android:id="@+id/reason"
<org.joinmastodon.android.ui.views.FloatingHintEditTextLayout
android:id="@+id/reason_wrap"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:inputType="textCapSentences|textMultiLine"
android:hint="@string/signup_reason"/>
android:paddingTop="4dp"
app:labelTextColor="@color/m3_outlined_text_field_label"
android:foreground="@drawable/bg_m3_outlined_text_field">
<EditText
android:id="@+id/reason"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="56dp"
android:layout_marginEnd="24dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="12dp"
android:padding="16dp"
android:background="@null"
android:elevation="0dp"
android:inputType="textCapSentences|textMultiLine"
android:hint="@string/signup_reason"/>
</org.joinmastodon.android.ui.views.FloatingHintEditTextLayout>
<TextView
android:id="@+id/reason_explain"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="?android:textColorSecondary"
android:layout_marginStart="56dp"
android:layout_marginEnd="20dp"
android:textAppearance="@style/m3_body_small"
android:textColor="?colorM3OnSurfaceVariant"
android:text="@string/signup_reason_note"/>
</LinearLayout>
@ -163,32 +235,18 @@
android:id="@+id/button_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorBackgroundLight"
android:outlineProvider="bounds"
android:orientation="horizontal"
android:elevation="3dp">
<Button
android:id="@+id/btn_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:minWidth="145dp"
style="?secondaryLargeButtonStyle"
android:text="@string/back"/>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1"/>
android:orientation="horizontal">
<Button
android:id="@+id/btn_next"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:minWidth="145dp"
style="?primaryLargeButtonStyle"
style="@style/Widget.Mastodon.M3.Button.Filled"
android:text="@string/next" />
</LinearLayout>

View File

@ -1,61 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:layout_marginTop="32dp"
android:layout_marginLeft="19dp"
android:layout_marginRight="19dp"
android:textAppearance="@style/m3_headline_medium"
android:minHeight="36dp"
android:gravity="center_vertical"
android:text="@string/instance_catalog_title"/>
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="19dp"
android:layout_marginRight="19dp"
android:layout_marginBottom="24dp"
android:textAppearance="@style/m3_title_medium"
android:textColor="?android:textColorSecondary"
android:text="@string/instance_catalog_subtitle"/>
<org.joinmastodon.android.ui.tabs.TabLayout
android:id="@+id/categories_list"
android:layout_width="match_parent"
android:layout_height="72dp"
android:background="@drawable/bg_catalog_tabs"
app:tabIndicator="@drawable/mtrl_tabs_default_indicator"
app:tabIndicatorAnimationMode="elastic"
app:tabIndicatorColor="?android:textColorPrimary"
app:tabMinWidth="120dp"
app:tabMaxWidth="120dp"
app:tabMode="scrollable"/>
<EditText
android:id="@+id/search_edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textFilter|textNoSuggestions"
android:singleLine="true"
android:imeOptions="actionGo"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="19dp"
android:layout_marginBottom="3dp"
android:drawableStart="@drawable/ic_fluent_search_20_regular"
android:drawablePadding="8dp"
android:drawableTint="?android:textColorSecondary"
android:hint="@string/search_communities"/>
</LinearLayout>

View File

@ -3,73 +3,60 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
android:paddingStart="16dp"
android:paddingEnd="24dp"
android:paddingTop="12dp"
android:paddingBottom="12dp">
<RadioButton
android:id="@+id/radiobtn"
android:layout_width="24dp"
android:layout_width="28dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp"
android:layout_centerVertical="true"
android:layout_marginStart="-3dp"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:button="@drawable/ic_round_checkbox"
android:buttonTint="?android:textColorSecondary"
android:buttonTint="@color/m3_radiobutton_tint"
android:background="@null"
android:focusable="false"
android:clickable="false"/>
<ImageView
android:id="@+id/image"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginEnd="16dp"
android:importantForAccessibility="no"
android:layout_toEndOf="@id/radiobtn"
android:scaleType="centerCrop"
tools:src="#0f0"/>
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/radiobtn"
android:layout_toEndOf="@id/image"
android:layout_alignParentTop="true"
android:layout_marginBottom="4dp"
android:textAppearance="@style/m3_title_medium"
android:textAppearance="@style/m3_body_large"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
android:textSize="16sp"
android:minHeight="24dp"
android:textColor="?colorM3OnSurface"
tools:text="mastodon.social"/>
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/radiobtn"
android:layout_toEndOf="@id/image"
android:layout_below="@id/title"
android:layout_marginBottom="8dp"
android:textAppearance="@style/m3_body_medium"
android:textColor="?android:textColorSecondary"
android:textColor="?colorM3OnSurfaceVariant"
android:textSize="14sp"
android:lineSpacingExtra="4sp"
tools:text="General-purpose server run by the lead developer of Mastodon"/>
<TextView
android:id="@+id/user_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/radiobtn"
android:layout_below="@id/description"
android:textAppearance="@style/m3_label_medium"
android:textColor="?android:textColorSecondary"
android:drawableStart="@drawable/ic_fluent_people_community_16_regular"
android:drawableTint="?android:textColorSecondary"
android:drawablePadding="8dp"
tools:text="588.8K"/>
<TextView
android:id="@+id/lang"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/user_count"
android:layout_below="@id/description"
android:layout_marginStart="24dp"
android:textAppearance="@style/m3_label_medium"
android:textColor="?android:textColorSecondary"
android:drawableStart="@drawable/ic_fluent_local_language_16_regular"
android:drawableTint="?android:textColorSecondary"
android:drawablePadding="8dp"
tools:text="EN"/>
</RelativeLayout>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_body_large"
android:textColor="?colorM3OnSurface"
android:paddingStart="56dp"
android:paddingTop="12dp"
android:paddingEnd="24dp"
android:paddingBottom="12dp">
</TextView>

View File

@ -1,44 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:elevation="3dp"
android:background="?colorBackgroundLightest"
android:foreground="?android:selectableItemBackground">
android:paddingTop="12dp"
android:paddingEnd="24dp"
android:paddingBottom="12dp"
android:paddingStart="16dp"
android:textSize="16sp"
android:textColor="?colorM3Primary"
android:drawableStart="@drawable/ic_outline_link_24"
android:drawablePadding="16dp"
android:drawableTint="?colorM3Primary"
tools:text="Privacy Policy - example.social">
<ImageView
android:id="@+id/favicon"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginTop="2dp"
android:importantForAccessibility="no"
tools:src="#0f0"/>
<TextView
android:id="@+id/domain"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_toEndOf="@id/favicon"
android:layout_marginStart="4dp"
android:singleLine="true"
android:ellipsize="end"
android:textAppearance="@style/m3_title_small"
android:textColor="?android:textColorPrimary"
android:gravity="center_vertical"
tools:text="joinmastodon.org"/>
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="24dp"
android:layout_below="@id/domain"
android:layout_marginTop="6dp"
android:textAppearance="@style/m3_title_medium"
android:singleLine="true"
android:ellipsize="end"
android:gravity="center_vertical"
tools:text="Mastodon for Android privacy policy"/>
</RelativeLayout>
</TextView>

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="12dp"
android:paddingEnd="24dp"
android:paddingBottom="12dp"
android:paddingStart="16dp"
android:baselineAligned="false">
<TextView
android:id="@+id/number"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp"
android:textColor="?colorM3Primary"
android:fontFamily="sans-serif-condensed"
android:textStyle="bold"
android:textSize="22dp"
android:gravity="center"
android:includeFontPadding="false"
tools:text="1"/>
<org.joinmastodon.android.ui.views.LinkedTextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:textAppearance="@style/m3_body_large"
tools:text="No discrimination, including (but not limited to) racism, sexism, homophobia or transphobia."/>
</LinearLayout>

View File

@ -37,6 +37,10 @@
<attr name="colorM3Outline" format="color"/>
<attr name="colorM3DisabledBackground" format="color"/>
<attr name="colorM3PressedOverlay" format="color"/>
<attr name="colorM3Error" format="color"/>
<attr name="colorM3OnError" format="color"/>
<attr name="colorM3ErrorContainer" format="color"/>
<attr name="colorM3OnErrorContainer" format="color"/>
<attr name="primaryLargeButtonStyle" format="reference"/>
<attr name="secondaryLargeButtonStyle" format="reference"/>
@ -48,5 +52,6 @@
<declare-styleable name="FloatingHintEditTextLayout">
<attr name="editTextOffsetY" format="dimension"/>
<attr name="android:labelTextSize" format="dimension"/>
<attr name="labelTextColor" format="color"/>
</declare-styleable>
</resources>

View File

@ -179,15 +179,16 @@
<string name="back">Back</string>
<string name="instance_catalog_title">Mastodon is made of users on different servers.</string>
<string name="instance_catalog_subtitle">Pick a server based on your interests, region, or a general purpose one. You can still connect with everyone, regardless of server.</string>
<string name="search_communities">Search servers or enter URL</string>
<string name="instance_rules_title">Some ground rules</string>
<string name="instance_rules_subtitle">Take a minute to review the rules set and enforced by %s admins.</string>
<string name="signup_title">Let\'s get you set up on %s</string>
<string name="search_communities">Server name or URL</string>
<string name="instance_rules_title">Server Rules</string>
<string name="instance_rules_subtitle">By continuing, you agree to follow by the following rules set and enforced by the %s moderators.</string>
<string name="signup_title">Create Account</string>
<string name="edit_photo">edit</string>
<string name="display_name">display name</string>
<string name="username">username</string>
<string name="email">email</string>
<string name="password">password</string>
<string name="display_name">Name</string>
<string name="username">Username</string>
<string name="email">Email</string>
<string name="password">Password</string>
<string name="confirm_password">Confirm password</string>
<string name="password_note">Include capital letters, special characters, and numbers to increase your password strength.</string>
<string name="category_academia">Academia</string>
<string name="category_activism">Activism</string>
@ -202,8 +203,10 @@
<string name="category_music">Music</string>
<string name="category_regional">Regional</string>
<string name="category_tech">Tech</string>
<string name="confirm_email_title">One last thing</string>
<string name="confirm_email_subtitle">Tap the link we emailed to you to verify your account.</string>
<string name="confirm_email_title">Check Your Inbox</string>
<!-- %s is the email address -->
<string name="confirm_email_subtitle">Tap the link we sent you to verify %s. We\'ll wait right here.</string>
<string name="confirm_email_didnt_get">Didn\'t get a link?</string>
<string name="resend">Resend</string>
<string name="open_email_app">Open email app</string>
<string name="resent_email">Confirmation email sent</string>
@ -291,7 +294,7 @@
<string name="open_in_browser">Open in browser</string>
<string name="hide_boosts_from_user">Hide reblogs from %s</string>
<string name="show_boosts_from_user">Show reblogs from %s</string>
<string name="signup_reason">why do you want to join?</string>
<string name="signup_reason">Why do you want to join?</string>
<string name="signup_reason_note">This will help us review your application.</string>
<string name="clear">Clear</string>
<string name="profile_header">Header image</string>
@ -385,7 +388,7 @@
<!-- %s is file size -->
<string name="download_update">Download (%s)</string>
<string name="install_update">Install</string>
<string name="privacy_policy_title">Mastodon and your privacy</string>
<string name="privacy_policy_title">Your Privacy</string>
<string name="privacy_policy_subtitle">Although the Mastodon app does not collect any data, the server you sign up through may have a different policy. Take a minute to review and agree to the Mastodon app privacy policy and your server\'s privacy policy.</string>
<string name="i_agree">I Agree</string>
<string name="empty_list">This list is empty</string>
@ -405,4 +408,18 @@
<string name="welcome_page2_text">Your handle might be @gothgirl654@example.social, but you can still follow, reblog, and chat with @fallout5ever@example.online.</string>
<string name="welcome_page3_title">How do I pick a server?</string>
<string name="welcome_page3_text">Different people choose different servers for any number of reasons. art.example is a great place for artists, while glasgow.example might be a good pick for Scots.\n\nYou cant go wrong with any of our recommend servers, so regardless of which one you pick (or if you enter your own in the server search bar), youll never miss a beat anywhere.</string>
<string name="signup_random_server_explain">We\'ll pick a server based on your language if you continue without making a selection.</string>
<string name="server_filter_any_language">Any Language</string>
<string name="server_filter_instant_signup">Instant Sign-up</string>
<string name="server_filter_manual_review">Manual Review</string>
<string name="server_filter_any_signup_speed">Any Sign-up Speed</string>
<string name="server_filter_region_europe">Europe</string>
<string name="server_filter_region_north_america">North America</string>
<string name="server_filter_region_south_america">South America</string>
<string name="server_filter_region_africa">Africa</string>
<string name="server_filter_region_asia">Asia</string>
<string name="server_filter_region_oceania">Oceania</string>
<string name="not_accepting_new_members">Not accepting new members</string>
<string name="category_special_interests">Special Interests</string>
<string name="signup_passwords_dont_match">Passwords don\'t match</string>
</resources>

View File

@ -66,6 +66,10 @@
<item name="colorM3Outline">@color/m3_sys_light_outline</item>
<item name="colorM3DisabledBackground">#1F1F1F1F</item>
<item name="colorM3PressedOverlay">@color/m3_sys_light_on_primary</item>
<item name="colorM3Error">#B3261E</item>
<item name="colorM3OnError">#FFF</item>
<item name="colorM3ErrorContainer">#F9DEDC</item>
<item name="colorM3OnErrorContainer">#410E0B</item>
</style>
<style name="Theme.Mastodon.Dark" parent="Theme.AppKit">
@ -136,6 +140,10 @@
<item name="colorM3Outline">@color/m3_sys_dark_outline</item>
<item name="colorM3DisabledBackground">#1FE3E3E3</item>
<item name="colorM3PressedOverlay">@color/m3_sys_dark_primary</item>
<item name="colorM3Error">#F2B8B5</item>
<item name="colorM3OnError">#601410</item>
<item name="colorM3ErrorContainer">#8C1D18</item>
<item name="colorM3OnErrorContainer">#F9DEDC</item>
</style>
<style name="Theme.Mastodon.Dark.TrueBlack">
@ -323,6 +331,12 @@
<item name="android:lineSpacingExtra">4dp</item>
</style>
<style name="m3_body_small">
<item name="android:textSize">12dp</item>
<item name="android:textColor">?android:textColorSecondary</item>
<item name="android:lineSpacingExtra">2dp</item>
</style>
<style name="m3_title_medium">
<item name="android:fontFamily">sans-serif-medium</item>
<item name="android:textSize">16dp</item>

View File

@ -1,11 +1,8 @@
// Run: java tools/GenerateLocaleConfig.java
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.stream.*;
public class GenerateLocaleConfig{
public static void main(String[] args) throws IOException{
@ -15,7 +12,7 @@ public class GenerateLocaleConfig{
if(!dir.exists())
throw new RuntimeException("Please run from project directory (can't find mastodon/src/main/res)");
ArrayList<String> locales=new ArrayList<>();
ArrayList<String> locales=new ArrayList<>(), rawLocales=new ArrayList<>();
locales.add("en");
for(File file:dir.listFiles()){
@ -23,11 +20,13 @@ public class GenerateLocaleConfig{
if(file.isDirectory() && name.startsWith("values-")){
if(new File(file, "strings.xml").exists()){
locales.add(name.substring(name.indexOf('-')+1).replace("-r", "-"));
rawLocales.add(name.substring(name.indexOf('-')+1));
}
}
}
locales.sort(String::compareTo);
rawLocales.sort(String::compareTo);
try(OutputStreamWriter writer=new OutputStreamWriter(new FileOutputStream(new File(dir, "xml/locales_config.xml")), StandardCharsets.UTF_8)){
writer.write("""
<?xml version="1.0" encoding="utf-8"?>
@ -40,5 +39,25 @@ public class GenerateLocaleConfig{
}
writer.write("</locale-config>");
}
File buildGradle=new File(dir, "../../../build.gradle");
ArrayList<String> buildGradleLines=new ArrayList<>();
try(BufferedReader reader=new BufferedReader(new InputStreamReader(new FileInputStream(buildGradle)))){
String line;
while((line=reader.readLine())!=null){
if(line.trim().startsWith("resConfigs")){
line=line.substring(0, line.indexOf('r'))+"resConfigs ";
line+=rawLocales.stream().map(l->'"'+l+'"').collect(Collectors.joining(", "));
}
buildGradleLines.add(line);
}
}
try(OutputStreamWriter writer=new OutputStreamWriter(new FileOutputStream(buildGradle))){
for(String line:buildGradleLines){
writer.write(line);
writer.write('\n');
}
}
}
}