Settings and other things

This commit is contained in:
Grishka
2022-04-06 03:11:15 +03:00
parent 0661ce265a
commit f73669124f
63 changed files with 1318 additions and 134 deletions

View File

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

View File

@@ -2,7 +2,6 @@ package org.joinmastodon.android;
import android.app.Fragment; import android.app.Fragment;
import android.content.ClipData; import android.content.ClipData;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.ColorDrawable;
import android.net.Uri; import android.net.Uri;
@@ -14,11 +13,11 @@ import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.ComposeFragment; import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import me.grishka.appkit.FragmentStackActivity; import me.grishka.appkit.FragmentStackActivity;
@@ -26,6 +25,7 @@ import me.grishka.appkit.FragmentStackActivity;
public class ExternalShareActivity extends FragmentStackActivity{ public class ExternalShareActivity extends FragmentStackActivity{
@Override @Override
protected void onCreate(@Nullable Bundle savedInstanceState){ protected void onCreate(@Nullable Bundle savedInstanceState){
UiUtils.setUserPreferredTheme(this);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
if(savedInstanceState==null){ if(savedInstanceState==null){
List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts(); List<AccountSession> sessions=AccountSessionManager.getInstance().getLoggedInAccounts();

View File

@@ -0,0 +1,38 @@
package org.joinmastodon.android;
import android.content.Context;
import android.content.SharedPreferences;
public class GlobalUserPreferences{
public static boolean playGifs;
public static boolean useCustomTabs;
public static boolean trueBlackTheme;
public static ThemePreference theme;
private static SharedPreferences getPrefs(){
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
}
public static void load(){
SharedPreferences prefs=getPrefs();
playGifs=prefs.getBoolean("playGifs", true);
useCustomTabs=prefs.getBoolean("useCustomTabs", true);
trueBlackTheme=prefs.getBoolean("trueBlackTheme", false);
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
}
public static void save(){
getPrefs().edit()
.putBoolean("playGifs", playGifs)
.putBoolean("useCustomTabs", useCustomTabs)
.putBoolean("trueBlackTheme", trueBlackTheme)
.putInt("theme", theme.ordinal())
.apply();
}
public enum ThemePreference{
AUTO,
LIGHT,
DARK
}
}

View File

@@ -15,6 +15,7 @@ import org.joinmastodon.android.fragments.SplashFragment;
import org.joinmastodon.android.fragments.ThreadFragment; import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment; import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels; import org.parceler.Parcels;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
@@ -25,6 +26,7 @@ import me.grishka.appkit.FragmentStackActivity;
public class MainActivity extends FragmentStackActivity{ public class MainActivity extends FragmentStackActivity{
@Override @Override
protected void onCreate(@Nullable Bundle savedInstanceState){ protected void onCreate(@Nullable Bundle savedInstanceState){
UiUtils.setUserPreferredTheme(this);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
if(savedInstanceState==null){ if(savedInstanceState==null){

View File

@@ -27,5 +27,6 @@ public class MastodonApp extends Application{
context=getApplicationContext(); context=getApplicationContext();
PushSubscriptionManager.tryRegisterFCM(); PushSubscriptionManager.tryRegisterFCM();
GlobalUserPreferences.load();
} }
} }

View File

@@ -16,6 +16,7 @@ import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Application; import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Token; import org.joinmastodon.android.model.Token;
import org.joinmastodon.android.ui.utils.UiUtils;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
@@ -24,6 +25,7 @@ import me.grishka.appkit.api.ErrorResponse;
public class OAuthActivity extends Activity{ public class OAuthActivity extends Activity{
@Override @Override
protected void onCreate(@Nullable Bundle savedInstanceState){ protected void onCreate(@Nullable Bundle savedInstanceState){
UiUtils.setUserPreferredTheme(this);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
Uri uri=getIntent().getData(); Uri uri=getIntent().getData();
if(uri==null || isTaskRoot()){ if(uri==null || isTaskRoot()){

View File

@@ -1,7 +1,6 @@
package org.joinmastodon.android.api; package org.joinmastodon.android.api;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteException;
@@ -14,8 +13,6 @@ import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.api.requests.notifications.GetNotifications; import org.joinmastodon.android.api.requests.notifications.GetNotifications;
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline; import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.SearchResult; import org.joinmastodon.android.model.SearchResult;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
@@ -26,7 +23,6 @@ import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
import androidx.annotation.Nullable;
import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.utils.WorkerThread; import me.grishka.appkit.utils.WorkerThread;
@@ -87,7 +83,7 @@ public class CacheController{
.exec(accountID); .exec(accountID);
}catch(SQLiteException x){ }catch(SQLiteException x){
Log.w(TAG, x); Log.w(TAG, x);
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage()))); uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500)));
}finally{ }finally{
closeDelayed(); closeDelayed();
} }
@@ -145,7 +141,7 @@ public class CacheController{
.exec(accountID); .exec(accountID);
}catch(SQLiteException x){ }catch(SQLiteException x){
Log.w(TAG, x); Log.w(TAG, x);
uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage()))); uiHandler.post(()->callback.onError(new MastodonErrorResponse(x.getLocalizedMessage(), 500)));
}finally{ }finally{
closeDelayed(); closeDelayed();
} }

View File

@@ -103,7 +103,7 @@ public class MastodonAPIController{
synchronized(req){ synchronized(req){
req.okhttpCall=null; req.okhttpCall=null;
} }
req.onError(e.getLocalizedMessage()); req.onError(e.getLocalizedMessage(), 0);
} }
@Override @Override
@@ -136,7 +136,7 @@ public class MastodonAPIController{
}catch(JsonIOException|JsonSyntaxException x){ }catch(JsonIOException|JsonSyntaxException x){
if(BuildConfig.DEBUG) if(BuildConfig.DEBUG)
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x); Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error parsing or reading body", x);
req.onError(x.getLocalizedMessage()); req.onError(x.getLocalizedMessage(), response.code());
return; return;
} }
@@ -145,7 +145,7 @@ public class MastodonAPIController{
}catch(IOException x){ }catch(IOException x){
if(BuildConfig.DEBUG) if(BuildConfig.DEBUG)
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error post-processing or validating response", x); Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" error post-processing or validating response", x);
req.onError(x.getLocalizedMessage()); req.onError(x.getLocalizedMessage(), response.code());
return; return;
} }
@@ -157,11 +157,11 @@ public class MastodonAPIController{
try{ try{
JsonObject error=JsonParser.parseReader(reader).getAsJsonObject(); JsonObject error=JsonParser.parseReader(reader).getAsJsonObject();
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" received error: "+error); Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] "+response+" received error: "+error);
req.onError(error.get("error").getAsString()); req.onError(error.get("error").getAsString(), response.code());
}catch(JsonIOException|JsonSyntaxException x){ }catch(JsonIOException|JsonSyntaxException x){
req.onError(response.code()+" "+response.message()); req.onError(response.code()+" "+response.message(), response.code());
}catch(IllegalStateException x){ }catch(IllegalStateException x){
req.onError("Error parsing an API error"); req.onError("Error parsing an API error", response.code());
} }
} }
} }
@@ -170,7 +170,7 @@ public class MastodonAPIController{
}catch(Exception x){ }catch(Exception x){
if(BuildConfig.DEBUG) if(BuildConfig.DEBUG)
Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] error creating and sending http request", x); Log.w(TAG, "["+(session==null ? "no-auth" : session.getID())+"] error creating and sending http request", x);
req.onError(x.getLocalizedMessage()); req.onError(x.getLocalizedMessage(), 0);
} }
}, 0); }, 0);
} }

View File

@@ -2,13 +2,13 @@ package org.joinmastodon.android.api;
import android.app.Activity; import android.app.Activity;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.net.Uri; import android.net.Uri;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.BaseModel; import org.joinmastodon.android.model.BaseModel;
@@ -60,6 +60,8 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
@Override @Override
public synchronized void cancel(){ public synchronized void cancel(){
if(BuildConfig.DEBUG)
Log.d(TAG, "canceling request "+this);
canceled=true; canceled=true;
if(okhttpCall!=null){ if(okhttpCall!=null){
okhttpCall.cancel(); okhttpCall.cancel();
@@ -181,8 +183,8 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
} }
} }
void onError(String msg){ void onError(String msg, int httpStatus){
invokeErrorCallback(new MastodonErrorResponse(msg)); invokeErrorCallback(new MastodonErrorResponse(msg, httpStatus));
} }
void onSuccess(T resp){ void onSuccess(T resp){

View File

@@ -11,9 +11,11 @@ import me.grishka.appkit.api.ErrorResponse;
public class MastodonErrorResponse extends ErrorResponse{ public class MastodonErrorResponse extends ErrorResponse{
public final String error; public final String error;
public final int httpStatus;
public MastodonErrorResponse(String error){ public MastodonErrorResponse(String error, int httpStatus){
this.error=error; this.error=error;
this.httpStatus=httpStatus;
} }
@Override @Override

View File

@@ -13,6 +13,7 @@ import android.util.Log;
import org.joinmastodon.android.BuildConfig; import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.api.requests.notifications.RegisterForPushNotifications; import org.joinmastodon.android.api.requests.notifications.RegisterForPushNotifications;
import org.joinmastodon.android.api.requests.notifications.UpdatePushSettings;
import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.PushNotification; import org.joinmastodon.android.model.PushNotification;
@@ -119,6 +120,10 @@ public class PushSubscriptionManager{
} }
public void registerAccountForPush(){ public void registerAccountForPush(){
registerAccountForPush(null);
}
public void registerAccountForPush(PushSubscription subscription){
if(TextUtils.isEmpty(deviceToken)) if(TextUtils.isEmpty(deviceToken))
throw new IllegalStateException("No device push token available"); throw new IllegalStateException("No device push token available");
MastodonAPIController.runInBackground(()->{ MastodonAPIController.runInBackground(()->{
@@ -143,19 +148,22 @@ public class PushSubscriptionManager{
Log.e(TAG, "registerAccountForPush: error generating encryption key", e); Log.e(TAG, "registerAccountForPush: error generating encryption key", e);
return; return;
} }
new RegisterForPushNotifications(deviceToken, encodedPublicKey, encodedAuthKey, PushSubscription.Alerts.ofAll(), accountID) new RegisterForPushNotifications(deviceToken,
encodedPublicKey,
encodedAuthKey,
subscription==null ? PushSubscription.Alerts.ofAll() : subscription.alerts,
subscription==null ? PushSubscription.Policy.ALL : subscription.policy,
accountID)
.setCallback(new Callback<>(){ .setCallback(new Callback<>(){
@Override @Override
public void onSuccess(PushSubscription result){ public void onSuccess(PushSubscription result){
MastodonAPIController.runInBackground(()->{ MastodonAPIController.runInBackground(()->{
serverKey=deserializeRawPublicKey(Base64.decode(result.serverKey, Base64.URL_SAFE)); serverKey=deserializeRawPublicKey(Base64.decode(result.serverKey, Base64.URL_SAFE));
if(serverKey!=null){
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID); AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
session.pushServerKey=Base64.encodeToString(serverKey.getEncoded(), Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING); session.pushSubscription=result;
AccountSessionManager.getInstance().writeAccountsFile(); AccountSessionManager.getInstance().writeAccountsFile();
Log.d(TAG, "Successfully registered "+accountID+" for push notifications"); Log.d(TAG, "Successfully registered "+accountID+" for push notifications");
}
}); });
} }
@@ -168,6 +176,34 @@ public class PushSubscriptionManager{
}); });
} }
public void updatePushSettings(PushSubscription subscription){
new UpdatePushSettings(subscription.alerts, subscription.policy)
.setCallback(new Callback<>(){
@Override
public void onSuccess(PushSubscription result){
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
if(result.policy!=subscription.policy)
result.policy=subscription.policy;
session.pushSubscription=result;
session.needUpdatePushSettings=false;
AccountSessionManager.getInstance().writeAccountsFile();
}
@Override
public void onError(ErrorResponse error){
if(((MastodonErrorResponse)error).httpStatus==404){ // Not registered for push, register now
registerAccountForPush(subscription);
}else{
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
session.needUpdatePushSettings=true;
session.pushSubscription=subscription;
AccountSessionManager.getInstance().writeAccountsFile();
}
}
})
.exec(accountID);
}
private PublicKey deserializeRawPublicKey(byte[] rawBytes){ private PublicKey deserializeRawPublicKey(byte[] rawBytes){
if(rawBytes.length!=65 && rawBytes.length!=64) if(rawBytes.length!=65 && rawBytes.length!=64)
return null; return null;
@@ -320,8 +356,10 @@ public class PushSubscriptionManager{
private static void registerAllAccountsForPush(){ private static void registerAllAccountsForPush(){
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){ for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
if(TextUtils.isEmpty(session.pushServerKey)) if(session.pushSubscription==null)
session.getPushSubscriptionManager().registerAccountForPush(); session.getPushSubscriptionManager().registerAccountForPush();
else if(session.needUpdatePushSettings)
session.getPushSubscriptionManager().updatePushSettings(session.pushSubscription);
} }
} }

View File

@@ -4,11 +4,12 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.PushSubscription; import org.joinmastodon.android.model.PushSubscription;
public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscription>{ public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscription>{
public RegisterForPushNotifications(String deviceToken, String encryptionKey, String authKey, PushSubscription.Alerts alerts, String accountID){ public RegisterForPushNotifications(String deviceToken, String encryptionKey, String authKey, PushSubscription.Alerts alerts, PushSubscription.Policy policy, String accountID){
super(HttpMethod.POST, "/push/subscription", PushSubscription.class); super(HttpMethod.POST, "/push/subscription", PushSubscription.class);
Request r=new Request(); Request r=new Request();
r.subscription.endpoint="https://app.joinmastodon.org/relay-to/fcm/"+deviceToken+"/"+accountID; r.subscription.endpoint="https://app.joinmastodon.org/relay-to/fcm/"+deviceToken+"/"+accountID;
r.data.alerts=alerts; r.data.alerts=alerts;
r.data.policy=policy;
r.subscription.keys.p256dh=encryptionKey; r.subscription.keys.p256dh=encryptionKey;
r.subscription.keys.auth=authKey; r.subscription.keys.auth=authKey;
setRequestBody(r); setRequestBody(r);
@@ -30,6 +31,7 @@ public class RegisterForPushNotifications extends MastodonAPIRequest<PushSubscri
private static class Data{ private static class Data{
public PushSubscription.Alerts alerts; public PushSubscription.Alerts alerts;
public PushSubscription.Policy policy;
} }
} }
} }

View File

@@ -0,0 +1,25 @@
package org.joinmastodon.android.api.requests.notifications;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.PushSubscription;
public class UpdatePushSettings extends MastodonAPIRequest<PushSubscription>{
public UpdatePushSettings(PushSubscription.Alerts alerts, PushSubscription.Policy policy){
super(HttpMethod.PUT, "/push/subscription", PushSubscription.class);
setRequestBody(new Request(alerts, policy));
}
private static class Request{
public Data data=new Data();
public Request(PushSubscription.Alerts alerts, PushSubscription.Policy policy){
this.data.alerts=alerts;
this.data.policy=policy;
}
private static class Data{
public PushSubscription.Alerts alerts;
public PushSubscription.Policy policy;
}
}
}

View File

@@ -6,7 +6,7 @@ import org.joinmastodon.android.api.PushSubscriptionManager;
import org.joinmastodon.android.api.StatusInteractionController; import org.joinmastodon.android.api.StatusInteractionController;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Application; import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.PushSubscription;
import org.joinmastodon.android.model.Token; import org.joinmastodon.android.model.Token;
public class AccountSession{ public class AccountSession{
@@ -19,7 +19,8 @@ public class AccountSession{
public String pushPrivateKey; public String pushPrivateKey;
public String pushPublicKey; public String pushPublicKey;
public String pushAuthKey; public String pushAuthKey;
public String pushServerKey; public PushSubscription pushSubscription;
public boolean needUpdatePushSettings;
private transient MastodonAPIController apiController; private transient MastodonAPIController apiController;
private transient StatusInteractionController statusInteractionController; private transient StatusInteractionController statusInteractionController;
private transient CacheController cacheController; private transient CacheController cacheController;

View File

@@ -9,8 +9,10 @@ import android.graphics.drawable.Drawable;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.Toolbar; import android.widget.Toolbar;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
@@ -592,6 +594,26 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
return list.getWidth(); return list.getWidth();
} }
protected boolean wantsOverlaySystemNavigation(){
return true;
}
protected void onSetFabBottomInset(int inset){
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0 && wantsOverlaySystemNavigation()){
list.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
onSetFabBottomInset(insets.getSystemWindowInsetBottom());
insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom());
}else{
onSetFabBottomInset(0);
}
super.onApplyWindowInsets(insets);
}
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{ protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
public DisplayItemsAdapter(){ public DisplayItemsAdapter(){

View File

@@ -993,32 +993,7 @@ public class ComposeFragment extends ToolbarFragment implements OnBackPressedLis
PopupMenu menu=new PopupMenu(getActivity(), v); PopupMenu menu=new PopupMenu(getActivity(), v);
menu.inflate(R.menu.compose_visibility); menu.inflate(R.menu.compose_visibility);
Menu m=menu.getMenu(); Menu m=menu.getMenu();
if(Build.VERSION.SDK_INT>=29){ UiUtils.enablePopupMenuIcons(getActivity(), menu);
menu.setForceShowIcon(true);
}else{
try{
Method setOptionalIconsVisible=m.getClass().getDeclaredMethod("setOptionalIconsVisible", boolean.class);
setOptionalIconsVisible.setAccessible(true);
setOptionalIconsVisible.invoke(m, true);
}catch(Exception ignore){}
}
ColorStateList iconTint=ColorStateList.valueOf(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorSecondary));
for(int i=0;i<m.size();i++){
MenuItem item=m.getItem(i);
Drawable icon=item.getIcon().mutate();
if(Build.VERSION.SDK_INT>=26){
item.setIconTintList(iconTint);
}else{
icon.setTintList(iconTint);
}
icon=new InsetDrawable(icon, V.dp(8), 0, 0, 0);
item.setIcon(icon);
SpannableStringBuilder ssb=new SpannableStringBuilder(item.getTitle());
ssb.insert(0, " ");
ssb.setSpan(new SpacerSpan(V.dp(24), 1), 0, 1, 0);
ssb.append(" ", new SpacerSpan(V.dp(8), 1), 0);
item.setTitle(ssb);
}
m.setGroupCheckable(0, true, true); m.setGroupCheckable(0, true, true);
m.findItem(switch(statusVisibility){ m.findItem(switch(statusVisibility){
case PUBLIC, UNLISTED -> R.id.vis_public; case PUBLIC, UNLISTED -> R.id.vis_public;
@@ -1113,6 +1088,16 @@ public class ComposeFragment extends ToolbarFragment implements OnBackPressedLis
finishAutocomplete(); finishAutocomplete();
} }
@Override
public boolean wantsLightStatusBar(){
return !UiUtils.isDarkTheme();
}
@Override
public boolean wantsLightNavigationBar(){
return !UiUtils.isDarkTheme();
}
@Parcel @Parcel
static class DraftMediaAttachment{ static class DraftMediaAttachment{
public Attachment serverAttachment; public Attachment serverAttachment;

View File

@@ -3,6 +3,7 @@ package org.joinmastodon.android.fragments;
import android.app.Activity; import android.app.Activity;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton; import android.widget.ImageButton;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
@@ -13,6 +14,7 @@ import java.util.List;
import me.grishka.appkit.Nav; import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback; import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.V;
public class HashtagTimelineFragment extends StatusListFragment{ public class HashtagTimelineFragment extends StatusListFragment{
private String hashtag; private String hashtag;
@@ -61,4 +63,9 @@ public class HashtagTimelineFragment extends StatusListFragment{
args.putString("prefilledText", '#'+hashtag+' '); args.putString("prefilledText", '#'+hashtag+' ');
Nav.go(getActivity(), ComposeFragment.class, args); Nav.go(getActivity(), ComposeFragment.class, args);
} }
@Override
protected void onSetFabBottomInset(int inset){
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+inset;
}
} }

View File

@@ -2,10 +2,12 @@ package org.joinmastodon.android.fragments;
import android.app.Fragment; import android.app.Fragment;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.content.Intent;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.graphics.Outline; import android.graphics.Outline;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -16,18 +18,28 @@ import android.widget.FrameLayout;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.PushNotificationReceiver; import org.joinmastodon.android.PushNotificationReceiver;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.discover.DiscoverFragment; import org.joinmastodon.android.fragments.discover.DiscoverFragment;
import org.joinmastodon.android.fragments.discover.SearchFragment;
import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.TabBar; import org.joinmastodon.android.ui.views.TabBar;
import org.parceler.Parcels; import org.parceler.Parcels;
import java.util.ArrayList;
import androidx.annotation.IdRes; import androidx.annotation.IdRes;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import me.grishka.appkit.FragmentStackActivity; import me.grishka.appkit.FragmentStackActivity;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.AppKitFragment; import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.fragments.LoaderFragment; import me.grishka.appkit.fragments.LoaderFragment;
import me.grishka.appkit.fragments.OnBackPressedListener; import me.grishka.appkit.fragments.OnBackPressedListener;
@@ -58,6 +70,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N) if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
setRetainInstance(true); setRetainInstance(true);
if(savedInstanceState==null){
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);
homeTimelineFragment=new HomeTimelineFragment(); homeTimelineFragment=new HomeTimelineFragment();
@@ -73,6 +86,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
args.putBoolean("noAutoLoad", true); args.putBoolean("noAutoLoad", true);
profileFragment=new ProfileFragment(); profileFragment=new ProfileFragment();
profileFragment.setArguments(args); profileFragment.setArguments(args);
}
} }
@@ -88,7 +102,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
inflater.inflate(R.layout.tab_bar, content); inflater.inflate(R.layout.tab_bar, content);
tabBar=content.findViewById(R.id.tabbar); tabBar=content.findViewById(R.id.tabbar);
tabBar.setListener(this::onTabSelected); tabBar.setListeners(this::onTabSelected, this::onTabLongClick);
tabBarWrap=content.findViewById(R.id.tabbar_wrap); tabBarWrap=content.findViewById(R.id.tabbar_wrap);
tabBarAvatar=tabBar.findViewById(R.id.tab_profile_ava); tabBarAvatar=tabBar.findViewById(R.id.tab_profile_ava);
@@ -123,12 +137,32 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
}); });
} }
}else{ }else{
tabBar.selectTab(currentTab);
} }
return content; return content;
} }
@Override
public void onViewStateRestored(Bundle savedInstanceState){
super.onViewStateRestored(savedInstanceState);
if(savedInstanceState==null || homeTimelineFragment!=null)
return;
homeTimelineFragment=(HomeTimelineFragment) getChildFragmentManager().getFragment(savedInstanceState, "homeTimelineFragment");
searchFragment=(DiscoverFragment) getChildFragmentManager().getFragment(savedInstanceState, "searchFragment");
notificationsFragment=(NotificationsFragment) getChildFragmentManager().getFragment(savedInstanceState, "notificationsFragment");
profileFragment=(ProfileFragment) getChildFragmentManager().getFragment(savedInstanceState, "profileFragment");
currentTab=savedInstanceState.getInt("selectedTab");
Fragment current=fragmentForTab(currentTab);
getChildFragmentManager().beginTransaction()
.hide(homeTimelineFragment)
.hide(searchFragment)
.hide(notificationsFragment)
.hide(profileFragment)
.show(current)
.commit();
maybeTriggerLoading(current);
}
@Override @Override
public void onHiddenChanged(boolean hidden){ public void onHiddenChanged(boolean hidden){
super.onHiddenChanged(hidden); super.onHiddenChanged(hidden);
@@ -137,12 +171,12 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
@Override @Override
public boolean wantsLightStatusBar(){ public boolean wantsLightStatusBar(){
return currentTab!=R.id.tab_profile && (MastodonApp.context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)!=Configuration.UI_MODE_NIGHT_YES; return currentTab!=R.id.tab_profile && !UiUtils.isDarkTheme();
} }
@Override @Override
public boolean wantsLightNavigationBar(){ public boolean wantsLightNavigationBar(){
return (MastodonApp.context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)!=Configuration.UI_MODE_NIGHT_YES; return !UiUtils.isDarkTheme();
} }
@Override @Override
@@ -182,6 +216,12 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
return; return;
} }
getChildFragmentManager().beginTransaction().hide(fragmentForTab(currentTab)).show(newFragment).commit(); getChildFragmentManager().beginTransaction().hide(fragmentForTab(currentTab)).show(newFragment).commit();
maybeTriggerLoading(newFragment);
currentTab=tab;
((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this);
}
private void maybeTriggerLoading(Fragment newFragment){
if(newFragment instanceof LoaderFragment){ if(newFragment instanceof LoaderFragment){
LoaderFragment lf=(LoaderFragment) newFragment; LoaderFragment lf=(LoaderFragment) newFragment;
if(!lf.loaded && !lf.dataLoading) if(!lf.loaded && !lf.dataLoading)
@@ -194,8 +234,28 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
NotificationManager nm=getActivity().getSystemService(NotificationManager.class); NotificationManager nm=getActivity().getSystemService(NotificationManager.class);
nm.cancel(accountID, PushNotificationReceiver.NOTIFICATION_ID); nm.cancel(accountID, PushNotificationReceiver.NOTIFICATION_ID);
} }
currentTab=tab; }
((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this);
private boolean onTabLongClick(@IdRes int tab){
if(tab==R.id.tab_profile){
ArrayList<String> options=new ArrayList<>();
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
options.add(session.self.displayName+"\n("+session.self.username+"@"+session.domain+")");
}
new M3AlertDialogBuilder(getActivity())
.setItems(options.toArray(new String[0]), (dialog, which)->{
AccountSession session=AccountSessionManager.getInstance().getLoggedInAccounts().get(which);
AccountSessionManager.getInstance().setLastActiveAccountID(session.getID());
getActivity().finish();
getActivity().startActivity(new Intent(getActivity(), MainActivity.class));
})
.setNegativeButton(R.string.add_account, (dialog, which)->{
Nav.go(getActivity(), SplashFragment.class, null);
})
.show();
return true;
}
return false;
} }
@Override @Override
@@ -206,4 +266,14 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
return searchFragment.onBackPressed(); return searchFragment.onBackPressed();
return false; return false;
} }
@Override
public void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
outState.putInt("selectedTab", currentTab);
getChildFragmentManager().putFragment(outState, "homeTimelineFragment", homeTimelineFragment);
getChildFragmentManager().putFragment(outState, "searchFragment", searchFragment);
getChildFragmentManager().putFragment(outState, "notificationsFragment", notificationsFragment);
getChildFragmentManager().putFragment(outState, "profileFragment", profileFragment);
}
} }

View File

@@ -83,42 +83,9 @@ public class HomeTimelineFragment extends StatusListFragment{
@Override @Override
public boolean onOptionsItemSelected(MenuItem item){ public boolean onOptionsItemSelected(MenuItem item){
ArrayList<String> options=new ArrayList<>(); Bundle args=new Bundle();
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){ args.putString("account", accountID);
options.add(session.self.displayName+"\n("+session.self.username+"@"+session.domain+")"); Nav.go(getActivity(), SettingsFragment.class, args);
}
new M3AlertDialogBuilder(getActivity())
.setItems(options.toArray(new String[0]), (dialog, which)->{
AccountSession session=AccountSessionManager.getInstance().getLoggedInAccounts().get(which);
AccountSessionManager.getInstance().setLastActiveAccountID(session.getID());
getActivity().finish();
getActivity().startActivity(new Intent(getActivity(), MainActivity.class));
})
.setPositiveButton(R.string.log_out, (dialog, which)->{
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
new RevokeOauthToken(session.app.clientId, session.app.clientSecret, session.token.accessToken)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Object result){
AccountSessionManager.getInstance().removeAccount(session.getID());
getActivity().finish();
getActivity().startActivity(new Intent(getActivity(), MainActivity.class));
}
@Override
public void onError(ErrorResponse error){
AccountSessionManager.getInstance().removeAccount(session.getID());
getActivity().finish();
getActivity().startActivity(new Intent(getActivity(), MainActivity.class));
}
})
.wrapProgress(getActivity(), R.string.loading, false)
.execNoAuth(session.domain);
})
.setNegativeButton(R.string.add_account, (dialog, which)->{
Nav.go(getActivity(), SplashFragment.class, null);
})
.show();
return true; return true;
} }

View File

@@ -8,12 +8,14 @@ import android.graphics.Paint;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.RoundRectShape; import android.graphics.drawable.shapes.RoundRectShape;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewOutlineProvider; import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver; import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.widget.EditText; import android.widget.EditText;
import android.widget.TextView; import android.widget.TextView;
@@ -34,6 +36,7 @@ import androidx.annotation.Nullable;
import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.fragments.WindowInsetsAwareFragment;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter; import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder; import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.ListImageLoaderWrapper; import me.grishka.appkit.imageloader.ListImageLoaderWrapper;
@@ -44,7 +47,7 @@ import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V; import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView; import me.grishka.appkit.views.UsableRecyclerView;
public class ProfileAboutFragment extends Fragment{ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareFragment{
private static final int MAX_FIELDS=4; private static final int MAX_FIELDS=4;
public UsableRecyclerView list; public UsableRecyclerView list;
@@ -111,6 +114,23 @@ public class ProfileAboutFragment extends Fragment{
return fields; return fields;
} }
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
list.setPadding(0, V.dp(16), 0, V.dp(12)+insets.getSystemWindowInsetBottom());
}
}
@Override
public boolean wantsLightStatusBar(){
return false;
}
@Override
public boolean wantsLightNavigationBar(){
return false;
}
private class AboutAdapter extends UsableRecyclerView.Adapter<BaseViewHolder> implements ImageLoaderRecyclerAdapter{ private class AboutAdapter extends UsableRecyclerView.Adapter<BaseViewHolder> implements ImageLoaderRecyclerAdapter{
public AboutAdapter(){ public AboutAdapter(){
super(imgLoader); super(imgLoader);

View File

@@ -33,6 +33,7 @@ import android.widget.RelativeLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toolbar; import android.widget.Toolbar;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountByID; import org.joinmastodon.android.api.requests.accounts.GetAccountByID;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships; import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
@@ -115,6 +116,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private String profileAccountID; private String profileAccountID;
private boolean refreshing; private boolean refreshing;
private View fab; private View fab;
private WindowInsets childInsets;
public ProfileFragment(){ public ProfileFragment(){
super(R.layout.loader_fragment_overlay_toolbar); super(R.layout.loader_fragment_overlay_toolbar);
@@ -365,16 +367,35 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override @Override
public void onApplyWindowInsets(WindowInsets insets){ public void onApplyWindowInsets(WindowInsets insets){
statusBarHeight=insets.getSystemWindowInsetTop(); statusBarHeight=insets.getSystemWindowInsetTop();
((ViewGroup.MarginLayoutParams)getToolbar().getLayoutParams()).topMargin=statusBarHeight; if(contentView!=null){
((ViewGroup.MarginLayoutParams) getToolbar().getLayoutParams()).topMargin=statusBarHeight;
refreshLayout.setProgressViewEndTarget(true, statusBarHeight+refreshLayout.getProgressCircleDiameter()+V.dp(24)); refreshLayout.setProgressViewEndTarget(true, statusBarHeight+refreshLayout.getProgressCircleDiameter()+V.dp(24));
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
int insetBottom=insets.getSystemWindowInsetBottom();
childInsets=insets.inset(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0);
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+insetBottom;
applyChildWindowInsets();
insets=insets.inset(0, 0, 0, insetBottom);
}else{
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24);
}
}
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom())); super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
} }
private void applyChildWindowInsets(){
if(postsFragment!=null && postsFragment.isAdded() && childInsets!=null){
postsFragment.onApplyWindowInsets(childInsets);
postsWithRepliesFragment.onApplyWindowInsets(childInsets);
mediaFragment.onApplyWindowInsets(childInsets);
}
}
private void bindHeaderView(){ private void bindHeaderView(){
setTitle(account.displayName); setTitle(account.displayName);
setSubtitle(getResources().getQuantityString(R.plurals.x_posts, account.statusesCount, account.statusesCount)); setSubtitle(getResources().getQuantityString(R.plurals.x_posts, account.statusesCount, account.statusesCount));
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(account.avatar, V.dp(100), V.dp(100))); ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(account.header, 1000, 1000)); ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName); SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName);
HtmlParser.parseCustomEmoji(ssb, account.emojis); HtmlParser.parseCustomEmoji(ssb, account.emojis);
name.setText(ssb); name.setText(ssb);
@@ -790,8 +811,19 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override @Override
public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position){ public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position){
Fragment fragment=getFragmentForPage(position); Fragment fragment=getFragmentForPage(position);
if(!fragment.isAdded()) if(!fragment.isAdded()){
getChildFragmentManager().beginTransaction().add(holder.itemView.getId(), getFragmentForPage(position)).commit(); getChildFragmentManager().beginTransaction().add(holder.itemView.getId(), fragment).commit();
holder.itemView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
if(fragment.isAdded()){
holder.itemView.getViewTreeObserver().removeOnPreDrawListener(this);
applyChildWindowInsets();
}
return true;
}
});
}
} }
@Override @Override

View File

@@ -0,0 +1,612 @@
package org.joinmastodon.android.fragments;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.PopupMenu;
import android.widget.Switch;
import android.widget.TextView;
import android.widget.Toast;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.PushNotification;
import org.joinmastodon.android.model.PushSubscription;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.ArrayList;
import java.util.function.Consumer;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.imageloader.ImageCache;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class SettingsFragment extends ToolbarFragment{
private UsableRecyclerView list;
private ArrayList<Item> items=new ArrayList<>();
private ThemeItem themeItem;
private NotificationPolicyItem notificationPolicyItem;
private String accountID;
private boolean needUpdateNotificationSettings;
private PushSubscription pushSubscription;
private ImageView themeTransitionWindowView;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
setRetainInstance(true);
setTitle(R.string.settings);
accountID=getArguments().getString("account");
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
items.add(new HeaderItem(R.string.settings_theme));
items.add(themeItem=new ThemeItem());
items.add(new SwitchItem(R.string.theme_true_black, R.drawable.ic_fluent_dark_theme_24_regular, GlobalUserPreferences.trueBlackTheme, this::onTrueBlackThemeChanged));
items.add(new HeaderItem(R.string.settings_behavior));
items.add(new SwitchItem(R.string.settings_gif, R.drawable.ic_fluent_gif_24_regular, GlobalUserPreferences.playGifs, i->{
GlobalUserPreferences.playGifs=i.checked;
GlobalUserPreferences.save();
}));
items.add(new SwitchItem(R.string.settings_custom_tabs, R.drawable.ic_fluent_link_24_regular, GlobalUserPreferences.useCustomTabs, i->{
GlobalUserPreferences.useCustomTabs=i.checked;
GlobalUserPreferences.save();
}));
items.add(new HeaderItem(R.string.settings_notifications));
items.add(notificationPolicyItem=new NotificationPolicyItem());
PushSubscription pushSubscription=getPushSubscription();
items.add(new SwitchItem(R.string.notify_favorites, R.drawable.ic_fluent_star_24_regular, pushSubscription.alerts.favourite, i->onNotificationsChanged(PushNotification.Type.FAVORITE, i.checked)));
items.add(new SwitchItem(R.string.notify_follow, R.drawable.ic_fluent_person_add_24_regular, pushSubscription.alerts.follow, i->onNotificationsChanged(PushNotification.Type.FOLLOW, i.checked)));
items.add(new SwitchItem(R.string.notify_reblog, R.drawable.ic_fluent_arrow_repeat_all_24_regular, pushSubscription.alerts.reblog, i->onNotificationsChanged(PushNotification.Type.REBLOG, i.checked)));
items.add(new SwitchItem(R.string.notify_mention, R.drawable.ic_at_symbol, pushSubscription.alerts.mention, i->onNotificationsChanged(PushNotification.Type.MENTION, i.checked)));
items.add(new HeaderItem(R.string.settings_boring));
items.add(new TextItem(R.string.settings_account, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/auth/edit")));
items.add(new TextItem(R.string.settings_contribute, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/mastodon/mastodon-android")));
items.add(new TextItem(R.string.settings_tos, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")));
items.add(new TextItem(R.string.settings_privacy_policy, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")));
items.add(new RedHeaderItem(R.string.settings_spicy));
items.add(new TextItem(R.string.settings_clear_cache, this::clearImageCache));
items.add(new TextItem(R.string.log_out, this::confirmLogOut));
items.add(new FooterItem(getString(R.string.settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)));
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
if(themeTransitionWindowView!=null){
// Activity has finished recreating. Remove the overlay.
MastodonApp.context.getSystemService(WindowManager.class).removeView(themeTransitionWindowView);
themeTransitionWindowView=null;
}
}
@Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
list=new UsableRecyclerView(getActivity());
list.setLayoutManager(new LinearLayoutManager(getActivity()));
list.setAdapter(new SettingsAdapter());
list.setBackgroundColor(UiUtils.getThemeColor(getActivity(), android.R.attr.colorBackground));
list.setPadding(0, V.dp(16), 0, V.dp(12));
list.setClipToPadding(false);
list.addItemDecoration(new RecyclerView.ItemDecoration(){
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
// Add 32dp gaps between sections
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
if((holder instanceof HeaderViewHolder || holder instanceof FooterViewHolder) && holder.getAbsoluteAdapterPosition()>0)
outRect.top=V.dp(32);
}
});
return list;
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
list.setPadding(0, V.dp(16), 0, V.dp(12)+insets.getSystemWindowInsetBottom());
insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom());
}
super.onApplyWindowInsets(insets);
}
@Override
public void onDestroy(){
super.onDestroy();
if(needUpdateNotificationSettings){
AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().updatePushSettings(pushSubscription);
}
}
private void onThemePreferenceClick(GlobalUserPreferences.ThemePreference theme){
GlobalUserPreferences.theme=theme;
GlobalUserPreferences.save();
restartActivityToApplyNewTheme();
}
private void onTrueBlackThemeChanged(SwitchItem item){
GlobalUserPreferences.trueBlackTheme=item.checked;
GlobalUserPreferences.save();
RecyclerView.ViewHolder themeHolder=list.findViewHolderForAdapterPosition(items.indexOf(themeItem));
if(themeHolder!=null){
((ThemeViewHolder)themeHolder).bindSubitems();
}else{
list.getAdapter().notifyItemChanged(items.indexOf(themeItem));
}
if(UiUtils.isDarkTheme()){
restartActivityToApplyNewTheme();
}
}
private void restartActivityToApplyNewTheme(){
// Calling activity.recreate() causes a black screen for like half a second.
// So, let's take a screenshot and overlay it on top to create the illusion of a smoother transition.
// As a bonus, we can fade it out to make it even smoother.
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
View activityDecorView=getActivity().getWindow().getDecorView();
Bitmap bitmap=Bitmap.createBitmap(activityDecorView.getWidth(), activityDecorView.getHeight(), Bitmap.Config.ARGB_8888);
activityDecorView.draw(new Canvas(bitmap));
themeTransitionWindowView=new ImageView(MastodonApp.context);
themeTransitionWindowView.setImageBitmap(bitmap);
WindowManager.LayoutParams lp=new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION);
lp.flags=WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE|WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR | WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
lp.systemUiVisibility=View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
lp.systemUiVisibility|=(activityDecorView.getWindowSystemUiVisibility() & (View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR));
lp.width=lp.height=WindowManager.LayoutParams.MATCH_PARENT;
lp.token=getActivity().getWindow().getAttributes().token;
lp.windowAnimations=R.style.window_fade_out;
MastodonApp.context.getSystemService(WindowManager.class).addView(themeTransitionWindowView, lp);
}
getActivity().recreate();
}
private PushSubscription getPushSubscription(){
if(pushSubscription!=null)
return pushSubscription;
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
if(session.pushSubscription==null){
pushSubscription=new PushSubscription();
pushSubscription.alerts=PushSubscription.Alerts.ofAll();
}else{
pushSubscription=session.pushSubscription.clone();
}
return pushSubscription;
}
private void onNotificationsChanged(PushNotification.Type type, boolean enabled){
PushSubscription subscription=getPushSubscription();
switch(type){
case FAVORITE -> subscription.alerts.favourite=enabled;
case FOLLOW -> subscription.alerts.follow=enabled;
case REBLOG -> subscription.alerts.reblog=enabled;
case MENTION -> subscription.alerts.mention=subscription.alerts.poll=enabled;
}
needUpdateNotificationSettings=true;
}
private void onNotificationsPolicyChanged(PushSubscription.Policy policy){
PushSubscription subscription=getPushSubscription();
PushSubscription.Policy prevPolicy=subscription.policy;
if(prevPolicy==policy)
return;
subscription.policy=policy;
int index=items.indexOf(notificationPolicyItem);
RecyclerView.ViewHolder policyHolder=list.findViewHolderForAdapterPosition(index);
if(policyHolder!=null){
((NotificationPolicyViewHolder)policyHolder).rebind();
}else{
list.getAdapter().notifyItemChanged(index);
}
if((prevPolicy==PushSubscription.Policy.NONE)!=(policy==PushSubscription.Policy.NONE)){
index++;
while(items.get(index) instanceof SwitchItem){
SwitchItem si=(SwitchItem) items.get(index);
si.enabled=si.checked=policy!=PushSubscription.Policy.NONE;
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(index);
if(holder!=null)
((BindableViewHolder<?>)holder).rebind();
else
list.getAdapter().notifyItemChanged(index);
index++;
}
}
needUpdateNotificationSettings=true;
}
private void confirmLogOut(){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.log_out)
.setMessage(R.string.confirm_log_out)
.setPositiveButton(R.string.log_out, (dialog, which) -> logOut())
.setNegativeButton(R.string.cancel, null)
.show();
}
private void logOut(){
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
new RevokeOauthToken(session.app.clientId, session.app.clientSecret, session.token.accessToken)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Object result){
onLoggedOut();
}
@Override
public void onError(ErrorResponse error){
onLoggedOut();
}
})
.wrapProgress(getActivity(), R.string.loading, false)
.exec(accountID);
}
private void onLoggedOut(){
AccountSessionManager.getInstance().removeAccount(accountID);
Intent intent=new Intent(getActivity(), MainActivity.class);
startActivity(intent);
getActivity().finish();
}
private void clearImageCache(){
MastodonAPIController.runInBackground(()->{
Activity activity=getActivity();
ImageCache.getInstance(getActivity()).clear();
Toast.makeText(activity, R.string.media_cache_cleared, Toast.LENGTH_SHORT).show();
});
}
private static abstract class Item{
public abstract int getViewType();
}
private class HeaderItem extends Item{
private String text;
public HeaderItem(@StringRes int text){
this.text=getString(text);
}
@Override
public int getViewType(){
return 0;
}
}
private class SwitchItem extends Item{
private String text;
private int icon;
private boolean checked;
private Consumer<SwitchItem> onChanged;
private boolean enabled=true;
public SwitchItem(@StringRes int text, @DrawableRes int icon, boolean checked, Consumer<SwitchItem> onChanged){
this.text=getString(text);
this.icon=icon;
this.checked=checked;
this.onChanged=onChanged;
}
public SwitchItem(@StringRes int text, int icon, boolean checked, Consumer<SwitchItem> onChanged, boolean enabled){
this.text=getString(text);
this.icon=icon;
this.checked=checked;
this.onChanged=onChanged;
this.enabled=enabled;
}
@Override
public int getViewType(){
return 1;
}
}
private static class ThemeItem extends Item{
@Override
public int getViewType(){
return 2;
}
}
private static class NotificationPolicyItem extends Item{
@Override
public int getViewType(){
return 3;
}
}
private class TextItem extends Item{
private String text;
private Runnable onClick;
public TextItem(@StringRes int text, Runnable onClick){
this.text=getString(text);
this.onClick=onClick;
}
@Override
public int getViewType(){
return 4;
}
}
private class RedHeaderItem extends HeaderItem{
public RedHeaderItem(int text){
super(text);
}
@Override
public int getViewType(){
return 5;
}
}
private class FooterItem extends Item{
private String text;
public FooterItem(String text){
this.text=text;
}
@Override
public int getViewType(){
return 6;
}
}
private class SettingsAdapter extends RecyclerView.Adapter<BindableViewHolder<Item>>{
@NonNull
@Override
public BindableViewHolder<Item> onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
//noinspection unchecked
return (BindableViewHolder<Item>) switch(viewType){
case 0 -> new HeaderViewHolder(false);
case 1 -> new SwitchViewHolder();
case 2 -> new ThemeViewHolder();
case 3 -> new NotificationPolicyViewHolder();
case 4 -> new TextViewHolder();
case 5 -> new HeaderViewHolder(true);
case 6 -> new FooterViewHolder();
default -> throw new IllegalStateException("Unexpected value: "+viewType);
};
}
@Override
public void onBindViewHolder(@NonNull BindableViewHolder<Item> holder, int position){
holder.bind(items.get(position));
}
@Override
public int getItemCount(){
return items.size();
}
@Override
public int getItemViewType(int position){
return items.get(position).getViewType();
}
}
private class HeaderViewHolder extends BindableViewHolder<HeaderItem>{
private final TextView text;
public HeaderViewHolder(boolean red){
super(getActivity(), R.layout.item_settings_header, list);
text=(TextView) itemView;
if(red)
text.setTextColor(getResources().getColor(R.color.error_700));
}
@Override
public void onBind(HeaderItem item){
text.setText(item.text);
}
}
private class SwitchViewHolder extends BindableViewHolder<SwitchItem> implements UsableRecyclerView.DisableableClickable{
private final TextView text;
private final ImageView icon;
private final Switch checkbox;
public SwitchViewHolder(){
super(getActivity(), R.layout.item_settings_switch, list);
text=findViewById(R.id.text);
icon=findViewById(R.id.icon);
checkbox=findViewById(R.id.checkbox);
}
@Override
public void onBind(SwitchItem item){
text.setText(item.text);
icon.setImageResource(item.icon);
checkbox.setChecked(item.checked && item.enabled);
checkbox.setEnabled(item.enabled);
}
@Override
public void onClick(){
item.checked=!item.checked;
checkbox.setChecked(item.checked);
item.onChanged.accept(item);
}
@Override
public boolean isEnabled(){
return item.enabled;
}
}
private class ThemeViewHolder extends BindableViewHolder<ThemeItem>{
private SubitemHolder autoHolder, lightHolder, darkHolder;
public ThemeViewHolder(){
super(getActivity(), R.layout.item_settings_theme, list);
autoHolder=new SubitemHolder(findViewById(R.id.theme_auto));
lightHolder=new SubitemHolder(findViewById(R.id.theme_light));
darkHolder=new SubitemHolder(findViewById(R.id.theme_dark));
}
@Override
public void onBind(ThemeItem item){
bindSubitems();
}
public void bindSubitems(){
autoHolder.bind(R.string.theme_auto, GlobalUserPreferences.trueBlackTheme ? R.drawable.theme_auto_trueblack : R.drawable.theme_auto, GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.AUTO);
lightHolder.bind(R.string.theme_light, R.drawable.theme_light, GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.LIGHT);
darkHolder.bind(R.string.theme_dark, GlobalUserPreferences.trueBlackTheme ? R.drawable.theme_dark_trueblack : R.drawable.theme_dark, GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.DARK);
}
private void onSubitemClick(View v){
GlobalUserPreferences.ThemePreference pref;
if(v.getId()==R.id.theme_auto)
pref=GlobalUserPreferences.ThemePreference.AUTO;
else if(v.getId()==R.id.theme_light)
pref=GlobalUserPreferences.ThemePreference.LIGHT;
else if(v.getId()==R.id.theme_dark)
pref=GlobalUserPreferences.ThemePreference.DARK;
else
return;
onThemePreferenceClick(pref);
}
private class SubitemHolder{
public TextView text;
public ImageView icon, checkbox;
public SubitemHolder(View view){
text=view.findViewById(R.id.text);
icon=view.findViewById(R.id.icon);
checkbox=view.findViewById(R.id.checkbox);
view.setOnClickListener(ThemeViewHolder.this::onSubitemClick);
icon.setClipToOutline(true);
icon.setOutlineProvider(OutlineProviders.roundedRect(4));
}
public void bind(int text, int icon, boolean checked){
this.text.setText(text);
this.icon.setImageResource(icon);
checkbox.setSelected(checked);
}
public void setChecked(boolean checked){
checkbox.setSelected(checked);
}
}
}
private class NotificationPolicyViewHolder extends BindableViewHolder<NotificationPolicyItem>{
private final Button button;
private final PopupMenu popupMenu;
@SuppressLint("ClickableViewAccessibility")
public NotificationPolicyViewHolder(){
super(getActivity(), R.layout.item_settings_notification_policy, list);
button=findViewById(R.id.button);
popupMenu=new PopupMenu(getActivity(), button, Gravity.CENTER_HORIZONTAL);
popupMenu.inflate(R.menu.notification_policy);
popupMenu.setOnMenuItemClickListener(item->{
PushSubscription.Policy policy;
int id=item.getItemId();
if(id==R.id.notify_anyone)
policy=PushSubscription.Policy.ALL;
else if(id==R.id.notify_followed)
policy=PushSubscription.Policy.FOLLOWED;
else if(id==R.id.notify_follower)
policy=PushSubscription.Policy.FOLLOWER;
else if(id==R.id.notify_none)
policy=PushSubscription.Policy.NONE;
else
return false;
onNotificationsPolicyChanged(policy);
return true;
});
UiUtils.enablePopupMenuIcons(getActivity(), popupMenu);
button.setOnTouchListener(popupMenu.getDragToOpenListener());
button.setOnClickListener(v->popupMenu.show());
}
@Override
public void onBind(NotificationPolicyItem item){
button.setText(switch(getPushSubscription().policy){
case ALL -> R.string.notify_anyone;
case FOLLOWED -> R.string.notify_followed;
case FOLLOWER -> R.string.notify_follower;
case NONE -> R.string.notify_none;
});
}
}
private class TextViewHolder extends BindableViewHolder<TextItem> implements UsableRecyclerView.Clickable{
private final TextView text;
public TextViewHolder(){
super(getActivity(), R.layout.item_settings_text, list);
text=(TextView) itemView;
}
@Override
public void onBind(TextItem item){
text.setText(item.text);
}
@Override
public void onClick(){
item.onClick.run();
}
}
private class FooterViewHolder extends BindableViewHolder<FooterItem>{
private final TextView text;
public FooterViewHolder(){
super(getActivity(), R.layout.item_settings_footer, list);
text=(TextView) itemView;
}
@Override
public void onBind(FooterItem item){
text.setText(item.text);
}
}
}

View File

@@ -65,7 +65,7 @@ public class AccountActivationFragment extends AppKitFragment{
@Override @Override
public boolean wantsLightStatusBar(){ public boolean wantsLightStatusBar(){
return (MastodonApp.context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)!=Configuration.UI_MODE_NIGHT_YES; return !UiUtils.isDarkTheme();
} }
@Override @Override

View File

@@ -281,4 +281,9 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
if(ev.reportAccountID.equals(reportAccount.id)) if(ev.reportAccountID.equals(reportAccount.id))
Nav.finish(this); Nav.finish(this);
} }
@Override
protected boolean wantsOverlaySystemNavigation(){
return false;
}
} }

View File

@@ -1,13 +1,20 @@
package org.joinmastodon.android.model; package org.joinmastodon.android.model;
import com.google.gson.annotations.SerializedName;
import org.joinmastodon.android.api.AllFieldsAreRequired; import org.joinmastodon.android.api.AllFieldsAreRequired;
import androidx.annotation.NonNull;
@AllFieldsAreRequired @AllFieldsAreRequired
public class PushSubscription extends BaseModel{ public class PushSubscription extends BaseModel implements Cloneable{
public int id; public int id;
public String endpoint; public String endpoint;
public Alerts alerts; public Alerts alerts;
public String serverKey; public String serverKey;
public Policy policy=Policy.ALL;
public PushSubscription(){}
@Override @Override
public String toString(){ public String toString(){
@@ -19,7 +26,18 @@ public class PushSubscription extends BaseModel{
'}'; '}';
} }
public static class Alerts{ @NonNull
@Override
public PushSubscription clone(){
PushSubscription copy=null;
try{
copy=(PushSubscription) super.clone();
}catch(CloneNotSupportedException ignore){}
copy.alerts=alerts.clone();
return copy;
}
public static class Alerts implements Cloneable{
public boolean follow; public boolean follow;
public boolean favourite; public boolean favourite;
public boolean reblog; public boolean reblog;
@@ -42,5 +60,26 @@ public class PushSubscription extends BaseModel{
", poll="+poll+ ", poll="+poll+
'}'; '}';
} }
@NonNull
@Override
public Alerts clone(){
try{
return (Alerts) super.clone();
}catch(CloneNotSupportedException e){
return null;
}
}
}
public enum Policy{
@SerializedName("all")
ALL,
@SerializedName("followed")
FOLLOWED,
@SerializedName("follower")
FOLLOWER,
@SerializedName("none")
NONE
} }
} }

View File

@@ -15,6 +15,7 @@ import android.widget.ImageView;
import android.widget.PopupMenu; import android.widget.PopupMenu;
import android.widget.TextView; import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.fragments.BaseStatusListFragment;
@@ -52,7 +53,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
super(parentID, parentFragment); super(parentID, parentFragment);
this.user=user; this.user=user;
this.createdAt=createdAt; this.createdAt=createdAt;
avaRequest=new UrlImageLoaderRequest(user.avatar, V.dp(50), V.dp(50)); avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? user.avatar : user.avatarStatic, V.dp(50), V.dp(50));
this.accountID=accountID; this.accountID=accountID;
parsedName=new SpannableStringBuilder(user.displayName); parsedName=new SpannableStringBuilder(user.displayName);
this.status=status; this.status=status;

View File

@@ -6,6 +6,7 @@ import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.text.style.ReplacementSpan; import android.text.style.ReplacementSpan;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.model.Emoji;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@@ -53,6 +54,6 @@ public class CustomEmojiSpan extends ReplacementSpan{
public UrlImageLoaderRequest createImageLoaderRequest(){ public UrlImageLoaderRequest createImageLoaderRequest(){
int size=V.dp(20); int size=V.dp(20);
return new UrlImageLoaderRequest(emoji.url, size, size); return new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? emoji.url : emoji.staticUrl, size, size);
} }
} }

View File

@@ -3,24 +3,34 @@ package org.joinmastodon.android.ui.utils;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.provider.OpenableColumns; import android.provider.OpenableColumns;
import android.text.SpannableStringBuilder;
import android.text.Spanned; import android.text.Spanned;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.webkit.MimeTypeMap; import android.webkit.MimeTypeMap;
import android.widget.Button; import android.widget.Button;
import android.widget.PopupMenu;
import android.widget.TextView; import android.widget.TextView;
import org.joinmastodon.android.E; import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked; import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked;
@@ -36,8 +46,10 @@ import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.M3AlertDialogBuilder; import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.text.CustomEmojiSpan; import org.joinmastodon.android.ui.text.CustomEmojiSpan;
import org.joinmastodon.android.ui.text.SpacerSpan;
import java.io.File; import java.io.File;
import java.lang.reflect.Method;
import java.time.Instant; import java.time.Instant;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
@@ -69,11 +81,14 @@ public class UiUtils{
private UiUtils(){} private UiUtils(){}
public static void launchWebBrowser(Context context, String url){ public static void launchWebBrowser(Context context, String url){
// TODO setting for custom tabs if(GlobalUserPreferences.useCustomTabs){
new CustomTabsIntent.Builder() new CustomTabsIntent.Builder()
.setShowTitle(true) .setShowTitle(true)
.build() .build()
.launchUrl(context, Uri.parse(url)); .launchUrl(context, Uri.parse(url));
}else{
context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
}
} }
public static String formatRelativeTimestamp(Context context, Instant instant){ public static String formatRelativeTimestamp(Context context, Instant instant){
@@ -374,4 +389,48 @@ public class UiUtils{
d.draw(new Canvas(bitmap)); d.draw(new Canvas(bitmap));
return bitmap; return bitmap;
} }
public static void enablePopupMenuIcons(Context context, PopupMenu menu){
Menu m=menu.getMenu();
if(Build.VERSION.SDK_INT>=29){
menu.setForceShowIcon(true);
}else{
try{
Method setOptionalIconsVisible=m.getClass().getDeclaredMethod("setOptionalIconsVisible", boolean.class);
setOptionalIconsVisible.setAccessible(true);
setOptionalIconsVisible.invoke(m, true);
}catch(Exception ignore){}
}
ColorStateList iconTint=ColorStateList.valueOf(UiUtils.getThemeColor(context, android.R.attr.textColorSecondary));
for(int i=0;i<m.size();i++){
MenuItem item=m.getItem(i);
Drawable icon=item.getIcon().mutate();
if(Build.VERSION.SDK_INT>=26){
item.setIconTintList(iconTint);
}else{
icon.setTintList(iconTint);
}
icon=new InsetDrawable(icon, V.dp(8), 0, 0, 0);
item.setIcon(icon);
SpannableStringBuilder ssb=new SpannableStringBuilder(item.getTitle());
ssb.insert(0, " ");
ssb.setSpan(new SpacerSpan(V.dp(24), 1), 0, 1, 0);
ssb.append(" ", new SpacerSpan(V.dp(8), 1), 0);
item.setTitle(ssb);
}
}
public static void setUserPreferredTheme(Context context){
context.setTheme(switch(GlobalUserPreferences.theme){
case AUTO -> GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_AutoLightDark_TrueBlack : R.style.Theme_Mastodon_AutoLightDark;
case LIGHT -> R.style.Theme_Mastodon_Light;
case DARK -> GlobalUserPreferences.trueBlackTheme ? R.style.Theme_Mastodon_Dark_TrueBlack : R.style.Theme_Mastodon_Dark;
});
}
public static boolean isDarkTheme(){
if(GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.AUTO)
return (MastodonApp.context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK)==Configuration.UI_MODE_NIGHT_YES;
return GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.DARK;
}
} }

View File

@@ -9,6 +9,7 @@ import android.widget.LinearLayout;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import java.util.function.IntConsumer; import java.util.function.IntConsumer;
import java.util.function.IntPredicate;
import androidx.annotation.IdRes; import androidx.annotation.IdRes;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@@ -17,6 +18,7 @@ public class TabBar extends LinearLayout{
@IdRes @IdRes
private int selectedTabID; private int selectedTabID;
private IntConsumer listener; private IntConsumer listener;
private IntPredicate longClickListener;
public TabBar(Context context){ public TabBar(Context context){
this(context, null); this(context, null);
@@ -39,6 +41,7 @@ public class TabBar extends LinearLayout{
child.setSelected(true); child.setSelected(true);
} }
child.setOnClickListener(this::onChildClick); child.setOnClickListener(this::onChildClick);
child.setOnLongClickListener(this::onChildLongClick);
} }
} }
@@ -51,8 +54,13 @@ public class TabBar extends LinearLayout{
selectedTabID=v.getId(); selectedTabID=v.getId();
} }
public void setListener(IntConsumer listener){ private boolean onChildLongClick(View v){
return longClickListener.test(v.getId());
}
public void setListeners(IntConsumer listener, IntPredicate longClickListener){
this.listener=listener; this.listener=listener;
this.longClickListener=longClickListener;
} }
public void selectTab(int id){ public void selectTab(int id){

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="200"
android:interpolator="@android:anim/decelerate_interpolator"
android:fromAlpha="1.0"
android:toAlpha="0.0"/>

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="?android:colorControlHighlight">
<item>
<shape>
<solid android:color="?colorPollVoted"/>
<corners android:radius="4dp"/>
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10zm0-1.5v-17c4.694 0 8.5 3.806 8.5 8.5s-3.806 8.5-8.5 8.5z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M18.75 3.501c1.795 0 3.25 1.455 3.25 3.25v10.503c0 1.794-1.455 3.25-3.25 3.25H5.25c-1.795 0-3.25-1.456-3.25-3.25V6.75C2 4.955 3.455 3.5 5.25 3.5h13.5zm0 1.5H5.25c-0.966 0-1.75 0.784-1.75 1.75v10.503c0 0.966 0.784 1.75 1.75 1.75h13.5c0.966 0 1.75-0.784 1.75-1.75V6.75C20.5 5.784 19.716 5 18.75 5zM8.015 8.872c0.596 0 1.018 0.082 1.502 0.314 0.31 0.15 0.442 0.523 0.293 0.834-0.15 0.31-0.523 0.442-0.834 0.293-0.3-0.144-0.54-0.19-0.961-0.19-0.867 0-1.504 0.796-1.504 1.872 0 1.077 0.638 1.876 1.504 1.876 0.428 0 0.791-0.18 0.98-0.501L9 13.355v-0.734H8.625c-0.314 0-0.573-0.23-0.618-0.532L8 11.997c0-0.314 0.232-0.574 0.533-0.619l0.092-0.006h1.002c0.314 0 0.573 0.23 0.618 0.532l0.007 0.093-0.002 1.547-0.006 0.056-0.021 0.09-0.02 0.055c-0.377 0.89-1.241 1.376-2.188 1.376-1.626 0-2.754-1.413-2.754-3.126 0-1.713 1.127-3.123 2.754-3.123zm4.614 0.122c0.314 0 0.574 0.232 0.618 0.533l0.007 0.092v4.763c0 0.345-0.28 0.625-0.625 0.625-0.314 0-0.574-0.232-0.618-0.533l-0.007-0.092V9.619c0-0.345 0.28-0.625 0.625-0.625zm2.996 0l1.997 0.007c0.345 0.002 0.624 0.282 0.623 0.627-0.001 0.314-0.233 0.573-0.535 0.617l-0.092 0.006-1.371-0.005V12h1.123c0.314 0 0.574 0.232 0.618 0.534l0.007 0.092c0 0.314-0.231 0.573-0.533 0.618l-0.092 0.007-1.123-0.001v1.115c0 0.314-0.23 0.574-0.532 0.619l-0.092 0.006c-0.314 0-0.574-0.23-0.619-0.532l-0.006-0.093V9.617c0-0.314 0.233-0.573 0.534-0.616l0.093-0.007z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M9.25 7C9.664 7 10 7.336 10 7.75c0 0.377-0.277 0.688-0.64 0.742L9.25 8.5H7c-1.933 0-3.5 1.567-3.5 3.5 0 1.864 1.457 3.388 3.294 3.494L7 15.5h2.25c0.414 0 0.75 0.336 0.75 0.75 0 0.377-0.277 0.688-0.64 0.742L9.25 17H7c-2.761 0-5-2.239-5-5 0-2.678 2.105-4.864 4.75-4.994L7 7h2.25zM17 7c2.761 0 5 2.239 5 5 0 2.678-2.105 4.864-4.75 4.994L17 17h-2.25C14.336 17 14 16.664 14 16.25c0-0.377 0.277-0.688 0.64-0.742l0.11-0.008H17c1.933 0 3.5-1.567 3.5-3.5 0-1.864-1.457-3.388-3.294-3.494L17 8.5h-2.25C14.336 8.5 14 8.164 14 7.75c0-0.377 0.277-0.688 0.64-0.742L14.75 7H17zM7 11.25h10c0.414 0 0.75 0.336 0.75 0.75 0 0.38-0.282 0.694-0.648 0.743L17 12.75H7c-0.414 0-0.75-0.336-0.75-0.75 0-0.38 0.282-0.694 0.648-0.743L7 11.25h10H7z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="25dp" android:height="24dp" android:viewportWidth="25" android:viewportHeight="24">
<path android:pathData="M17.916 12c3.038 0 5.5 2.463 5.5 5.5 0 3.038-2.462 5.5-5.5 5.5-3.037 0-5.5-2.462-5.5-5.5 0-3.037 2.463-5.5 5.5-5.5zm-5.477 2c-0.297 0.463-0.537 0.966-0.709 1.5H4.669c-0.414 0-0.75 0.335-0.75 0.75v0.577c0 0.535 0.192 1.053 0.54 1.46 1.253 1.469 3.22 2.214 5.957 2.214 0.597 0 1.157-0.035 1.68-0.106 0.246 0.495 0.553 0.954 0.912 1.367-0.795 0.16-1.66 0.24-2.592 0.24-3.146 0-5.532-0.906-7.098-2.74-0.58-0.679-0.898-1.543-0.898-2.435v-0.578C2.42 15.007 3.427 14 4.669 14h7.77zm5.477 0l-0.09 0.008c-0.204 0.037-0.364 0.198-0.402 0.402l-0.008 0.09V17H14.92l-0.09 0.008c-0.204 0.037-0.365 0.198-0.402 0.402L14.42 17.5l0.008 0.09c0.037 0.204 0.198 0.365 0.402 0.402L14.92 18h2.495l0.001 2.5 0.008 0.09c0.038 0.204 0.198 0.365 0.402 0.402L17.916 21l0.09-0.008c0.204-0.037 0.365-0.198 0.402-0.402l0.008-0.09V18h2.504l0.09-0.008c0.204-0.037 0.365-0.198 0.402-0.402l0.008-0.09-0.008-0.09c-0.037-0.204-0.198-0.365-0.402-0.402L20.92 17h-2.505l0.001-2.5-0.008-0.09c-0.037-0.204-0.198-0.365-0.402-0.402L17.916 14zm-7.5-11.995c2.762 0 5 2.239 5 5s-2.238 5-5 5c-2.761 0-5-2.239-5-5s2.239-5 5-5zm0 1.5c-1.933 0-3.5 1.567-3.5 3.5s1.567 3.5 3.5 3.5 3.5-1.567 3.5-3.5-1.567-3.5-3.5-3.5z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="25dp" android:height="24dp" android:viewportWidth="25" android:viewportHeight="24">
<path android:pathData="M12.416 2c5.523 0 10 4.477 10 10s-4.477 10-10 10-10-4.477-10-10 4.477-10 10-10zm6.517 4.543L6.96 18.517c1.477 1.238 3.38 1.983 5.457 1.983 4.694 0 8.5-3.806 8.5-8.5 0-2.077-0.745-3.98-1.983-5.457zM12.416 3.5c-4.694 0-8.5 3.806-8.5 8.5 0 2.077 0.745 3.98 1.983 5.457L17.873 5.483C16.396 4.245 14.493 3.5 12.416 3.5z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -5,6 +5,6 @@
android:viewportHeight="24"> android:viewportHeight="24">
<path <path
android:pathData="M16.7096,17.7682C19.4819,17.4391 21.8955,15.7408 22.199,14.1888C22.6769,11.7442 22.6376,8.2231 22.6376,8.2231C22.6376,3.4504 19.4929,2.0516 19.4929,2.0516C17.9073,1.3274 15.1846,1.023 12.356,1H12.2865C9.4579,1.023 6.7369,1.3274 5.1513,2.0516C5.1513,2.0516 2.0066,3.4504 2.0066,8.2231C2.0066,8.5125 2.0051,8.8169 2.0035,9.1339C1.9991,10.0135 1.9943,10.9896 2.0199,12.0083C2.1341,16.6755 2.8805,21.2752 7.2202,22.4175C9.2213,22.944 10.9392,23.0542 12.323,22.9785C14.832,22.8403 16.2406,22.0883 16.2406,22.0883L16.1577,20.2779C16.1577,20.2779 14.3648,20.8402 12.3511,20.7717C10.356,20.7037 8.2496,20.5577 7.9269,18.1221C7.8972,17.9082 7.8823,17.6794 7.8823,17.4391C7.8823,17.4391 9.8408,17.9152 12.323,18.0283C13.8407,18.0974 15.2639,17.9399 16.7096,17.7682ZM18.8747,14.3719V8.5932C18.8747,7.4121 18.5723,6.4736 17.9648,5.7792C17.3382,5.0849 16.518,4.729 15.4997,4.729C14.3212,4.729 13.4291,5.1792 12.8392,6.0799L12.2657,7.0359L11.692,6.0799C11.1023,5.1792 10.21,4.729 9.0316,4.729C8.0134,4.729 7.193,5.0849 6.5664,5.7792C5.9589,6.4736 5.6565,7.4121 5.6565,8.5932V14.3719H7.959V8.763C7.959,7.5805 8.4594,6.9806 9.4602,6.9806C10.5665,6.9806 11.1211,7.6925 11.1211,9.1001V12.1701H13.4101V9.1001C13.4101,7.6925 13.9647,6.9806 15.071,6.9806C16.0718,6.9806 16.5722,7.5805 16.5722,8.763V14.3719H18.8747Z" android:pathData="M16.7096,17.7682C19.4819,17.4391 21.8955,15.7408 22.199,14.1888C22.6769,11.7442 22.6376,8.2231 22.6376,8.2231C22.6376,3.4504 19.4929,2.0516 19.4929,2.0516C17.9073,1.3274 15.1846,1.023 12.356,1H12.2865C9.4579,1.023 6.7369,1.3274 5.1513,2.0516C5.1513,2.0516 2.0066,3.4504 2.0066,8.2231C2.0066,8.5125 2.0051,8.8169 2.0035,9.1339C1.9991,10.0135 1.9943,10.9896 2.0199,12.0083C2.1341,16.6755 2.8805,21.2752 7.2202,22.4175C9.2213,22.944 10.9392,23.0542 12.323,22.9785C14.832,22.8403 16.2406,22.0883 16.2406,22.0883L16.1577,20.2779C16.1577,20.2779 14.3648,20.8402 12.3511,20.7717C10.356,20.7037 8.2496,20.5577 7.9269,18.1221C7.8972,17.9082 7.8823,17.6794 7.8823,17.4391C7.8823,17.4391 9.8408,17.9152 12.323,18.0283C13.8407,18.0974 15.2639,17.9399 16.7096,17.7682ZM18.8747,14.3719V8.5932C18.8747,7.4121 18.5723,6.4736 17.9648,5.7792C17.3382,5.0849 16.518,4.729 15.4997,4.729C14.3212,4.729 13.4291,5.1792 12.8392,6.0799L12.2657,7.0359L11.692,6.0799C11.1023,5.1792 10.21,4.729 9.0316,4.729C8.0134,4.729 7.193,5.0849 6.5664,5.7792C5.9589,6.4736 5.6565,7.4121 5.6565,8.5932V14.3719H7.959V8.763C7.959,7.5805 8.4594,6.9806 9.4602,6.9806C10.5665,6.9806 11.1211,7.6925 11.1211,9.1001V12.1701H13.4101V9.1001C13.4101,7.6925 13.9647,6.9806 15.071,6.9806C16.0718,6.9806 16.5722,7.5805 16.5722,8.763V14.3719H18.8747Z"
android:fillColor="#000000" android:fillColor="#fff"
android:fillType="evenOdd"/> android:fillType="evenOdd"/>
</vector> </vector>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="24dp"
android:textSize="16dp"
android:textColor="?android:textColorSecondary"
android:gravity="center"
tools:text="Visual appearance"/>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14dp"
android:fontFamily="sans-serif-medium"
android:textColor="?android:colorAccent"
android:padding="16dp"
tools:text="Visual appearance"/>

View File

@@ -0,0 +1,37 @@
<?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="32dp"
android:layoutDirection="locale"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:textColor="?android:textColorPrimary"
android:textSize="20dp"
android:fontFamily="sans-serif-medium"
android:singleLine="true"
android:ellipsize="end"
android:text="@string/notify_me_when"/>
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:background="@drawable/bg_inline_button"
android:textColor="?android:textColorPrimary"
android:textSize="20dp"
android:fontFamily="sans-serif-medium"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:stateListAnimator="@null"
android:elevation="0dp"
tools:text="@string/notify_followed"/>
</LinearLayout>

View File

@@ -0,0 +1,39 @@
<?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="48dp"
android:gravity="center_vertical"
android:layoutDirection="locale">
<ImageView
android:id="@+id/icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="32dp"
android:importantForAccessibility="no"
android:tint="?android:textColorPrimary"
tools:src="@drawable/ic_fluent_star_24_regular"/>
<TextView
android:id="@+id/text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16dp"
android:textColor="?android:textColorPrimary"
android:singleLine="true"
android:ellipsize="end"
tools:text="@string/theme_true_black"/>
<Switch
android:id="@+id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="12dp"
android:clickable="false"/>
</LinearLayout>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="48dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:gravity="center_vertical"
android:textSize="16dp"
android:textColor="?android:textColorPrimary"
android:singleLine="true"
android:ellipsize="end"
tools:text="daffdsa"/>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingBottom="8dp"
android:clipChildren="false"
android:clipToPadding="false">
<include layout="@layout/item_settings_theme_subitem" android:id="@+id/theme_auto"/>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1"/>
<include layout="@layout/item_settings_theme_subitem" android:id="@+id/theme_light"/>
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1"/>
<include layout="@layout/item_settings_theme_subitem" android:id="@+id/theme_dark"/>
</LinearLayout>

View File

@@ -0,0 +1,36 @@
<?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="vertical"
android:layout_width="100dp"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/icon"
android:layout_width="100dp"
android:layout_height="121dp"
android:elevation="1dp"
android:foreground="?android:selectableItemBackground"
android:duplicateParentState="true"
tools:src="@drawable/theme_auto"/>
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="20dp"
android:layout_marginTop="8dp"
android:textAppearance="@style/m3_body_medium"
android:gravity="center"
android:singleLine="true"
android:ellipsize="end"
tools:text="@string/theme_auto"/>
<ImageView
android:id="@+id/checkbox"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="8dp"
android:layout_gravity="center_horizontal"
android:src="@drawable/ic_round_checkbox"/>
</LinearLayout>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/notify_anyone" android:title="@string/notify_anyone" android:icon="@drawable/ic_fluent_earth_24_filled"/>
<item android:id="@+id/notify_follower" android:title="@string/notify_follower" android:icon="@drawable/ic_fluent_people_checkmark_24_regular"/>
<item android:id="@+id/notify_followed" android:title="@string/notify_followed" android:icon="@drawable/ic_fluent_people_checkmark_24_regular"/>
<item android:id="@+id/notify_none" android:title="@string/notify_none" android:icon="@drawable/ic_fluent_prohibited_24_regular"/>
</menu>

View File

@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.Mastodon.AutoLightDark" parent="Theme.Mastodon.Dark"/> <style name="Theme.Mastodon.AutoLightDark" parent="Theme.Mastodon.Dark"/>
<style name="Theme.Mastodon.AutoLightDark.TrueBlack" parent="Theme.Mastodon.Dark.TrueBlack"/>
</resources> </resources>

View File

@@ -130,7 +130,7 @@
<string name="notification_channel_audio_player">Audio playback</string> <string name="notification_channel_audio_player">Audio playback</string>
<string name="play">Play</string> <string name="play">Play</string>
<string name="pause">Pause</string> <string name="pause">Pause</string>
<string name="log_out">Log out</string> <string name="log_out">Sign out</string>
<string name="add_account">Add account</string> <string name="add_account">Add account</string>
<string name="search_hint">Search</string> <string name="search_hint">Search</string>
<string name="hashtags">Hashtags</string> <string name="hashtags">Hashtags</string>
@@ -236,4 +236,32 @@
</plurals> </plurals>
<string name="media_attachment_unsupported_type">File %s is of an unsupported type</string> <string name="media_attachment_unsupported_type">File %s is of an unsupported type</string>
<string name="media_attachment_too_big">File %1$s exceeds the size limit of %2$s MB</string> <string name="media_attachment_too_big">File %1$s exceeds the size limit of %2$s MB</string>
<string name="settings_theme">Visual appearance</string>
<string name="theme_auto">Automatic</string>
<string name="theme_light">Light</string>
<string name="theme_dark">Dark</string>
<string name="theme_true_black">True black mode</string>
<string name="settings_behavior">Behavior</string>
<string name="settings_gif">Play animated avatars and emoji</string>
<string name="settings_custom_tabs">Use in-app browser</string>
<string name="settings_notifications">Notifications</string>
<string name="notify_me_when">Notify me when</string>
<string name="notify_anyone">anyone</string>
<string name="notify_follower">a follower</string>
<string name="notify_followed">someone I follow</string>
<string name="notify_none">no one</string>
<string name="notify_favorites">Favorites my post</string>
<string name="notify_follow">Follows me</string>
<string name="notify_reblog">Reblogs my post</string>
<string name="notify_mention">Mentions me</string>
<string name="settings_boring">The boring zone</string>
<string name="settings_account">Account settings</string>
<string name="settings_contribute">Contribute to Mastodon</string>
<string name="settings_tos">Terms of service</string>
<string name="settings_privacy_policy">Privacy policy</string>
<string name="settings_spicy">The spicy zone</string>
<string name="settings_clear_cache">Clear media cache</string>
<string name="settings_app_version">Mastodon for Android v%1$s (%2$d)</string>
<string name="media_cache_cleared">Media cache cleared</string>
<string name="confirm_log_out">Are you sure you want to sign out?</string>
</resources> </resources>

View File

@@ -92,7 +92,12 @@
<item name="android:actionOverflowMenuStyle">@style/Widget.Mastodon.PopupMenu</item> <item name="android:actionOverflowMenuStyle">@style/Widget.Mastodon.PopupMenu</item>
</style> </style>
<style name="Theme.Mastodon.Dark.TrueBlack">
<item name="colorWindowBackground">#000</item>
</style>
<style name="Theme.Mastodon.AutoLightDark" parent="Theme.Mastodon.Light"/> <style name="Theme.Mastodon.AutoLightDark" parent="Theme.Mastodon.Light"/>
<style name="Theme.Mastodon.AutoLightDark.TrueBlack" parent="Theme.Mastodon.Light"/>
<style name="Theme.Mastodon.Toolbar" parent="android:ThemeOverlay.Material.ActionBar"> <style name="Theme.Mastodon.Toolbar" parent="android:ThemeOverlay.Material.ActionBar">
<item name="android:colorPrimary">@color/gray_50</item> <item name="android:colorPrimary">@color/gray_50</item>
@@ -282,4 +287,8 @@
<item name="android:textColor">?android:textColorPrimary</item> <item name="android:textColor">?android:textColorPrimary</item>
<item name="android:lineSpacingExtra">3dp</item> <item name="android:lineSpacingExtra">3dp</item>
</style> </style>
<style name="window_fade_out">
<item name="android:windowExitAnimation">@anim/fade_out_fast</item>
</style>
</resources> </resources>