Settings M3 redesign wip
This commit is contained in:
parent
7c6ec2e3d7
commit
31c8665653
|
@ -5,7 +5,7 @@ buildscript {
|
|||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:7.1.3"
|
||||
classpath "com.android.tools.build:gradle:7.4.2"
|
||||
classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1"
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#Thu Jan 13 11:33:43 MSK 2022
|
||||
#Sat Jun 03 23:40:27 MSK 2023
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
|
@ -9,7 +9,7 @@ android {
|
|||
applicationId "org.joinmastodon.android"
|
||||
minSdk 23
|
||||
targetSdk 33
|
||||
versionCode 55
|
||||
versionCode 56
|
||||
versionName "1.3.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "da-rDK", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fa-rIR", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "ig-rNG", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "kab", "ko-rKR", "my-rMM", "nl-rNL", "no-rNO", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "ur-rIN", "vi-rVN", "zh-rCN", "zh-rTW"
|
||||
|
@ -37,6 +37,9 @@ android {
|
|||
githubRelease{
|
||||
initWith release
|
||||
}
|
||||
githubDebug{
|
||||
initWith debug
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
|
@ -53,6 +56,9 @@ android {
|
|||
githubRelease{
|
||||
setRoot "src/github"
|
||||
}
|
||||
githubDebug{
|
||||
setRoot "src/github"
|
||||
}
|
||||
}
|
||||
lintOptions{
|
||||
checkReleaseBuilds false
|
||||
|
@ -77,7 +83,7 @@ dependencies {
|
|||
implementation 'de.psdev:async-otto:1.0.3'
|
||||
implementation 'org.parceler:parceler-api:1.1.12'
|
||||
annotationProcessor 'org.parceler:parceler:1.1.12'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
|
||||
|
||||
def appCenterSdkVersion = "4.4.2"
|
||||
appcenterPrivateBetaImplementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
|
||||
|
|
|
@ -95,7 +95,7 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
|
|||
if(state!=UpdateState.NO_UPDATE && state!=UpdateState.UPDATE_AVAILABLE)
|
||||
return;
|
||||
long timeSinceLastCheck=System.currentTimeMillis()-getPrefs().getLong("lastCheck", 0);
|
||||
if(timeSinceLastCheck>CHECK_PERIOD){
|
||||
if(timeSinceLastCheck>CHECK_PERIOD || forceUpdate){
|
||||
setState(UpdateState.CHECKING);
|
||||
MastodonAPIController.runInBackground(this::actuallyCheckForUpdates);
|
||||
}
|
||||
|
@ -109,23 +109,26 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
|
|||
try(Response resp=call.execute()){
|
||||
JsonObject obj=JsonParser.parseReader(resp.body().charStream()).getAsJsonObject();
|
||||
String tag=obj.get("tag_name").getAsString();
|
||||
Pattern pattern=Pattern.compile("v?(\\d+)\\.(\\d+)\\.(\\d+)");
|
||||
Pattern pattern=Pattern.compile("v?(\\d+)\\.(\\d+)(?:\\.(\\d+))?");
|
||||
Matcher matcher=pattern.matcher(tag);
|
||||
if(!matcher.find()){
|
||||
Log.w(TAG, "actuallyCheckForUpdates: release tag has wrong format: "+tag);
|
||||
return;
|
||||
}
|
||||
int newMajor=Integer.parseInt(matcher.group(1)), newMinor=Integer.parseInt(matcher.group(2)), newRevision=Integer.parseInt(matcher.group(3));
|
||||
matcher=pattern.matcher(BuildConfig.VERSION_NAME);
|
||||
if(!matcher.find()){
|
||||
int newMajor=Integer.parseInt(matcher.group(1)), newMinor=Integer.parseInt(matcher.group(2)), newRevision=matcher.group(3)!=null ? Integer.parseInt(matcher.group(3)) : 0;
|
||||
Matcher curMatcher=pattern.matcher(BuildConfig.VERSION_NAME);
|
||||
if(!curMatcher.find()){
|
||||
Log.w(TAG, "actuallyCheckForUpdates: current version has wrong format: "+BuildConfig.VERSION_NAME);
|
||||
return;
|
||||
}
|
||||
int curMajor=Integer.parseInt(matcher.group(1)), curMinor=Integer.parseInt(matcher.group(2)), curRevision=Integer.parseInt(matcher.group(3));
|
||||
int curMajor=Integer.parseInt(curMatcher.group(1)), curMinor=Integer.parseInt(curMatcher.group(2)), curRevision=matcher.group(3)!=null ? Integer.parseInt(curMatcher.group(3)) : 0;
|
||||
long newVersion=((long)newMajor << 32) | ((long)newMinor << 16) | newRevision;
|
||||
long curVersion=((long)curMajor << 32) | ((long)curMinor << 16) | curRevision;
|
||||
if(newVersion>curVersion || BuildConfig.DEBUG){
|
||||
String version=newMajor+"."+newMinor+"."+newRevision;
|
||||
if(newVersion>curVersion || forceUpdate){
|
||||
forceUpdate=false;
|
||||
String version=newMajor+"."+newMinor;
|
||||
if(matcher.group(3)!=null)
|
||||
version+="."+newRevision;
|
||||
Log.d(TAG, "actuallyCheckForUpdates: new version: "+version);
|
||||
for(JsonElement el:obj.getAsJsonArray("assets")){
|
||||
JsonObject asset=el.getAsJsonObject();
|
||||
|
@ -295,6 +298,15 @@ public class GithubSelfUpdaterImpl extends GithubSelfUpdater{
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset(){
|
||||
getPrefs().edit().clear().apply();
|
||||
File apk=getUpdateApkFile();
|
||||
if(apk.exists())
|
||||
apk.delete();
|
||||
state=UpdateState.NO_UPDATE;
|
||||
}
|
||||
|
||||
/*public static class InstallerStatusReceiver extends BroadcastReceiver{
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
*{
|
||||
box-sizing: border-box;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
body{
|
||||
background: {{colorSurface}};
|
||||
padding: 16px 16px 0 16px;
|
||||
margin: 0;
|
||||
color: {{colorOnSurface}};
|
||||
font-family: Roboto, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
-webkit-tap-highlight-color: {{colorPrimaryTransparent}};
|
||||
}
|
||||
a{
|
||||
text-decoration: none;
|
||||
color: {{colorPrimary}};
|
||||
}
|
||||
p, h1, h2, h3, h4, h5, h6, ul, ol{
|
||||
margin-bottom: 8px;
|
||||
margin-top: 0;
|
||||
}
|
||||
h1, h2{
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
h3, h4, h5, h6{
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: 500;
|
||||
}
|
||||
b, strong{
|
||||
font-weight: 500;
|
||||
}
|
||||
ul, ol{
|
||||
padding-inline-start: 16px;
|
||||
}
|
||||
ul>li, ol>li{
|
||||
padding-inline-start: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{{content}}
|
||||
</body>
|
||||
</html>
|
|
@ -6,7 +6,7 @@ import android.content.SharedPreferences;
|
|||
public class GlobalUserPreferences{
|
||||
public static boolean playGifs;
|
||||
public static boolean useCustomTabs;
|
||||
public static boolean trueBlackTheme;
|
||||
public static boolean altTextReminders, confirmUnfollow, confirmBoost, confirmDeletePost;
|
||||
public static ThemePreference theme;
|
||||
|
||||
private static SharedPreferences getPrefs(){
|
||||
|
@ -17,7 +17,10 @@ public class GlobalUserPreferences{
|
|||
SharedPreferences prefs=getPrefs();
|
||||
playGifs=prefs.getBoolean("playGifs", true);
|
||||
useCustomTabs=prefs.getBoolean("useCustomTabs", true);
|
||||
trueBlackTheme=prefs.getBoolean("trueBlackTheme", false);
|
||||
altTextReminders=prefs.getBoolean("altTextReminders", false);
|
||||
confirmUnfollow=prefs.getBoolean("confirmUnfollow", false);
|
||||
confirmBoost=prefs.getBoolean("confirmBoost", false);
|
||||
confirmDeletePost=prefs.getBoolean("confirmDeletePost", true);
|
||||
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
|
||||
}
|
||||
|
||||
|
@ -25,8 +28,11 @@ public class GlobalUserPreferences{
|
|||
getPrefs().edit()
|
||||
.putBoolean("playGifs", playGifs)
|
||||
.putBoolean("useCustomTabs", useCustomTabs)
|
||||
.putBoolean("trueBlackTheme", trueBlackTheme)
|
||||
.putInt("theme", theme.ordinal())
|
||||
.putBoolean("altTextReminders", altTextReminders)
|
||||
.putBoolean("confirmUnfollow", confirmUnfollow)
|
||||
.putBoolean("confirmBoost", confirmBoost)
|
||||
.putBoolean("confirmDeletePost", confirmDeletePost)
|
||||
.apply();
|
||||
}
|
||||
|
||||
|
|
|
@ -3,11 +3,10 @@ package org.joinmastodon.android;
|
|||
import android.annotation.SuppressLint;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
|
||||
import me.grishka.appkit.imageloader.ImageCache;
|
||||
import me.grishka.appkit.utils.NetworkUtils;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
@ -30,5 +29,8 @@ public class MastodonApp extends Application{
|
|||
|
||||
PushSubscriptionManager.tryRegisterFCM();
|
||||
GlobalUserPreferences.load();
|
||||
if(BuildConfig.DEBUG){
|
||||
WebView.setWebContentsDebuggingEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,6 +69,10 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
|||
Log.w(TAG, "onReceive: account for id '"+pushAccountID+"' not found");
|
||||
return;
|
||||
}
|
||||
if(account.getLocalPreferences().getNotificationsPauseEndTime()>System.currentTimeMillis()){
|
||||
Log.i(TAG, "onReceive: dropping notification because user has paused notifications for this account");
|
||||
return;
|
||||
}
|
||||
String accountID=account.getID();
|
||||
PushNotification pn=AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().decryptNotification(k, p, s);
|
||||
new GetNotificationByID(pn.notificationId+"")
|
||||
|
|
|
@ -15,7 +15,8 @@ import org.joinmastodon.android.api.requests.notifications.GetNotifications;
|
|||
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.SearchResult;
|
||||
|
@ -59,7 +60,7 @@ public class CacheController{
|
|||
cancelDelayedClose();
|
||||
databaseThread.postRunnable(()->{
|
||||
try{
|
||||
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
|
||||
List<LegacyFilter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(FilterContext.HOME)).collect(Collectors.toList());
|
||||
if(!forceReload){
|
||||
SQLiteDatabase db=getOrOpenDatabase();
|
||||
try(Cursor cursor=db.query("home_timeline", new String[]{"json", "flags"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
|
||||
|
@ -74,7 +75,7 @@ public class CacheController{
|
|||
int flags=cursor.getInt(1);
|
||||
status.hasGapAfter=((flags & POST_FLAG_GAP_AFTER)!=0);
|
||||
newMaxID=status.id;
|
||||
for(Filter filter:filters){
|
||||
for(LegacyFilter filter:filters){
|
||||
if(filter.matches(status))
|
||||
continue outer;
|
||||
}
|
||||
|
@ -139,7 +140,7 @@ public class CacheController{
|
|||
}
|
||||
return;
|
||||
}
|
||||
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.NOTIFICATIONS)).collect(Collectors.toList());
|
||||
List<LegacyFilter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(FilterContext.NOTIFICATIONS)).collect(Collectors.toList());
|
||||
if(!forceReload){
|
||||
SQLiteDatabase db=getOrOpenDatabase();
|
||||
try(Cursor cursor=db.query(onlyMentions ? "notifications_mentions" : "notifications_all", new String[]{"json"}, maxID==null ? null : "`id`<?", maxID==null ? null : new String[]{maxID}, null, null, "`time` DESC", count+"")){
|
||||
|
@ -153,7 +154,7 @@ public class CacheController{
|
|||
ntf.postprocess();
|
||||
newMaxID=ntf.id;
|
||||
if(ntf.status!=null){
|
||||
for(Filter filter:filters){
|
||||
for(LegacyFilter filter:filters){
|
||||
if(filter.matches(ntf.status))
|
||||
continue outer;
|
||||
}
|
||||
|
@ -176,7 +177,7 @@ public class CacheController{
|
|||
public void onSuccess(List<Notification> result){
|
||||
PaginatedResponse<List<Notification>> res=new PaginatedResponse<>(result.stream().filter(ntf->{
|
||||
if(ntf.status!=null){
|
||||
for(Filter filter:filters){
|
||||
for(LegacyFilter filter:filters){
|
||||
if(filter.matches(ntf.status)){
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -122,13 +122,17 @@ public class MastodonAPIController{
|
|||
Log.d(TAG, "["+(session==null ? "no-auth" : session.getID())+"] response body: "+respJson);
|
||||
if(req.respTypeToken!=null)
|
||||
respObj=gson.fromJson(respJson, req.respTypeToken.getType());
|
||||
else
|
||||
else if(req.respClass!=null)
|
||||
respObj=gson.fromJson(respJson, req.respClass);
|
||||
else
|
||||
respObj=null;
|
||||
}else{
|
||||
if(req.respTypeToken!=null)
|
||||
respObj=gson.fromJson(reader, req.respTypeToken.getType());
|
||||
else
|
||||
else if(req.respClass!=null)
|
||||
respObj=gson.fromJson(reader, req.respClass);
|
||||
else
|
||||
respObj=null;
|
||||
}
|
||||
}catch(JsonIOException|JsonSyntaxException x){
|
||||
if(BuildConfig.DEBUG)
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package org.joinmastodon.android.api;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
public abstract class ResultlessMastodonAPIRequest extends MastodonAPIRequest<Void>{
|
||||
public ResultlessMastodonAPIRequest(HttpMethod method, String path){
|
||||
super(method, path, (Class<Void>)null);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
|
||||
public class UpdateAccountCredentialsPreferences extends MastodonAPIRequest<Account>{
|
||||
public UpdateAccountCredentialsPreferences(Preferences preferences, Boolean locked, Boolean discoverable){
|
||||
super(HttpMethod.PATCH, "/accounts/update_credentials", Account.class);
|
||||
setRequestBody(new Request(locked, discoverable, new RequestSource(preferences.postingDefaultVisibility, preferences.postingDefaultLanguage)));
|
||||
}
|
||||
|
||||
private static class Request{
|
||||
public Boolean locked, discoverable;
|
||||
public RequestSource source;
|
||||
|
||||
public Request(Boolean locked, Boolean discoverable, RequestSource source){
|
||||
this.locked=locked;
|
||||
this.discoverable=discoverable;
|
||||
this.source=source;
|
||||
}
|
||||
}
|
||||
|
||||
private static class RequestSource{
|
||||
public StatusPrivacy privacy;
|
||||
public String language;
|
||||
|
||||
public RequestSource(StatusPrivacy privacy, String language){
|
||||
this.privacy=privacy;
|
||||
this.language=language;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FilterKeyword;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class CreateFilter extends MastodonAPIRequest<Filter>{
|
||||
public CreateFilter(String title, EnumSet<FilterContext> context, FilterAction action, int expiresIn, List<FilterKeyword> words){
|
||||
super(HttpMethod.POST, "/filters", Filter.class);
|
||||
setRequestBody(new FilterRequest(title, context, action, expiresIn==0 ? null : expiresIn, words.stream().map(w->new KeywordAttribute(null, null, w.keyword, w.wholeWord)).collect(Collectors.toList())));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v2";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||
|
||||
public class DeleteFilter extends ResultlessMastodonAPIRequest{
|
||||
public DeleteFilter(String id){
|
||||
super(HttpMethod.DELETE, "/filters/"+id);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v2";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
|
||||
class FilterRequest{
|
||||
public String title;
|
||||
public EnumSet<FilterContext> context;
|
||||
public FilterAction filterAction;
|
||||
public Integer expiresIn;
|
||||
public List<KeywordAttribute> keywordsAttributes;
|
||||
|
||||
public FilterRequest(String title, EnumSet<FilterContext> context, FilterAction filterAction, Integer expiresIn, List<KeywordAttribute> keywordsAttributes){
|
||||
this.title=title;
|
||||
this.context=context;
|
||||
this.filterAction=filterAction;
|
||||
this.expiresIn=expiresIn;
|
||||
this.keywordsAttributes=keywordsAttributes;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package org.joinmastodon.android.api.requests.accounts;
|
||||
package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
|
@ -7,8 +7,13 @@ import org.joinmastodon.android.model.Filter;
|
|||
|
||||
import java.util.List;
|
||||
|
||||
public class GetWordFilters extends MastodonAPIRequest<List<Filter>>{
|
||||
public GetWordFilters(){
|
||||
public class GetFilters extends MastodonAPIRequest<List<Filter>>{
|
||||
public GetFilters(){
|
||||
super(HttpMethod.GET, "/filters", new TypeToken<>(){});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v2";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class GetLegacyFilters extends MastodonAPIRequest<List<LegacyFilter>>{
|
||||
public GetLegacyFilters(){
|
||||
super(HttpMethod.GET, "/filters", new TypeToken<>(){});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
class KeywordAttribute{
|
||||
public String id;
|
||||
@SerializedName("_destroy")
|
||||
public Boolean delete;
|
||||
public String keyword;
|
||||
public Boolean wholeWord;
|
||||
|
||||
public KeywordAttribute(String id, Boolean delete, String keyword, Boolean wholeWord){
|
||||
this.id=id;
|
||||
this.delete=delete;
|
||||
this.keyword=keyword;
|
||||
this.wholeWord=wholeWord;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package org.joinmastodon.android.api.requests.filters;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FilterKeyword;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class UpdateFilter extends MastodonAPIRequest<Filter>{
|
||||
public UpdateFilter(String id, String title, EnumSet<FilterContext> context, FilterAction action, int expiresIn, List<FilterKeyword> words, List<String> deletedWords){
|
||||
super(HttpMethod.PUT, "/filters/"+id, Filter.class);
|
||||
|
||||
List<KeywordAttribute> attrs=Stream.of(
|
||||
words.stream().map(w->new KeywordAttribute(w.id, null, w.keyword, w.wholeWord)),
|
||||
deletedWords.stream().map(wid->new KeywordAttribute(wid, true, null, null))
|
||||
).flatMap(Function.identity()).collect(Collectors.toList());
|
||||
setRequestBody(new FilterRequest(title, context, action, expiresIn==0 ? null : expiresIn, attrs));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPathPrefix(){
|
||||
return "/api/v2";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package org.joinmastodon.android.api.requests.instance;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
public class GetInstanceExtendedDescription extends MastodonAPIRequest<GetInstanceExtendedDescription.Response>{
|
||||
public GetInstanceExtendedDescription(){
|
||||
super(HttpMethod.GET, "/instance/extended_description", Response.class);
|
||||
}
|
||||
|
||||
public static class Response{
|
||||
public Instant updatedAt;
|
||||
public String content;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package org.joinmastodon.android.api.session;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
public class AccountLocalPreferences{
|
||||
private final SharedPreferences prefs;
|
||||
|
||||
public boolean showInteractionCounts;
|
||||
public boolean customEmojiInNames;
|
||||
public boolean showCWs;
|
||||
public boolean hideSensitiveMedia;
|
||||
|
||||
public AccountLocalPreferences(SharedPreferences prefs){
|
||||
this.prefs=prefs;
|
||||
showInteractionCounts=prefs.getBoolean("interactionCounts", true);
|
||||
customEmojiInNames=prefs.getBoolean("emojiInNames", true);
|
||||
showCWs=prefs.getBoolean("showCWs", true);
|
||||
hideSensitiveMedia=prefs.getBoolean("hideSensitive", true);
|
||||
}
|
||||
|
||||
public long getNotificationsPauseEndTime(){
|
||||
return prefs.getLong("notificationsPauseTime", 0L);
|
||||
}
|
||||
|
||||
public void setNotificationsPauseEndTime(long time){
|
||||
prefs.edit().putLong("notificationsPauseTime", time).apply();
|
||||
}
|
||||
|
||||
public void save(){
|
||||
prefs.edit()
|
||||
.putBoolean("interactionCounts", showInteractionCounts)
|
||||
.putBoolean("emojiInNames", customEmojiInNames)
|
||||
.putBoolean("showCWs", showCWs)
|
||||
.putBoolean("hideSensitive", hideSensitiveMedia)
|
||||
.apply();
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package org.joinmastodon.android.api.session;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.text.TextUtils;
|
||||
|
@ -7,17 +8,20 @@ import android.util.Log;
|
|||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.CacheController;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||
import org.joinmastodon.android.api.StatusInteractionController;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetPreferences;
|
||||
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentialsPreferences;
|
||||
import org.joinmastodon.android.api.requests.markers.GetMarkers;
|
||||
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
||||
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
|
||||
import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Application;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
import org.joinmastodon.android.model.TimelineMarkers;
|
||||
|
@ -46,7 +50,7 @@ public class AccountSession{
|
|||
public PushSubscription pushSubscription;
|
||||
public boolean needUpdatePushSettings;
|
||||
public long filtersLastUpdated;
|
||||
public List<Filter> wordFilters=new ArrayList<>();
|
||||
public List<LegacyFilter> wordFilters=new ArrayList<>();
|
||||
public String pushAccountID;
|
||||
public AccountActivationInfo activationInfo;
|
||||
public Preferences preferences;
|
||||
|
@ -55,6 +59,8 @@ public class AccountSession{
|
|||
private transient CacheController cacheController;
|
||||
private transient PushSubscriptionManager pushSubscriptionManager;
|
||||
private transient SharedPreferences prefs;
|
||||
private transient boolean preferencesNeedSaving;
|
||||
private transient AccountLocalPreferences localPreferences;
|
||||
|
||||
AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){
|
||||
this.token=token;
|
||||
|
@ -106,7 +112,8 @@ public class AccountSession{
|
|||
@Override
|
||||
public void onSuccess(Preferences result){
|
||||
preferences=result;
|
||||
callback.accept(result);
|
||||
if(callback!=null)
|
||||
callback.accept(result);
|
||||
AccountSessionManager.getInstance().writeAccountsFile();
|
||||
}
|
||||
|
||||
|
@ -118,7 +125,7 @@ public class AccountSession{
|
|||
.exec(getID());
|
||||
}
|
||||
|
||||
public SharedPreferences getLocalPreferences(){
|
||||
public SharedPreferences getRawLocalPreferences(){
|
||||
if(prefs==null)
|
||||
prefs=MastodonApp.context.getSharedPreferences(getID(), Context.MODE_PRIVATE);
|
||||
return prefs;
|
||||
|
@ -150,11 +157,60 @@ public class AccountSession{
|
|||
}
|
||||
|
||||
public String getLastKnownNotificationsMarker(){
|
||||
return getLocalPreferences().getString("notificationsMarker", null);
|
||||
return getRawLocalPreferences().getString("notificationsMarker", null);
|
||||
}
|
||||
|
||||
public void setNotificationsMarker(String id, boolean clearUnread){
|
||||
getLocalPreferences().edit().putString("notificationsMarker", id).apply();
|
||||
getRawLocalPreferences().edit().putString("notificationsMarker", id).apply();
|
||||
E.post(new NotificationsMarkerUpdatedEvent(getID(), id, clearUnread));
|
||||
}
|
||||
|
||||
public void logOut(Activity activity, Runnable onDone){
|
||||
new RevokeOauthToken(app.clientId, app.clientSecret, token.accessToken)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Object result){
|
||||
AccountSessionManager.getInstance().removeAccount(getID());
|
||||
onDone.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
AccountSessionManager.getInstance().removeAccount(getID());
|
||||
onDone.run();
|
||||
}
|
||||
})
|
||||
.wrapProgress(activity, R.string.loading, false)
|
||||
.exec(getID());
|
||||
}
|
||||
|
||||
public void savePreferencesLater(){
|
||||
preferencesNeedSaving=true;
|
||||
}
|
||||
|
||||
public void savePreferencesIfPending(){
|
||||
if(preferencesNeedSaving){
|
||||
new UpdateAccountCredentialsPreferences(preferences, null, null)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Account result){
|
||||
preferencesNeedSaving=false;
|
||||
self=result;
|
||||
AccountSessionManager.getInstance().writeAccountsFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
Log.e(TAG, "failed to save preferences: "+error);
|
||||
}
|
||||
})
|
||||
.exec(getID());
|
||||
}
|
||||
}
|
||||
|
||||
public AccountLocalPreferences getLocalPreferences(){
|
||||
if(localPreferences==null)
|
||||
localPreferences=new AccountLocalPreferences(getRawLocalPreferences());
|
||||
return localPreferences;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,8 +13,6 @@ import android.net.Uri;
|
|||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
|
@ -22,7 +20,7 @@ import org.joinmastodon.android.MastodonApp;
|
|||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetWordFilters;
|
||||
import org.joinmastodon.android.api.requests.filters.GetLegacyFilters;
|
||||
import org.joinmastodon.android.api.requests.instance.GetCustomEmojis;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
|
||||
import org.joinmastodon.android.api.requests.instance.GetInstance;
|
||||
|
@ -32,7 +30,7 @@ import org.joinmastodon.android.model.Account;
|
|||
import org.joinmastodon.android.model.Application;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.EmojiCategory;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
|
||||
|
@ -190,6 +188,7 @@ public class AccountSessionManager{
|
|||
lastActiveAccountID=null;
|
||||
else
|
||||
lastActiveAccountID=getLoggedInAccounts().get(0).getID();
|
||||
prefs.edit().putString("lastActiveAccount", lastActiveAccountID).apply();
|
||||
}
|
||||
writeAccountsFile();
|
||||
String domain=session.domain.toLowerCase();
|
||||
|
@ -299,10 +298,10 @@ public class AccountSessionManager{
|
|||
}
|
||||
|
||||
private void updateSessionWordFilters(AccountSession session){
|
||||
new GetWordFilters()
|
||||
new GetLegacyFilters()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<Filter> result){
|
||||
public void onSuccess(List<LegacyFilter> result){
|
||||
session.wordFilters=result;
|
||||
session.filtersLastUpdated=System.currentTimeMillis();
|
||||
writeAccountsFile();
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package org.joinmastodon.android.events;
|
||||
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
|
||||
public class SettingsFilterCreatedOrUpdatedEvent{
|
||||
public final String accountID;
|
||||
public final Filter filter;
|
||||
|
||||
public SettingsFilterCreatedOrUpdatedEvent(String accountID, Filter filter){
|
||||
this.accountID=accountID;
|
||||
this.filter=filter;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package org.joinmastodon.android.events;
|
||||
|
||||
public class SettingsFilterDeletedEvent{
|
||||
public final String accountID;
|
||||
public final String filterID;
|
||||
|
||||
public SettingsFilterDeletedEvent(String accountID, String filterID){
|
||||
this.accountID=accountID;
|
||||
this.filterID=filterID;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package org.joinmastodon.android.events;
|
||||
|
||||
public class StatusDisplaySettingsChangedEvent{
|
||||
public final String accountID;
|
||||
|
||||
public StatusDisplaySettingsChangedEvent(String accountID){
|
||||
this.accountID=accountID;
|
||||
}
|
||||
}
|
|
@ -554,6 +554,14 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
|||
return attachmentViewsPool;
|
||||
}
|
||||
|
||||
public void rebuildAllDisplayItems(){
|
||||
displayItems.clear();
|
||||
for(T item:data){
|
||||
displayItems.addAll(buildDisplayItems(item));
|
||||
}
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> holder){}
|
||||
|
||||
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
|
||||
|
|
|
@ -46,6 +46,7 @@ import android.widget.TextView;
|
|||
import com.twitter.twittertext.TwitterTextEmojiRegex;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonErrorResponse;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetPreferences;
|
||||
|
@ -546,7 +547,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
if(item.getItemId()==R.id.publish){
|
||||
publish();
|
||||
if(GlobalUserPreferences.altTextReminders)
|
||||
checkAltTextsAndPublish();
|
||||
else
|
||||
publish();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -641,6 +645,28 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
return true;
|
||||
}
|
||||
|
||||
private void checkAltTextsAndPublish(){
|
||||
int count=mediaViewController.getMissingAltTextAttachmentCount();
|
||||
if(count==0){
|
||||
publish();
|
||||
}else{
|
||||
String msg=getResources().getQuantityString(mediaViewController.areAllAttachmentsImages() ? R.plurals.alt_text_reminder_x_images : R.plurals.alt_text_reminder_x_attachments,
|
||||
count, switch(count){
|
||||
case 1 -> getString(R.string.count_one);
|
||||
case 2 -> getString(R.string.count_two);
|
||||
case 3 -> getString(R.string.count_three);
|
||||
case 4 -> getString(R.string.count_four);
|
||||
default -> String.valueOf(count);
|
||||
});
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.alt_text_reminder_title)
|
||||
.setMessage(msg)
|
||||
.setPositiveButton(R.string.alt_text_reminder_post_anyway, (dlg, item)->publish())
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void publish(){
|
||||
sendingOverlay=new View(getActivity());
|
||||
WindowManager.LayoutParams overlayParams=new WindowManager.LayoutParams();
|
||||
|
@ -655,7 +681,6 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
|||
publishButton.setEnabled(false);
|
||||
V.setVisibilityAnimated(sendProgress, View.VISIBLE);
|
||||
|
||||
|
||||
mediaViewController.saveAltTextsBeforePublishing(this::actuallyPublish, this::handlePublishError);
|
||||
}
|
||||
|
||||
|
|
|
@ -3,14 +3,11 @@ package org.joinmastodon.android.fragments;
|
|||
import android.annotation.SuppressLint;
|
||||
import android.app.Fragment;
|
||||
import android.app.NotificationManager;
|
||||
import android.graphics.Outline;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewOutlineProvider;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.FrameLayout;
|
||||
|
@ -20,19 +17,19 @@ import android.widget.TextView;
|
|||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.PushNotificationReceiver;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.markers.GetMarkers;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent;
|
||||
import org.joinmastodon.android.events.StatusDisplaySettingsChangedEvent;
|
||||
import org.joinmastodon.android.fragments.discover.DiscoverFragment;
|
||||
import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestionsFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.model.TimelineMarkers;
|
||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
@ -265,7 +262,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
|||
new AccountSwitcherSheet(getActivity(), this).show();
|
||||
return true;
|
||||
}
|
||||
if(tab==R.id.tab_home){
|
||||
if(tab==R.id.tab_home && BuildConfig.DEBUG){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), OnboardingFollowSuggestionsFragment.class, args);
|
||||
|
@ -328,7 +325,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
|||
notificationsBadge.setVisibility(View.GONE);
|
||||
}else{
|
||||
notificationsBadge.setVisibility(View.VISIBLE);
|
||||
if(notifications.get(notifications.size()-1).id.compareTo(marker)<=0){
|
||||
if(notifications.get(notifications.size()-1).id.compareTo(marker)>0){
|
||||
notificationsBadge.setText(String.format("%d+", notifications.size()));
|
||||
}else{
|
||||
int count=0;
|
||||
|
@ -349,4 +346,14 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
|||
if(ev.clearUnread)
|
||||
notificationsBadge.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onStatusDisplaySettingsChanged(StatusDisplaySettingsChangedEvent ev){
|
||||
if(!ev.accountID.equals(accountID))
|
||||
return;
|
||||
if(homeTimelineFragment.loaded)
|
||||
homeTimelineFragment.rebuildAllDisplayItems();
|
||||
if(notificationsFragment.loaded)
|
||||
notificationsFragment.rebuildAllDisplayItems();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,8 +30,10 @@ import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
|
|||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
|
||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.TimelineMarkers;
|
||||
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
|
||||
|
@ -123,7 +125,7 @@ public class HomeTimelineFragment extends StatusListFragment{
|
|||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), SettingsFragment.class, args);
|
||||
Nav.go(getActivity(), SettingsMainFragment.class, args);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -200,7 +202,7 @@ public class HomeTimelineFragment extends StatusListFragment{
|
|||
result.get(result.size()-1).hasGapAfter=true;
|
||||
toAdd=result;
|
||||
}
|
||||
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
|
||||
List<LegacyFilter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(FilterContext.HOME)).collect(Collectors.toList());
|
||||
if(!filters.isEmpty()){
|
||||
toAdd=toAdd.stream().filter(new StatusFilterPredicate(filters)).collect(Collectors.toList());
|
||||
}
|
||||
|
@ -277,12 +279,12 @@ public class HomeTimelineFragment extends StatusListFragment{
|
|||
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
|
||||
targetList.clear();
|
||||
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
|
||||
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.HOME)).collect(Collectors.toList());
|
||||
List<LegacyFilter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(FilterContext.HOME)).collect(Collectors.toList());
|
||||
outer:
|
||||
for(Status s:result){
|
||||
if(idsBelowGap.contains(s.id))
|
||||
break;
|
||||
for(Filter filter:filters){
|
||||
for(LegacyFilter filter:filters){
|
||||
if(filter.matches(s)){
|
||||
continue outer;
|
||||
}
|
||||
|
@ -444,6 +446,11 @@ public class HomeTimelineFragment extends StatusListFragment{
|
|||
getToolbar().getMenu().findItem(R.id.settings).setIcon(R.drawable.ic_settings_24_badged);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean wantsToolbarMenuIconsTinted(){
|
||||
return false;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
|
||||
updateUpdateState(ev.state);
|
||||
|
|
|
@ -315,10 +315,12 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
|||
|
||||
private void markAsRead(){
|
||||
String id=data.get(0).id;
|
||||
new SaveMarkers(null, id).exec(accountID);
|
||||
AccountSessionManager.get(accountID).setNotificationsMarker(id, true);
|
||||
realUnreadMarker=id;
|
||||
updateMarkAllReadButton();
|
||||
if(ObjectIdComparator.INSTANCE.compare(id, realUnreadMarker)>0){
|
||||
new SaveMarkers(null, id).exec(accountID);
|
||||
AccountSessionManager.get(accountID).setNotificationsMarker(id, true);
|
||||
realUnreadMarker=id;
|
||||
updateMarkAllReadButton();
|
||||
}
|
||||
}
|
||||
|
||||
private void resetUnreadBackground(){
|
||||
|
|
|
@ -458,7 +458,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
|||
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
|
||||
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName);
|
||||
HtmlParser.parseCustomEmoji(ssb, account.emojis);
|
||||
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
|
||||
HtmlParser.parseCustomEmoji(ssb, account.emojis);
|
||||
name.setText(ssb);
|
||||
setTitle(ssb);
|
||||
|
||||
|
|
|
@ -1,761 +0,0 @@
|
|||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
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.view.animation.LinearInterpolator;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.E;
|
||||
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.PushSubscriptionManager;
|
||||
import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken;
|
||||
import org.joinmastodon.android.api.session.AccountActivationInfo;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
|
||||
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 org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
|
||||
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.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
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 MastodonToolbarFragment{
|
||||
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);
|
||||
|
||||
if(GithubSelfUpdater.needSelfUpdating()){
|
||||
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||
GithubSelfUpdater.UpdateState state=updater.getState();
|
||||
if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING){
|
||||
items.add(new UpdateItem());
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
if(BuildConfig.DEBUG){
|
||||
items.add(new RedHeaderItem("Debug options"));
|
||||
items.add(new TextItem("Test e-mail confirmation flow", ()->{
|
||||
AccountSession sess=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
sess.activated=false;
|
||||
sess.activationInfo=new AccountActivationInfo("test@email", System.currentTimeMillis());
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putBoolean("debug", true);
|
||||
Nav.goClearingStack(getActivity(), AccountActivationFragment.class, args);
|
||||
}));
|
||||
}
|
||||
|
||||
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()>1)
|
||||
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 && PushSubscriptionManager.arePushNotificationsAvailable()){
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().updatePushSettings(pushSubscription);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
if(GithubSelfUpdater.needSelfUpdating())
|
||||
E.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView(){
|
||||
super.onDestroyView();
|
||||
if(GithubSelfUpdater.needSelfUpdating())
|
||||
E.unregister(this);
|
||||
}
|
||||
|
||||
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_LAYOUT_IN_SCREEN | 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 si){
|
||||
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);
|
||||
getActivity().finish();
|
||||
Intent intent=new Intent(getActivity(), MainActivity.class);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void clearImageCache(){
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
Activity activity=getActivity();
|
||||
ImageCache.getInstance(getActivity()).clear();
|
||||
Toast.makeText(activity, R.string.media_cache_cleared, Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
|
||||
if(items.get(0) instanceof UpdateItem item){
|
||||
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(0);
|
||||
if(holder instanceof UpdateViewHolder uvh){
|
||||
uvh.bind(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public HeaderItem(String text){
|
||||
this.text=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;
|
||||
}
|
||||
|
||||
public TextItem(String text, Runnable onClick){
|
||||
this.text=text;
|
||||
this.onClick=onClick;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
private class RedHeaderItem extends HeaderItem{
|
||||
|
||||
public RedHeaderItem(int text){
|
||||
super(text);
|
||||
}
|
||||
|
||||
public RedHeaderItem(String 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 UpdateItem extends Item{
|
||||
|
||||
@Override
|
||||
public int getViewType(){
|
||||
return 7;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
case 7 -> new UpdateViewHolder();
|
||||
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(UiUtils.isDarkTheme() ? R.color.error_400 : 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;
|
||||
public RadioButton 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.setChecked(checked);
|
||||
}
|
||||
|
||||
public void setChecked(boolean checked){
|
||||
checkbox.setChecked(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);
|
||||
}
|
||||
}
|
||||
|
||||
private class UpdateViewHolder extends BindableViewHolder<UpdateItem>{
|
||||
|
||||
private final TextView text;
|
||||
private final Button button;
|
||||
private final ImageButton cancelBtn;
|
||||
private final ProgressBar progress;
|
||||
|
||||
private ObjectAnimator rotationAnimator;
|
||||
private Runnable progressUpdater=this::updateProgress;
|
||||
|
||||
public UpdateViewHolder(){
|
||||
super(getActivity(), R.layout.item_settings_update, list);
|
||||
text=findViewById(R.id.text);
|
||||
button=findViewById(R.id.button);
|
||||
cancelBtn=findViewById(R.id.cancel_btn);
|
||||
progress=findViewById(R.id.progress);
|
||||
button.setOnClickListener(v->{
|
||||
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||
switch(updater.getState()){
|
||||
case UPDATE_AVAILABLE -> updater.downloadUpdate();
|
||||
case DOWNLOADED -> updater.installUpdate(getActivity());
|
||||
}
|
||||
});
|
||||
cancelBtn.setOnClickListener(v->GithubSelfUpdater.getInstance().cancelDownload());
|
||||
rotationAnimator=ObjectAnimator.ofFloat(progress, View.ROTATION, 0f, 360f);
|
||||
rotationAnimator.setInterpolator(new LinearInterpolator());
|
||||
rotationAnimator.setDuration(1500);
|
||||
rotationAnimator.setRepeatCount(ObjectAnimator.INFINITE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(UpdateItem item){
|
||||
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||
GithubSelfUpdater.UpdateInfo info=updater.getUpdateInfo();
|
||||
GithubSelfUpdater.UpdateState state=updater.getState();
|
||||
if(state!=GithubSelfUpdater.UpdateState.DOWNLOADED){
|
||||
text.setText(getString(R.string.update_available, info.version));
|
||||
button.setText(getString(R.string.download_update, UiUtils.formatFileSize(getActivity(), info.size, false)));
|
||||
}else{
|
||||
text.setText(getString(R.string.update_ready, info.version));
|
||||
button.setText(R.string.install_update);
|
||||
}
|
||||
if(state==GithubSelfUpdater.UpdateState.DOWNLOADING){
|
||||
rotationAnimator.start();
|
||||
button.setVisibility(View.INVISIBLE);
|
||||
cancelBtn.setVisibility(View.VISIBLE);
|
||||
progress.setVisibility(View.VISIBLE);
|
||||
updateProgress();
|
||||
}else{
|
||||
rotationAnimator.cancel();
|
||||
button.setVisibility(View.VISIBLE);
|
||||
cancelBtn.setVisibility(View.GONE);
|
||||
progress.setVisibility(View.GONE);
|
||||
progress.removeCallbacks(progressUpdater);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateProgress(){
|
||||
GithubSelfUpdater updater=GithubSelfUpdater.getInstance();
|
||||
if(updater.getState()!=GithubSelfUpdater.UpdateState.DOWNLOADING)
|
||||
return;
|
||||
int value=Math.round(progress.getMax()*updater.getDownloadProgress());
|
||||
if(Build.VERSION.SDK_INT>=24)
|
||||
progress.setProgress(value, true);
|
||||
else
|
||||
progress.setProgress(value);
|
||||
progress.postDelayed(progressUpdater, 1000);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,6 @@
|
|||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
@ -13,7 +11,8 @@ import org.joinmastodon.android.api.requests.statuses.GetStatusContext;
|
|||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusContext;
|
||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||
|
@ -47,7 +46,10 @@ public class ThreadFragment extends StatusListFragment{
|
|||
knownAccounts.put(inReplyToAccount.id, inReplyToAccount);
|
||||
data.add(mainStatus);
|
||||
onAppendItems(Collections.singletonList(mainStatus));
|
||||
setTitle(HtmlParser.parseCustomEmoji(getString(R.string.post_from_user, mainStatus.account.displayName), mainStatus.account.emojis));
|
||||
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
|
||||
setTitle(HtmlParser.parseCustomEmoji(getString(R.string.post_from_user, mainStatus.account.displayName), mainStatus.account.emojis));
|
||||
else
|
||||
setTitle(getString(R.string.post_from_user, mainStatus.account.displayName));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -102,11 +104,11 @@ public class ThreadFragment extends StatusListFragment{
|
|||
}
|
||||
|
||||
private List<Status> filterStatuses(List<Status> statuses){
|
||||
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.THREAD)).collect(Collectors.toList());
|
||||
List<LegacyFilter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(FilterContext.THREAD)).collect(Collectors.toList());
|
||||
if(filters.isEmpty())
|
||||
return statuses;
|
||||
return statuses.stream().filter(status->{
|
||||
for(Filter filter:filters){
|
||||
for(LegacyFilter filter:filters){
|
||||
if(filter.matches(status))
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -95,7 +95,7 @@ public class ComposeAccountSearchFragment extends BaseAccountListFragment{
|
|||
@Override
|
||||
public void onSuccess(SearchResults result){
|
||||
setEmptyText(R.string.no_search_results);
|
||||
onDataLoaded(result.accounts.stream().map(AccountViewModel::new).collect(Collectors.toList()), false);
|
||||
onDataLoaded(result.accounts.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), false);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
|
|
|
@ -24,7 +24,7 @@ public abstract class PaginatedAccountListFragment extends BaseAccountListFragme
|
|||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||
else
|
||||
nextMaxID=null;
|
||||
onDataLoaded(result.stream().map(AccountViewModel::new).collect(Collectors.toList()), nextMaxID!=null);
|
||||
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), nextMaxID!=null);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
|
|
|
@ -5,7 +5,7 @@ import android.view.View;
|
|||
|
||||
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
|
||||
import org.joinmastodon.android.fragments.StatusListFragment;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
|
||||
import org.joinmastodon.android.utils.StatusFilterPredicate;
|
||||
|
@ -27,7 +27,7 @@ public class LocalTimelineFragment extends StatusListFragment{
|
|||
public void onSuccess(List<Status> result){
|
||||
if(!result.isEmpty())
|
||||
maxID=result.get(result.size()-1).id;
|
||||
onDataLoaded(result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList()), !result.isEmpty());
|
||||
onDataLoaded(result.stream().filter(new StatusFilterPredicate(accountID, FilterContext.PUBLIC)).collect(Collectors.toList()), !result.isEmpty());
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
|
|
|
@ -3,7 +3,6 @@ package org.joinmastodon.android.fragments.onboarding;
|
|||
import android.annotation.SuppressLint;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
@ -23,8 +22,7 @@ import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
|
|||
import org.joinmastodon.android.api.session.AccountActivationInfo;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.HomeFragment;
|
||||
import org.joinmastodon.android.fragments.SettingsFragment;
|
||||
import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
@ -38,7 +36,6 @@ import me.grishka.appkit.api.APIRequest;
|
|||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.ToolbarFragment;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class AccountActivationFragment extends ToolbarFragment{
|
||||
private String accountID;
|
||||
|
@ -70,7 +67,7 @@ public class AccountActivationFragment extends ToolbarFragment{
|
|||
openEmailBtn.setOnLongClickListener(v->{
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), SettingsFragment.class, args);
|
||||
Nav.go(getActivity(), SettingsMainFragment.class, args);
|
||||
return true;
|
||||
});
|
||||
resendBtn=view.findViewById(R.id.btn_resend);
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package org.joinmastodon.android.fragments.onboarding;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.Html;
|
||||
import android.view.LayoutInflater;
|
||||
|
@ -15,20 +13,16 @@ import android.widget.TextView;
|
|||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.adapters.InstanceRulesAdapter;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.fragments.ToolbarFragment;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
|
@ -68,9 +62,8 @@ public class InstanceRulesFragment extends ToolbarFragment{
|
|||
|
||||
adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
|
||||
adapter.addAdapter(new ItemsAdapter());
|
||||
adapter.addAdapter(new InstanceRulesAdapter(instance.rules));
|
||||
list.setAdapter(adapter);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3SurfaceVariant, 1, 56, 0, DividerItemDecoration.NOT_FIRST));
|
||||
|
||||
btn=view.findViewById(R.id.btn_next);
|
||||
btn.setOnClickListener(v->onButtonClick());
|
||||
|
@ -113,43 +106,4 @@ public class InstanceRulesFragment extends ToolbarFragment{
|
|||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
|
||||
}
|
||||
|
||||
private class ItemsAdapter extends RecyclerView.Adapter<ItemViewHolder>{
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new ItemViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ItemViewHolder holder, int position){
|
||||
holder.bind(instance.rules.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return instance.rules.size();
|
||||
}
|
||||
}
|
||||
|
||||
private class ItemViewHolder extends BindableViewHolder<Instance.Rule>{
|
||||
private final TextView text, number;
|
||||
|
||||
public ItemViewHolder(){
|
||||
super(getActivity(), R.layout.item_server_rule, list);
|
||||
text=findViewById(R.id.text);
|
||||
number=findViewById(R.id.number);
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
@Override
|
||||
public void onBind(Instance.Rule item){
|
||||
if(item.parsedText==null){
|
||||
item.parsedText=HtmlParser.parseLinks(item.text);
|
||||
}
|
||||
text.setText(item.parsedText);
|
||||
number.setText(String.format("%d", getAbsoluteAdapterPosition()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,8 +22,8 @@ import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
|||
import org.joinmastodon.android.fragments.HomeFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.model.FollowSuggestion;
|
||||
import org.joinmastodon.android.model.ParsedAccount;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
||||
|
@ -52,7 +52,7 @@ import me.grishka.appkit.utils.V;
|
|||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment<ParsedAccount>{
|
||||
public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment<AccountViewModel>{
|
||||
private String accountID;
|
||||
private Map<String, Relationship> relationships=Collections.emptyMap();
|
||||
private GetAccountRelationships relationshipsRequest;
|
||||
|
@ -97,7 +97,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment<Pa
|
|||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<FollowSuggestion> result){
|
||||
onDataLoaded(result.stream().map(fs->new ParsedAccount(fs.account, accountID)).collect(Collectors.toList()), false);
|
||||
onDataLoaded(result.stream().map(fs->new AccountViewModel(fs.account, accountID)).collect(Collectors.toList()), false);
|
||||
loadRelationships();
|
||||
}
|
||||
})
|
||||
|
@ -146,7 +146,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment<Pa
|
|||
return;
|
||||
}
|
||||
ArrayList<String> accountIdsToFollow=new ArrayList<>();
|
||||
for(ParsedAccount acc:data){
|
||||
for(AccountViewModel acc:data){
|
||||
Relationship rel=relationships.get(acc.account.id);
|
||||
if(rel==null)
|
||||
continue;
|
||||
|
@ -239,14 +239,14 @@ public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment<Pa
|
|||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int position, int image){
|
||||
ParsedAccount account=data.get(position);
|
||||
AccountViewModel account=data.get(position);
|
||||
if(image==0)
|
||||
return account.avatarRequest;
|
||||
return account.avaRequest;
|
||||
return account.emojiHelper.getImageRequest(image-1);
|
||||
}
|
||||
}
|
||||
|
||||
private class SuggestionViewHolder extends BindableViewHolder<ParsedAccount> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
|
||||
private class SuggestionViewHolder extends BindableViewHolder<AccountViewModel> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
|
||||
private final TextView name, username, bio;
|
||||
private final ImageView avatar;
|
||||
private final ProgressBarButton actionButton;
|
||||
|
@ -271,7 +271,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseRecyclerFragment<Pa
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onBind(ParsedAccount item){
|
||||
public void onBind(AccountViewModel item){
|
||||
name.setText(item.parsedName);
|
||||
username.setText(item.account.getDisplayUsername());
|
||||
if(TextUtils.isEmpty(item.parsedBio)){
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.MastodonRecyclerFragment;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
|
||||
import org.joinmastodon.android.ui.viewholders.CheckableListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.ListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.SimpleListItemViewHolder;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public abstract class BaseSettingsFragment<T> extends MastodonRecyclerFragment<ListItem<T>>{
|
||||
protected GenericListItemsAdapter<T> itemsAdapter;
|
||||
protected String accountID;
|
||||
|
||||
public BaseSettingsFragment(){
|
||||
super(20);
|
||||
}
|
||||
|
||||
public BaseSettingsFragment(int perPage){
|
||||
super(perPage);
|
||||
}
|
||||
|
||||
public BaseSettingsFragment(int layout, int perPage){
|
||||
super(layout, perPage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setRetainInstance(true);
|
||||
accountID=getArguments().getString("account");
|
||||
setRefreshEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
return itemsAdapter=new GenericListItemsAdapter<T>(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 1, 0, 0, vh->vh instanceof SimpleListItemViewHolder ivh && ivh.getItem().dividerAfter));
|
||||
list.setItemAnimator(new BetterItemAnimator());
|
||||
}
|
||||
|
||||
protected int indexOfItemsAdapter(){
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected void toggleCheckableItem(CheckableListItem<T> item){
|
||||
item.toggle();
|
||||
rebindItem(item);
|
||||
}
|
||||
|
||||
protected void rebindItem(ListItem<T> item){
|
||||
if(list==null)
|
||||
return;
|
||||
if(list.findViewHolderForAdapterPosition(indexOfItemsAdapter()+data.indexOf(item)) instanceof ListItemViewHolder<?> holder){
|
||||
holder.rebind();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
|
||||
list.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
emptyView.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
progress.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
}else{
|
||||
list.setPadding(0, 0, 0, 0);
|
||||
}
|
||||
super.onApplyWindowInsets(insets);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,324 @@
|
|||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.DatePicker;
|
||||
import android.widget.EditText;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.filters.CreateFilter;
|
||||
import org.joinmastodon.android.api.requests.filters.DeleteFilter;
|
||||
import org.joinmastodon.android.api.requests.filters.UpdateFilter;
|
||||
import org.joinmastodon.android.events.SettingsFilterCreatedOrUpdatedEvent;
|
||||
import org.joinmastodon.android.events.SettingsFilterDeletedEvent;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FilterKeyword;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.APIRequest;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
|
||||
public class EditFilterFragment extends BaseSettingsFragment<Void> implements OnBackPressedListener{
|
||||
private static final int WORDS_RESULT=370;
|
||||
private static final int CONTEXT_RESULT=651;
|
||||
|
||||
private Filter filter;
|
||||
private ListItem<Void> durationItem, wordsItem, contextItem;
|
||||
private CheckableListItem<Void> cwItem;
|
||||
private FloatingHintEditTextLayout titleEditLayout;
|
||||
private EditText titleEdit;
|
||||
|
||||
private Instant endsAt;
|
||||
private ArrayList<FilterKeyword> keywords=new ArrayList<>();
|
||||
private ArrayList<String> deletedWordIDs=new ArrayList<>();
|
||||
private EnumSet<FilterContext> context=EnumSet.allOf(FilterContext.class);
|
||||
private boolean dirty;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
filter=Parcels.unwrap(getArguments().getParcelable("filter"));
|
||||
setTitle(filter==null ? R.string.settings_add_filter : R.string.settings_edit_filter);
|
||||
onDataLoaded(List.of(
|
||||
durationItem=new ListItem<>(R.string.settings_filter_duration, 0, this::onDurationClick),
|
||||
wordsItem=new ListItem<>(R.string.settings_filter_muted_words, 0, this::onWordsClick),
|
||||
contextItem=new ListItem<>(R.string.settings_filter_context, 0, this::onContextClick),
|
||||
cwItem=new CheckableListItem<>(R.string.settings_filter_show_cw, R.string.settings_filter_show_cw_explanation, CheckableListItem.Style.SWITCH, filter==null || filter.filterAction==FilterAction.WARN, ()->toggleCheckableItem(cwItem))
|
||||
));
|
||||
|
||||
if(filter!=null){
|
||||
endsAt=filter.expiresAt;
|
||||
keywords.addAll(filter.keywords);
|
||||
context=filter.context;
|
||||
data.add(new ListItem<>(R.string.settings_delete_filter, 0, this::onDeleteClick, R.attr.colorM3Error, false));
|
||||
}
|
||||
|
||||
updateDurationItem();
|
||||
updateWordsItem();
|
||||
updateContextItem();
|
||||
setHasOptionsMenu(true);
|
||||
setRetainInstance(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
titleEditLayout=(FloatingHintEditTextLayout) getActivity().getLayoutInflater().inflate(R.layout.floating_hint_edit_text, list, false);
|
||||
titleEdit=titleEditLayout.findViewById(R.id.edit);
|
||||
titleEdit.setHint(R.string.settings_filter_title);
|
||||
titleEditLayout.updateHint();
|
||||
if(filter!=null)
|
||||
titleEdit.setText(filter.title);
|
||||
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(titleEditLayout));
|
||||
adapter.addAdapter(super.getAdapter());
|
||||
return adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int indexOfItemsAdapter(){
|
||||
return 1;
|
||||
}
|
||||
|
||||
private void onDurationClick(){
|
||||
int[] durationOptions={
|
||||
1800,
|
||||
3600,
|
||||
6*3600,
|
||||
12*3600,
|
||||
24*3600,
|
||||
3*24*3600,
|
||||
7*24*3600
|
||||
};
|
||||
ArrayList<String> options=Arrays.stream(durationOptions).mapToObj(d->UiUtils.formatDuration(getActivity(), d)).collect(Collectors.toCollection(ArrayList<String>::new));
|
||||
options.add(0, getString(R.string.filter_duration_forever));
|
||||
options.add(getString(R.string.filter_duration_custom));
|
||||
Instant[] newEnd={null};
|
||||
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.settings_filter_duration_title)
|
||||
.setSupportingText(endsAt==null ? null : getString(R.string.settings_filter_ends, UiUtils.formatRelativeTimestampAsMinutesAgo(getActivity(), endsAt, false)))
|
||||
.setSingleChoiceItems(options.toArray(new String[0]), -1, (dlg, item)->{
|
||||
AlertDialog a=(AlertDialog) dlg;
|
||||
if(item==options.size()-1){ // custom
|
||||
showCustomDurationAlert(date->{
|
||||
if(date==null){
|
||||
a.getListView().setItemChecked(item, false);
|
||||
}else{
|
||||
newEnd[0]=date;
|
||||
a.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
|
||||
}
|
||||
});
|
||||
}else{
|
||||
if(item==0){
|
||||
newEnd[0]=null;
|
||||
}else{
|
||||
newEnd[0]=Instant.now().plusSeconds(durationOptions[item-1]);
|
||||
}
|
||||
a.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
|
||||
}
|
||||
})
|
||||
.setPositiveButton(R.string.ok, (dlg, item)->{
|
||||
if(!Objects.equals(endsAt, newEnd[0])){
|
||||
endsAt=newEnd[0];
|
||||
updateDurationItem();
|
||||
dirty=true;
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
alert.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||
}
|
||||
|
||||
private void showCustomDurationAlert(Consumer<Instant> callback){
|
||||
DatePicker picker=new DatePicker(getActivity());
|
||||
picker.setMinDate(LocalDate.now().plusDays(1).atStartOfDay(ZoneId.systemDefault()).toEpochSecond()*1000L);
|
||||
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
|
||||
.setView(picker)
|
||||
.setPositiveButton(R.string.ok, (dlg, item)->{
|
||||
((AlertDialog)dlg).setOnDismissListener(null);
|
||||
callback.accept(LocalDate.of(picker.getYear(), picker.getMonth()+1, picker.getDayOfMonth()).atStartOfDay(ZoneId.systemDefault()).toInstant());
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
alert.setOnDismissListener(dialog->callback.accept(null));
|
||||
}
|
||||
|
||||
private void onWordsClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelableArrayList("words", (ArrayList<? extends Parcelable>) keywords.stream().map(Parcels::wrap).collect(Collectors.toCollection(ArrayList::new)));
|
||||
Nav.goForResult(getActivity(), FilterWordsFragment.class, args, WORDS_RESULT, this);
|
||||
}
|
||||
|
||||
private void onContextClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putSerializable("context", context);
|
||||
Nav.goForResult(getActivity(), FilterContextFragment.class, args, CONTEXT_RESULT, this);
|
||||
}
|
||||
|
||||
private void onDeleteClick(){
|
||||
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(getString(R.string.settings_delete_filter_title, filter.title))
|
||||
.setMessage(R.string.settings_delete_filter_confirmation)
|
||||
.setPositiveButton(R.string.delete, (dlg, item)->deleteFilter())
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
alert.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Error));
|
||||
}
|
||||
|
||||
private void updateDurationItem(){
|
||||
if(endsAt==null){
|
||||
durationItem.subtitle=getString(R.string.filter_duration_forever);
|
||||
}else{
|
||||
durationItem.subtitle=getString(R.string.settings_filter_ends, UiUtils.formatRelativeTimestampAsMinutesAgo(getActivity(), endsAt, false));
|
||||
}
|
||||
rebindItem(durationItem);
|
||||
}
|
||||
|
||||
private void updateWordsItem(){
|
||||
wordsItem.subtitle=getResources().getQuantityString(R.plurals.settings_x_muted_words, keywords.size(), keywords.size());
|
||||
rebindItem(wordsItem);
|
||||
}
|
||||
|
||||
private void updateContextItem(){
|
||||
List<String> values=context.stream().map(c->getString(c.getDisplayNameRes())).collect(Collectors.toList());
|
||||
contextItem.subtitle=switch(values.size()){
|
||||
case 0 -> null;
|
||||
case 1 -> values.get(0);
|
||||
case 2 -> getString(R.string.selection_2_options, values.get(0), values.get(1));
|
||||
case 3 -> getString(R.string.selection_3_options, values.get(0), values.get(1), values.get(2));
|
||||
default -> getString(R.string.selection_4_or_more, values.get(0), values.get(1), values.size()-2);
|
||||
};
|
||||
rebindItem(contextItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(R.menu.settings_edit_filter, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
if(item.getItemId()==R.id.save){
|
||||
saveFilter();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void saveFilter(){
|
||||
if(titleEdit.length()==0){
|
||||
titleEditLayout.setErrorState(getString(R.string.required_form_field_blank));
|
||||
return;
|
||||
}
|
||||
MastodonAPIRequest<Filter> req;
|
||||
if(filter==null){
|
||||
req=new CreateFilter(titleEdit.getText().toString(), context, cwItem.checked ? FilterAction.WARN : FilterAction.HIDE, endsAt==null ? 0 : (int)(endsAt.getEpochSecond()-Instant.now().getEpochSecond()), keywords);
|
||||
}else{
|
||||
req=new UpdateFilter(filter.id, titleEdit.getText().toString(), context, cwItem.checked ? FilterAction.WARN : FilterAction.HIDE, endsAt==null ? 0 : (int)(endsAt.getEpochSecond()-Instant.now().getEpochSecond()), keywords, deletedWordIDs);
|
||||
}
|
||||
req.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Filter result){
|
||||
E.post(new SettingsFilterCreatedOrUpdatedEvent(accountID, result));
|
||||
Nav.finish(EditFilterFragment.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.saving, true)
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void deleteFilter(){
|
||||
new DeleteFilter(filter.id)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Void result){
|
||||
E.post(new SettingsFilterDeletedEvent(accountID, filter.id));
|
||||
Nav.finish(EditFilterFragment.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.deleting, false)
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFragmentResult(int reqCode, boolean success, Bundle result){
|
||||
if(success){
|
||||
if(reqCode==CONTEXT_RESULT){
|
||||
EnumSet<FilterContext> context=(EnumSet<FilterContext>) result.getSerializable("context");
|
||||
if(!context.equals(this.context)){
|
||||
this.context=context;
|
||||
dirty=true;
|
||||
updateContextItem();
|
||||
}
|
||||
}else if(reqCode==WORDS_RESULT){
|
||||
ArrayList<FilterKeyword> old=new ArrayList<>(keywords);
|
||||
keywords.clear();
|
||||
result.getParcelableArrayList("words").stream().map(p->(FilterKeyword)Parcels.unwrap(p)).forEach(keywords::add);
|
||||
if(!old.equals(keywords)){
|
||||
dirty=true;
|
||||
updateWordsItem();
|
||||
}
|
||||
deletedWordIDs.addAll(result.getStringArrayList("deleted"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isDirty(){
|
||||
return dirty || (filter!=null && !titleEdit.getText().toString().equals(filter.title));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
if(isDirty()){
|
||||
UiUtils.showConfirmationAlert(getActivity(), R.string.discard_changes, 0, R.string.discard, ()->Nav.finish(this));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
|
||||
public class FilterContextFragment extends BaseSettingsFragment<FilterContext> implements OnBackPressedListener{
|
||||
private EnumSet<FilterContext> context;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.settings_filter_context);
|
||||
context=(EnumSet<FilterContext>) getArguments().getSerializable("context");
|
||||
onDataLoaded(Arrays.stream(FilterContext.values()).map(c->{
|
||||
CheckableListItem<FilterContext> item=new CheckableListItem<>(c.getDisplayNameRes(), 0, CheckableListItem.Style.CHECKBOX, context.contains(c), null);
|
||||
item.parentObject=c;
|
||||
item.isEnabled=true;
|
||||
item.onClick=()->toggleCheckableItem(item);
|
||||
return item;
|
||||
}).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
context=EnumSet.noneOf(FilterContext.class);
|
||||
for(ListItem<FilterContext> item:data){
|
||||
if(((CheckableListItem<FilterContext>) item).checked)
|
||||
context.add(item.parentObject);
|
||||
}
|
||||
Bundle args=new Bundle();
|
||||
args.putSerializable("context", context);
|
||||
setResult(true, args);
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,327 @@
|
|||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.IntEvaluator;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.app.AlertDialog;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.text.InputType;
|
||||
import android.view.ActionMode;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.FilterKeyword;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> implements OnBackPressedListener{
|
||||
private ImageButton fab;
|
||||
private ActionMode actionMode;
|
||||
private ArrayList<ListItem<FilterKeyword>> selectedItems=new ArrayList<>();
|
||||
private ArrayList<String> deletedItemIDs=new ArrayList<>();
|
||||
private MenuItem deleteItem;
|
||||
|
||||
public FilterWordsFragment(){
|
||||
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.settings_filter_muted_words);
|
||||
onDataLoaded(getArguments().getParcelableArrayList("words").stream().map(p->{
|
||||
FilterKeyword word=Parcels.unwrap(p);
|
||||
ListItem<FilterKeyword> item=new ListItem<>(word.keyword, null, null, word);
|
||||
item.isEnabled=true;
|
||||
item.onClick=()->onWordClick(item);
|
||||
return item;
|
||||
}).collect(Collectors.toList()));
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
private void onWordClick(ListItem<FilterKeyword> item){
|
||||
showAlertForWord(item.parentObject);
|
||||
}
|
||||
|
||||
private void onSelectionModeWordClick(CheckableListItem<FilterKeyword> item){
|
||||
if(selectedItems.remove(item)){
|
||||
item.checked=false;
|
||||
}else{
|
||||
item.checked=true;
|
||||
selectedItems.add(item);
|
||||
}
|
||||
rebindItem(item);
|
||||
updateActionModeTitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
Bundle result=new Bundle();
|
||||
result.putParcelableArrayList("words", (ArrayList<? extends Parcelable>) data.stream().map(i->i.parentObject).map(Parcels::wrap).collect(Collectors.toCollection(ArrayList::new)));
|
||||
result.putStringArrayList("deleted", deletedItemIDs);
|
||||
setResult(true, result);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
fab=view.findViewById(R.id.fab);
|
||||
fab.setImageResource(R.drawable.ic_add_24px);
|
||||
fab.setContentDescription(getString(R.string.add_muted_word));
|
||||
fab.setOnClickListener(v->onFabClick());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
int fabInset=0;
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
|
||||
fabInset=insets.getSystemWindowInsetBottom();
|
||||
}
|
||||
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(16)+fabInset;
|
||||
super.onApplyWindowInsets(insets);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(R.menu.settings_filter_words, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
enterSelectionMode(item.getItemId()==R.id.select_all);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wantsLightStatusBar(){
|
||||
if(actionMode!=null)
|
||||
return UiUtils.isDarkTheme();
|
||||
return super.wantsLightStatusBar();
|
||||
}
|
||||
|
||||
private void onFabClick(){
|
||||
showAlertForWord(null);
|
||||
}
|
||||
|
||||
private void showAlertForWord(FilterKeyword word){
|
||||
AlertDialog.Builder bldr=new M3AlertDialogBuilder(getActivity())
|
||||
.setHelpText(R.string.filter_add_word_help)
|
||||
.setTitle(word==null ? R.string.add_muted_word : R.string.edit_muted_word)
|
||||
.setNegativeButton(R.string.cancel, null);
|
||||
|
||||
FloatingHintEditTextLayout editWrap=(FloatingHintEditTextLayout) bldr.getContext().getSystemService(LayoutInflater.class).inflate(R.layout.floating_hint_edit_text, null);
|
||||
EditText edit=editWrap.findViewById(R.id.edit);
|
||||
edit.setHint(R.string.filter_word_or_phrase);
|
||||
edit.setInputType(InputType.TYPE_TEXT_VARIATION_FILTER);
|
||||
editWrap.updateHint();
|
||||
bldr.setView(editWrap)
|
||||
.setPositiveButton(word==null ? R.string.add : R.string.save, null);
|
||||
|
||||
if(word!=null){
|
||||
edit.setText(word.keyword);
|
||||
bldr.setNeutralButton(R.string.delete, null);
|
||||
}
|
||||
AlertDialog alert=bldr.show();
|
||||
if(word!=null){
|
||||
Button deleteBtn=alert.getButton(AlertDialog.BUTTON_NEUTRAL);
|
||||
deleteBtn.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Error));
|
||||
deleteBtn.setOnClickListener(v->confirmDeleteWords(Collections.singletonList(word), alert::dismiss));
|
||||
}
|
||||
Button saveBtn=alert.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
saveBtn.setEnabled(false);
|
||||
saveBtn.setOnClickListener(v->{
|
||||
String input=edit.getText().toString();
|
||||
for(ListItem<FilterKeyword> item:data){
|
||||
if(item.parentObject.keyword.equalsIgnoreCase(input)){
|
||||
editWrap.setErrorState(getString(R.string.filter_word_already_in_list));
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(word==null){
|
||||
FilterKeyword w=new FilterKeyword();
|
||||
w.wholeWord=true;
|
||||
w.keyword=input;
|
||||
ListItem<FilterKeyword> item=new ListItem<>(w.keyword, null, null, w);
|
||||
item.isEnabled=true;
|
||||
item.onClick=()->onWordClick(item);
|
||||
data.add(item);
|
||||
itemsAdapter.notifyItemInserted(data.size()-1);
|
||||
}else{
|
||||
word.keyword=input;
|
||||
word.wholeWord=true;
|
||||
for(ListItem<FilterKeyword> item:data){
|
||||
if(item.parentObject==word){
|
||||
rebindItem(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
alert.dismiss();
|
||||
});
|
||||
edit.addTextChangedListener(new SimpleTextWatcher(e->saveBtn.setEnabled(e.length()>0)));
|
||||
}
|
||||
|
||||
private void confirmDeleteWords(List<FilterKeyword> words, Runnable onConfirmed){
|
||||
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(words.size()==1 ? getString(R.string.settings_delete_filter_word, words.get(0).keyword) : getResources().getQuantityString(R.plurals.settings_delete_x_filter_words, words.size(), words.size()))
|
||||
// .setMessage(R.string.settings_delete_filter_confirmation)
|
||||
.setPositiveButton(R.string.delete, (dlg, item)->{
|
||||
if(onConfirmed!=null)
|
||||
onConfirmed.run();
|
||||
removeWords(words);
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
alert.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Error));
|
||||
}
|
||||
|
||||
private void removeWords(List<FilterKeyword> words){
|
||||
ArrayList<Integer> indexes=new ArrayList<>();
|
||||
for(int i=0;i<data.size();i++){
|
||||
if(words.contains(data.get(i).parentObject)){
|
||||
indexes.add(0, i);
|
||||
}
|
||||
}
|
||||
for(int index:indexes){
|
||||
data.remove(index);
|
||||
itemsAdapter.notifyItemRemoved(index);
|
||||
}
|
||||
for(FilterKeyword w:words){
|
||||
if(w.id!=null)
|
||||
deletedItemIDs.add(w.id);
|
||||
}
|
||||
}
|
||||
|
||||
private void enterSelectionMode(boolean selectAll){
|
||||
if(actionMode!=null)
|
||||
return;
|
||||
V.setVisibilityAnimated(fab, View.GONE);
|
||||
|
||||
actionMode=getActivity().startActionMode(new ActionMode.Callback(){
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu){
|
||||
ObjectAnimator anim=ObjectAnimator.ofInt(getActivity().getWindow(), "statusBarColor", elevationOnScrollListener.getCurrentStatusBarColor(), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
|
||||
anim.setEvaluator(new IntEvaluator(){
|
||||
@Override
|
||||
public Integer evaluate(float fraction, Integer startValue, Integer endValue){
|
||||
return UiUtils.alphaBlendColors(startValue, endValue, fraction);
|
||||
}
|
||||
});
|
||||
anim.start();
|
||||
((FragmentStackActivity) getActivity()).invalidateSystemBarColors(FilterWordsFragment.this);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu){
|
||||
mode.getMenuInflater().inflate(R.menu.settings_filter_words_action_mode, menu);
|
||||
for(int i=0;i<menu.size();i++){
|
||||
Drawable icon=menu.getItem(i).getIcon().mutate();
|
||||
icon.setTint(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnPrimary));
|
||||
menu.getItem(i).setIcon(icon);
|
||||
}
|
||||
deleteItem=menu.findItem(R.id.delete);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item){
|
||||
if(item.getItemId()==R.id.delete){
|
||||
confirmDeleteWords(selectedItems.stream().map(i->i.parentObject).collect(Collectors.toList()), ()->leaveSelectionMode(false));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode){
|
||||
leaveSelectionMode(true);
|
||||
ObjectAnimator anim=ObjectAnimator.ofInt(getActivity().getWindow(), "statusBarColor", UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary), elevationOnScrollListener.getCurrentStatusBarColor());
|
||||
anim.setEvaluator(new IntEvaluator(){
|
||||
@Override
|
||||
public Integer evaluate(float fraction, Integer startValue, Integer endValue){
|
||||
return UiUtils.alphaBlendColors(startValue, endValue, fraction);
|
||||
}
|
||||
});
|
||||
anim.addListener(new AnimatorListenerAdapter(){
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation){
|
||||
getActivity().getWindow().setStatusBarColor(0);
|
||||
}
|
||||
});
|
||||
anim.start();
|
||||
((FragmentStackActivity) getActivity()).invalidateSystemBarColors(FilterWordsFragment.this);
|
||||
}
|
||||
});
|
||||
|
||||
selectedItems.clear();
|
||||
for(int i=0;i<data.size();i++){
|
||||
ListItem<FilterKeyword> item=data.get(i);
|
||||
CheckableListItem<FilterKeyword> newItem=new CheckableListItem<>(item.title, null, CheckableListItem.Style.CHECKBOX, selectAll, null);
|
||||
newItem.isEnabled=true;
|
||||
newItem.onClick=()->onSelectionModeWordClick(newItem);
|
||||
newItem.parentObject=item.parentObject;
|
||||
if(selectAll)
|
||||
selectedItems.add(newItem);
|
||||
data.set(i, newItem);
|
||||
}
|
||||
itemsAdapter.notifyItemRangeChanged(0, data.size());
|
||||
updateActionModeTitle();
|
||||
}
|
||||
|
||||
private void leaveSelectionMode(boolean fromActionMode){
|
||||
if(actionMode==null)
|
||||
return;
|
||||
ActionMode actionMode=this.actionMode;
|
||||
this.actionMode=null;
|
||||
if(!fromActionMode)
|
||||
actionMode.finish();
|
||||
V.setVisibilityAnimated(fab, View.VISIBLE);
|
||||
selectedItems.clear();
|
||||
|
||||
for(int i=0;i<data.size();i++){
|
||||
ListItem<FilterKeyword> item=data.get(i);
|
||||
ListItem<FilterKeyword> newItem=new ListItem<>(item.title, null, null);
|
||||
newItem.isEnabled=true;
|
||||
newItem.onClick=()->onWordClick(newItem);
|
||||
newItem.parentObject=item.parentObject;
|
||||
data.set(i, newItem);
|
||||
}
|
||||
itemsAdapter.notifyItemRangeChanged(0, data.size());
|
||||
}
|
||||
|
||||
private void updateActionModeTitle(){
|
||||
actionMode.setTitle(getResources().getQuantityString(R.plurals.x_items_selected, selectedItems.size(), selectedItems.size()));
|
||||
deleteItem.setEnabled(!selectedItems.isEmpty());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.Gravity;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.imageloader.ImageCache;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class SettingsAboutAppFragment extends BaseSettingsFragment<Void>{
|
||||
private ListItem<Void> mediaCacheItem;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(getString(R.string.about_app, getString(R.string.app_name)));
|
||||
AccountSession s=AccountSessionManager.get(accountID);
|
||||
onDataLoaded(List.of(
|
||||
new ListItem<>(R.string.settings_even_more, 0, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/auth/edit")),
|
||||
new ListItem<>(R.string.settings_contribute, 0, ()->UiUtils.launchWebBrowser(getActivity(), getString(R.string.github_url))),
|
||||
new ListItem<>(R.string.settings_tos, 0, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/terms")),
|
||||
new ListItem<>(R.string.settings_privacy_policy, 0, ()->UiUtils.launchWebBrowser(getActivity(), getString(R.string.privacy_policy_url)), 0, true),
|
||||
mediaCacheItem=new ListItem<>(R.string.settings_clear_cache, 0, this::onClearMediaCacheClick)
|
||||
));
|
||||
|
||||
updateMediaCacheItem();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(super.getAdapter());
|
||||
|
||||
TextView versionInfo=new TextView(getActivity());
|
||||
versionInfo.setSingleLine();
|
||||
versionInfo.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(32)));
|
||||
versionInfo.setTextAppearance(R.style.m3_label_medium);
|
||||
versionInfo.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Outline));
|
||||
versionInfo.setGravity(Gravity.CENTER);
|
||||
versionInfo.setText(getString(R.string.settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(versionInfo));
|
||||
|
||||
return adapter;
|
||||
}
|
||||
|
||||
private void onClearMediaCacheClick(){
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
Activity activity=getActivity();
|
||||
ImageCache.getInstance(getActivity()).clear();
|
||||
activity.runOnUiThread(()->{
|
||||
Toast.makeText(activity, R.string.media_cache_cleared, Toast.LENGTH_SHORT).show();
|
||||
updateMediaCacheItem();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void updateMediaCacheItem(){
|
||||
long size=ImageCache.getInstance(getActivity()).getDiskCache().size();
|
||||
mediaCacheItem.subtitle=UiUtils.formatFileSize(getActivity(), size, false);
|
||||
mediaCacheItem.isEnabled=size>0;
|
||||
rebindItem(mediaCacheItem);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.viewcontrollers.ComposeLanguageAlertViewController;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class SettingsBehaviorFragment extends BaseSettingsFragment<Void>{
|
||||
private ListItem<Void> languageItem;
|
||||
private CheckableListItem<Void> altTextItem, playGifsItem, customTabsItem, confirmUnfollowItem, confirmBoostItem, confirmDeleteItem;
|
||||
private Locale postLanguage;
|
||||
private ComposeLanguageAlertViewController.SelectedOption newPostLanguage;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.settings_behavior);
|
||||
|
||||
AccountSession s=AccountSessionManager.get(accountID);
|
||||
if(s.preferences!=null && s.preferences.postingDefaultLanguage!=null){
|
||||
postLanguage=Locale.forLanguageTag(s.preferences.postingDefaultLanguage);
|
||||
}
|
||||
|
||||
onDataLoaded(List.of(
|
||||
languageItem=new ListItem<>(getString(R.string.default_post_language), postLanguage!=null ? postLanguage.getDisplayName(Locale.getDefault()) : null, R.drawable.ic_language_24px, this::onDefaultLanguageClick),
|
||||
altTextItem=new CheckableListItem<>(R.string.settings_alt_text_reminders, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.altTextReminders, R.drawable.ic_alt_24px, ()->toggleCheckableItem(altTextItem)),
|
||||
playGifsItem=new CheckableListItem<>(R.string.settings_gif, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.playGifs, R.drawable.ic_animation_24px, ()->toggleCheckableItem(playGifsItem)),
|
||||
customTabsItem=new CheckableListItem<>(R.string.settings_custom_tabs, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.useCustomTabs, R.drawable.ic_open_in_browser_24px, ()->toggleCheckableItem(customTabsItem)),
|
||||
confirmUnfollowItem=new CheckableListItem<>(R.string.settings_confirm_unfollow, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmUnfollow, R.drawable.ic_person_remove_24px, ()->toggleCheckableItem(confirmUnfollowItem)),
|
||||
confirmBoostItem=new CheckableListItem<>(R.string.settings_confirm_boost, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmBoost, R.drawable.ic_repeat_24px, ()->toggleCheckableItem(confirmBoostItem)),
|
||||
confirmDeleteItem=new CheckableListItem<>(R.string.settings_confirm_delete_post, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmDeletePost, R.drawable.ic_delete_24px, ()->toggleCheckableItem(confirmDeleteItem))
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
private void onDefaultLanguageClick(){
|
||||
ComposeLanguageAlertViewController vc=new ComposeLanguageAlertViewController(getActivity(), null, new ComposeLanguageAlertViewController.SelectedOption(-1, postLanguage), null);
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.default_post_language)
|
||||
.setView(vc.getView())
|
||||
.setPositiveButton(R.string.ok, (dlg, which)->{
|
||||
ComposeLanguageAlertViewController.SelectedOption opt=vc.getSelectedOption();
|
||||
if(!opt.locale.equals(postLanguage)){
|
||||
newPostLanguage=opt;
|
||||
languageItem.subtitle=newPostLanguage.locale.getDisplayLanguage(Locale.getDefault());
|
||||
rebindItem(languageItem);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHidden(){
|
||||
super.onHidden();
|
||||
GlobalUserPreferences.playGifs=playGifsItem.checked;
|
||||
GlobalUserPreferences.useCustomTabs=customTabsItem.checked;
|
||||
GlobalUserPreferences.altTextReminders=altTextItem.checked;
|
||||
GlobalUserPreferences.confirmUnfollow=customTabsItem.checked;
|
||||
GlobalUserPreferences.confirmBoost=confirmBoostItem.checked;
|
||||
GlobalUserPreferences.confirmDeletePost=confirmDeleteItem.checked;
|
||||
GlobalUserPreferences.save();
|
||||
if(newPostLanguage!=null){
|
||||
AccountSession s=AccountSessionManager.get(accountID);
|
||||
if(s.preferences==null)
|
||||
s.preferences=new Preferences();
|
||||
s.preferences.postingDefaultLanguage=newPostLanguage.locale.toLanguageTag();
|
||||
s.savePreferencesLater();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.api.session.AccountActivationInfo;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.HomeFragment;
|
||||
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
|
||||
public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle("Debug settings");
|
||||
ListItem<Void> selfUpdateItem, resetUpdateItem;
|
||||
onDataLoaded(List.of(
|
||||
new ListItem<>("Test email confirmation flow", null, this::onTestEmailConfirmClick),
|
||||
selfUpdateItem=new ListItem<>("Force self-update", null, this::onForceSelfUpdateClick),
|
||||
resetUpdateItem=new ListItem<>("Reset self-updater", null, this::onResetUpdaterClick)
|
||||
));
|
||||
if(!GithubSelfUpdater.needSelfUpdating()){
|
||||
resetUpdateItem.isEnabled=selfUpdateItem.isEnabled=false;
|
||||
selfUpdateItem.subtitle="Self-updater is unavailable in this build flavor";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
private void onTestEmailConfirmClick(){
|
||||
AccountSession sess=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
sess.activated=false;
|
||||
sess.activationInfo=new AccountActivationInfo("test@email", System.currentTimeMillis());
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putBoolean("debug", true);
|
||||
Nav.goClearingStack(getActivity(), AccountActivationFragment.class, args);
|
||||
}
|
||||
|
||||
private void onForceSelfUpdateClick(){
|
||||
GithubSelfUpdater.forceUpdate=true;
|
||||
GithubSelfUpdater.getInstance().maybeCheckForUpdates();
|
||||
restartUI();
|
||||
}
|
||||
|
||||
private void onResetUpdaterClick(){
|
||||
GithubSelfUpdater.getInstance().reset();
|
||||
restartUI();
|
||||
}
|
||||
|
||||
private void restartUI(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.goClearingStack(getActivity(), HomeFragment.class, args);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountLocalPreferences;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.StatusDisplaySettingsChangedEvent;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
|
||||
public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
|
||||
private ImageView themeTransitionWindowView;
|
||||
private ListItem<Void> themeItem;
|
||||
private CheckableListItem<Void> showCWsItem, hideSensitiveMediaItem, interactionCountsItem, emojiInNamesItem;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.settings_display);
|
||||
AccountSession s=AccountSessionManager.get(accountID);
|
||||
AccountLocalPreferences lp=s.getLocalPreferences();
|
||||
onDataLoaded(List.of(
|
||||
themeItem=new ListItem<>(R.string.settings_theme, getAppearanceValue(), R.drawable.ic_dark_mode_24px, this::onAppearanceClick),
|
||||
showCWsItem=new CheckableListItem<>(R.string.settings_show_cws, 0, CheckableListItem.Style.SWITCH, lp.showCWs, R.drawable.ic_warning_24px, ()->toggleCheckableItem(showCWsItem)),
|
||||
hideSensitiveMediaItem=new CheckableListItem<>(R.string.settings_hide_sensitive_media, 0, CheckableListItem.Style.SWITCH, lp.hideSensitiveMedia, R.drawable.ic_no_adult_content_24px, ()->toggleCheckableItem(hideSensitiveMediaItem)),
|
||||
interactionCountsItem=new CheckableListItem<>(R.string.settings_show_interaction_counts, 0, CheckableListItem.Style.SWITCH, lp.showInteractionCounts, R.drawable.ic_social_leaderboard_24px, ()->toggleCheckableItem(interactionCountsItem)),
|
||||
emojiInNamesItem=new CheckableListItem<>(R.string.settings_show_emoji_in_names, 0, CheckableListItem.Style.SWITCH, lp.customEmojiInNames, R.drawable.ic_emoticon_24px, ()->toggleCheckableItem(emojiInNamesItem))
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
if(themeTransitionWindowView!=null){
|
||||
// Activity has finished recreating. Remove the overlay.
|
||||
activity.getSystemService(WindowManager.class).removeView(themeTransitionWindowView);
|
||||
themeTransitionWindowView=null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHidden(){
|
||||
super.onHidden();
|
||||
AccountSession s=AccountSessionManager.get(accountID);
|
||||
AccountLocalPreferences lp=s.getLocalPreferences();
|
||||
lp.showCWs=showCWsItem.checked;
|
||||
lp.hideSensitiveMedia=hideSensitiveMediaItem.checked;
|
||||
lp.showInteractionCounts=interactionCountsItem.checked;
|
||||
lp.customEmojiInNames=emojiInNamesItem.checked;
|
||||
lp.save();
|
||||
E.post(new StatusDisplaySettingsChangedEvent(accountID));
|
||||
}
|
||||
|
||||
private int getAppearanceValue(){
|
||||
return switch(GlobalUserPreferences.theme){
|
||||
case AUTO -> R.string.theme_auto;
|
||||
case LIGHT -> R.string.theme_light;
|
||||
case DARK -> R.string.theme_dark;
|
||||
};
|
||||
}
|
||||
|
||||
private void onAppearanceClick(){
|
||||
int selected=switch(GlobalUserPreferences.theme){
|
||||
case LIGHT -> 0;
|
||||
case DARK -> 1;
|
||||
case AUTO -> 2;
|
||||
};
|
||||
int[] newSelected={selected};
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.settings_theme)
|
||||
.setSingleChoiceItems((String[])IntStream.of(R.string.theme_light, R.string.theme_dark, R.string.theme_auto).mapToObj(this::getString).toArray(String[]::new),
|
||||
selected, (dlg, item)->newSelected[0]=item)
|
||||
.setPositiveButton(R.string.ok, (dlg, item)->{
|
||||
GlobalUserPreferences.ThemePreference pref=switch(newSelected[0]){
|
||||
case 0 -> GlobalUserPreferences.ThemePreference.LIGHT;
|
||||
case 1 -> GlobalUserPreferences.ThemePreference.DARK;
|
||||
case 2 -> GlobalUserPreferences.ThemePreference.AUTO;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+newSelected[0]);
|
||||
};
|
||||
if(pref!=GlobalUserPreferences.theme){
|
||||
GlobalUserPreferences.ThemePreference prev=GlobalUserPreferences.theme;
|
||||
GlobalUserPreferences.theme=pref;
|
||||
GlobalUserPreferences.save();
|
||||
themeItem.subtitleRes=getAppearanceValue();
|
||||
rebindItem(themeItem);
|
||||
maybeApplyNewThemeRightNow(prev);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void maybeApplyNewThemeRightNow(GlobalUserPreferences.ThemePreference prev){
|
||||
boolean isCurrentDark=prev==GlobalUserPreferences.ThemePreference.DARK ||
|
||||
(prev==GlobalUserPreferences.ThemePreference.AUTO && Build.VERSION.SDK_INT>=30 && getResources().getConfiguration().isNightModeActive());
|
||||
boolean isNewDark=GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.DARK ||
|
||||
(GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.AUTO && Build.VERSION.SDK_INT>=30 && getResources().getConfiguration().isNightModeActive());
|
||||
if(isCurrentDark!=isNewDark){
|
||||
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 && Build.VERSION.SDK_INT<Build.VERSION_CODES.S){
|
||||
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_LAYOUT_IN_SCREEN | 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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.filters.GetFilters;
|
||||
import org.joinmastodon.android.events.SettingsFilterCreatedOrUpdatedEvent;
|
||||
import org.joinmastodon.android.events.SettingsFilterDeletedEvent;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
|
||||
public class SettingsFiltersFragment extends BaseSettingsFragment<Filter>{
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.settings_filters);
|
||||
loadData();
|
||||
E.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
E.unregister(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
new GetFilters()
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Filter> result){
|
||||
onDataLoaded(result.stream().map(f->makeListItem(f)).collect(Collectors.toList()));
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(super.getAdapter());
|
||||
adapter.addAdapter(new GenericListItemsAdapter<>(Collections.singletonList(
|
||||
new ListItem<Void>(R.string.settings_add_filter, 0, R.drawable.ic_add_24px, this::onAddFilterClick)
|
||||
)));
|
||||
return adapter;
|
||||
}
|
||||
|
||||
private void onFilterClick(ListItem<Filter> filter){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("filter", Parcels.wrap(filter.parentObject));
|
||||
Nav.go(getActivity(), EditFilterFragment.class, args);
|
||||
}
|
||||
|
||||
private void onAddFilterClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), EditFilterFragment.class, args);
|
||||
}
|
||||
|
||||
private ListItem<Filter> makeListItem(Filter f){
|
||||
ListItem<Filter> item=new ListItem<>(f.title, getString(f.isActive() ? R.string.filter_active : R.string.filter_inactive), null, f);
|
||||
item.onClick=()->onFilterClick(item);
|
||||
item.isEnabled=true;
|
||||
return item;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onFilterDeleted(SettingsFilterDeletedEvent ev){
|
||||
if(!ev.accountID.equals(accountID))
|
||||
return;
|
||||
for(int i=0;i<data.size();i++){
|
||||
if(data.get(i).parentObject.id.equals(ev.filterID)){
|
||||
data.remove(i);
|
||||
itemsAdapter.notifyItemRemoved(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onFilterCreatedOrUpdated(SettingsFilterCreatedOrUpdatedEvent ev){
|
||||
if(!ev.accountID.equals(accountID))
|
||||
return;
|
||||
for(ListItem<Filter> item:data){
|
||||
if(item.parentObject.id.equals(ev.filter.id)){
|
||||
item.parentObject=ev.filter;
|
||||
item.title=ev.filter.title;
|
||||
item.subtitle=getString(ev.filter.isActive() ? R.string.filter_active : R.string.filter_inactive);
|
||||
rebindItem(item);
|
||||
return;
|
||||
}
|
||||
}
|
||||
data.add(makeListItem(ev.filter));
|
||||
itemsAdapter.notifyItemInserted(data.size()-1);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,213 @@
|
|||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
|
||||
public class SettingsMainFragment extends BaseSettingsFragment<Void>{
|
||||
private boolean loggedOut;
|
||||
private HideableSingleViewRecyclerAdapter bannerAdapter;
|
||||
private Button updateButton1, updateButton2;
|
||||
private TextView updateText;
|
||||
private Runnable updateDownloadProgressUpdater=new Runnable(){
|
||||
@Override
|
||||
public void run(){
|
||||
GithubSelfUpdater.UpdateState state=GithubSelfUpdater.getInstance().getState();
|
||||
if(state==GithubSelfUpdater.UpdateState.DOWNLOADING){
|
||||
updateButton1.setText(getString(R.string.downloading_update, Math.round(GithubSelfUpdater.getInstance().getDownloadProgress()*100f)));
|
||||
list.postDelayed(this, 250);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.settings);
|
||||
setSubtitle(AccountSessionManager.get(accountID).getFullUsername());
|
||||
onDataLoaded(List.of(
|
||||
new ListItem<>(R.string.settings_behavior, 0, R.drawable.ic_settings_24px, this::onBehaviorClick),
|
||||
new ListItem<>(R.string.settings_display, 0, R.drawable.ic_style_24px, this::onDisplayClick),
|
||||
new ListItem<>(R.string.settings_filters, 0, R.drawable.ic_filter_alt_24px, this::onFiltersClick),
|
||||
new ListItem<>(R.string.settings_notifications, 0, R.drawable.ic_notifications_24px, this::onNotificationsClick),
|
||||
new ListItem<>(R.string.settings_privacy, 0, R.drawable.ic_lock_24px, this::onPrivacyClick, 0, true),
|
||||
new ListItem<>(AccountSessionManager.get(accountID).domain, getString(R.string.settings_server_explanation), R.drawable.ic_dns_24px, this::onServerClick),
|
||||
new ListItem<>(getString(R.string.about_app, getString(R.string.app_name)), null, R.drawable.ic_info_24px, this::onAboutClick, null, 0, true),
|
||||
new ListItem<>(R.string.log_out, 0, R.drawable.ic_logout_24px, this::onLogOutClick, R.attr.colorM3Error, false)
|
||||
));
|
||||
|
||||
if(BuildConfig.DEBUG || BuildConfig.BUILD_TYPE.equals("appcenterPrivateBeta")){
|
||||
data.add(0, new ListItem<>("Debug settings", null, R.drawable.ic_settings_24px, ()->Nav.go(getActivity(), SettingsDebugFragment.class, makeFragmentArgs()), null, 0, true));
|
||||
}
|
||||
|
||||
AccountSessionManager.get(accountID).reloadPreferences(null);
|
||||
E.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
E.unregister(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
UiUtils.setToolbarWithSubtitleAppearance(getToolbar());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHidden(){
|
||||
super.onHidden();
|
||||
if(!loggedOut)
|
||||
AccountSessionManager.get(accountID).savePreferencesIfPending();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
View banner=getActivity().getLayoutInflater().inflate(R.layout.item_settings_banner, list, false);
|
||||
updateText=banner.findViewById(R.id.text);
|
||||
TextView bannerTitle=banner.findViewById(R.id.title);
|
||||
ImageView bannerIcon=banner.findViewById(R.id.icon);
|
||||
updateButton1=banner.findViewById(R.id.button);
|
||||
updateButton2=banner.findViewById(R.id.button2);
|
||||
bannerAdapter=new HideableSingleViewRecyclerAdapter(banner);
|
||||
bannerAdapter.setVisible(false);
|
||||
updateButton1.setOnClickListener(this::onUpdateButtonClick);
|
||||
updateButton2.setOnClickListener(this::onUpdateButtonClick);
|
||||
|
||||
bannerTitle.setText(R.string.app_update_ready);
|
||||
bannerIcon.setImageResource(R.drawable.ic_apk_install_24px);
|
||||
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(bannerAdapter);
|
||||
adapter.addAdapter(super.getAdapter());
|
||||
return adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
if(GithubSelfUpdater.needSelfUpdating()){
|
||||
updateUpdateBanner();
|
||||
}
|
||||
}
|
||||
|
||||
private Bundle makeFragmentArgs(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
return args;
|
||||
}
|
||||
|
||||
private void onBehaviorClick(){
|
||||
Nav.go(getActivity(), SettingsBehaviorFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onDisplayClick(){
|
||||
Nav.go(getActivity(), SettingsDisplayFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onFiltersClick(){
|
||||
Nav.go(getActivity(), SettingsFiltersFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onNotificationsClick(){
|
||||
Nav.go(getActivity(), SettingsNotificationsFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onPrivacyClick(){
|
||||
|
||||
}
|
||||
|
||||
private void onServerClick(){
|
||||
Nav.go(getActivity(), SettingsServerFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onAboutClick(){
|
||||
Nav.go(getActivity(), SettingsAboutAppFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onLogOutClick(){
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setMessage(getString(R.string.confirm_log_out, session.getFullUsername()))
|
||||
.setPositiveButton(R.string.log_out, (dialog, which)->AccountSessionManager.get(accountID).logOut(getActivity(), ()->{
|
||||
loggedOut=true;
|
||||
getActivity().finish();
|
||||
Intent intent=new Intent(getActivity(), MainActivity.class);
|
||||
startActivity(intent);
|
||||
}))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
|
||||
updateUpdateBanner();
|
||||
}
|
||||
|
||||
private void updateUpdateBanner(){
|
||||
GithubSelfUpdater.UpdateState state=GithubSelfUpdater.getInstance().getState();
|
||||
if(state==GithubSelfUpdater.UpdateState.NO_UPDATE || state==GithubSelfUpdater.UpdateState.CHECKING){
|
||||
bannerAdapter.setVisible(false);
|
||||
}else{
|
||||
bannerAdapter.setVisible(true);
|
||||
updateText.setText(getString(R.string.app_update_version, GithubSelfUpdater.getInstance().getUpdateInfo().version));
|
||||
if(state==GithubSelfUpdater.UpdateState.UPDATE_AVAILABLE){
|
||||
updateButton2.setVisibility(View.GONE);
|
||||
updateButton1.setEnabled(true);
|
||||
updateButton1.setText(getString(R.string.download_update, UiUtils.formatFileSize(getActivity(), GithubSelfUpdater.getInstance().getUpdateInfo().size, true)));
|
||||
}else if(state==GithubSelfUpdater.UpdateState.DOWNLOADING){
|
||||
updateButton2.setVisibility(View.VISIBLE);
|
||||
updateButton2.setText(R.string.cancel);
|
||||
updateButton1.setEnabled(false);
|
||||
list.removeCallbacks(updateDownloadProgressUpdater);
|
||||
updateDownloadProgressUpdater.run();
|
||||
}else if(state==GithubSelfUpdater.UpdateState.DOWNLOADED){
|
||||
updateButton2.setVisibility(View.GONE);
|
||||
updateButton1.setEnabled(true);
|
||||
updateButton1.setText(R.string.install_update);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onUpdateButtonClick(View v){
|
||||
if(v.getId()==R.id.button){
|
||||
GithubSelfUpdater.UpdateState state=GithubSelfUpdater.getInstance().getState();
|
||||
if(state==GithubSelfUpdater.UpdateState.UPDATE_AVAILABLE){
|
||||
GithubSelfUpdater.getInstance().downloadUpdate();
|
||||
}else if(state==GithubSelfUpdater.UpdateState.DOWNLOADED){
|
||||
GithubSelfUpdater.getInstance().installUpdate(getActivity());
|
||||
}
|
||||
}else if(v.getId()==R.id.button2){
|
||||
GithubSelfUpdater.getInstance().cancelDownload();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,286 @@
|
|||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.PushSubscription;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
|
||||
public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
|
||||
private PushSubscription pushSubscription;
|
||||
private CheckableListItem<Void> pauseItem;
|
||||
private ListItem<Void> policyItem;
|
||||
private MergeRecyclerAdapter mergeAdapter;
|
||||
|
||||
private HideableSingleViewRecyclerAdapter bannerAdapter;
|
||||
private ImageView bannerIcon;
|
||||
private TextView bannerText;
|
||||
private Button bannerButton;
|
||||
|
||||
private CheckableListItem<Void> mentionsItem, boostsItem, favoritesItem, followersItem, pollsItem;
|
||||
private List<CheckableListItem<Void>> typeItems;
|
||||
private boolean needUpdateNotificationSettings;
|
||||
private boolean notificationsAllowed=true;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.settings_notifications);
|
||||
|
||||
getPushSubscription();
|
||||
|
||||
onDataLoaded(List.of(
|
||||
pauseItem=new CheckableListItem<>(getString(R.string.pause_all_notifications), getPauseItemSubtitle(), CheckableListItem.Style.SWITCH, false, R.drawable.ic_notifications_paused_24px, ()->onPauseNotificationsClick(false)),
|
||||
policyItem=new ListItem<>(R.string.settings_notifications_policy, 0, R.drawable.ic_group_24px, this::onNotificationsPolicyClick),
|
||||
|
||||
mentionsItem=new CheckableListItem<>(R.string.notification_type_mentions_and_replies, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.mention, ()->toggleCheckableItem(mentionsItem)),
|
||||
boostsItem=new CheckableListItem<>(R.string.notification_type_reblog, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.reblog, ()->toggleCheckableItem(boostsItem)),
|
||||
favoritesItem=new CheckableListItem<>(R.string.notification_type_favorite, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.favourite, ()->toggleCheckableItem(favoritesItem)),
|
||||
followersItem=new CheckableListItem<>(R.string.notification_type_follow, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.follow, ()->toggleCheckableItem(followersItem)),
|
||||
pollsItem=new CheckableListItem<>(R.string.notification_type_poll, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.poll, ()->toggleCheckableItem(pollsItem))
|
||||
));
|
||||
|
||||
typeItems=List.of(mentionsItem, boostsItem, favoritesItem, followersItem, pollsItem);
|
||||
pauseItem.checkedChangeListener=checked->onPauseNotificationsClick(true);
|
||||
updatePolicyItem(null);
|
||||
updatePauseItem();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
protected void onHidden(){
|
||||
super.onHidden();
|
||||
PushSubscription ps=getPushSubscription();
|
||||
needUpdateNotificationSettings|=mentionsItem.checked!=ps.alerts.mention
|
||||
|| boostsItem.checked!=ps.alerts.reblog
|
||||
|| favoritesItem.checked!=ps.alerts.favourite
|
||||
|| followersItem.checked!=ps.alerts.follow
|
||||
|| pollsItem.checked!=ps.alerts.poll;
|
||||
if(needUpdateNotificationSettings && PushSubscriptionManager.arePushNotificationsAvailable()){
|
||||
ps.alerts.mention=mentionsItem.checked;
|
||||
ps.alerts.reblog=boostsItem.checked;
|
||||
ps.alerts.favourite=favoritesItem.checked;
|
||||
ps.alerts.follow=followersItem.checked;
|
||||
ps.alerts.poll=pollsItem.checked;
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().updatePushSettings(pushSubscription);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
boolean allowed=areNotificationsAllowed();
|
||||
PushSubscription ps=getPushSubscription();
|
||||
if(allowed!=notificationsAllowed){
|
||||
notificationsAllowed=allowed;
|
||||
updateBanner();
|
||||
pauseItem.isEnabled=allowed;
|
||||
policyItem.isEnabled=allowed;
|
||||
rebindItem(pauseItem);
|
||||
rebindItem(policyItem);
|
||||
for(CheckableListItem<Void> item:typeItems){
|
||||
item.isEnabled=allowed && ps.policy!=PushSubscription.Policy.NONE;
|
||||
rebindItem(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
View banner=getActivity().getLayoutInflater().inflate(R.layout.item_settings_banner, list, false);
|
||||
bannerText=banner.findViewById(R.id.text);
|
||||
bannerIcon=banner.findViewById(R.id.icon);
|
||||
bannerButton=banner.findViewById(R.id.button);
|
||||
bannerAdapter=new HideableSingleViewRecyclerAdapter(banner);
|
||||
bannerAdapter.setVisible(false);
|
||||
banner.findViewById(R.id.button2).setVisibility(View.GONE);
|
||||
banner.findViewById(R.id.title).setVisibility(View.GONE);
|
||||
|
||||
mergeAdapter=new MergeRecyclerAdapter();
|
||||
mergeAdapter.addAdapter(bannerAdapter);
|
||||
mergeAdapter.addAdapter(super.getAdapter());
|
||||
return mergeAdapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int indexOfItemsAdapter(){
|
||||
return mergeAdapter.getPositionForAdapter(itemsAdapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
updateBanner();
|
||||
}
|
||||
|
||||
private boolean areNotificationsAllowed(){
|
||||
return Build.VERSION.SDK_INT<Build.VERSION_CODES.N || getActivity().getSystemService(NotificationManager.class).areNotificationsEnabled();
|
||||
}
|
||||
|
||||
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 String getPauseItemSubtitle(){
|
||||
return getString(R.string.pause_notifications_off);
|
||||
}
|
||||
|
||||
private void resumePausedNotifications(){
|
||||
AccountSessionManager.get(accountID).getLocalPreferences().setNotificationsPauseEndTime(0);
|
||||
updatePauseItem();
|
||||
}
|
||||
|
||||
private void openSystemNotificationSettings(){
|
||||
Intent intent;
|
||||
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.O){
|
||||
intent=new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", getActivity().getPackageName(), null));
|
||||
}else{
|
||||
intent=new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getActivity().getPackageName());
|
||||
}
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void onPauseNotificationsClick(boolean fromSwitch){
|
||||
long time=AccountSessionManager.get(accountID).getLocalPreferences().getNotificationsPauseEndTime();
|
||||
if(time>System.currentTimeMillis() && fromSwitch){
|
||||
resumePausedNotifications();
|
||||
return;
|
||||
}
|
||||
int[] durationOptions={
|
||||
1800,
|
||||
3600,
|
||||
6*3600,
|
||||
12*3600,
|
||||
24*3600,
|
||||
3*24*3600,
|
||||
7*24*3600
|
||||
};
|
||||
int[] selectedOption={0};
|
||||
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.pause_all_notifications_title)
|
||||
.setSupportingText(time>System.currentTimeMillis() ? getString(R.string.pause_notifications_ends, UiUtils.formatRelativeTimestampAsMinutesAgo(getActivity(), Instant.ofEpochMilli(time), false)) : null)
|
||||
.setSingleChoiceItems((String[])Arrays.stream(durationOptions).mapToObj(d->UiUtils.formatDuration(getActivity(), d)).toArray(String[]::new), -1, (dlg, item)->{
|
||||
if(selectedOption[0]==0){
|
||||
((AlertDialog)dlg).getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
|
||||
}
|
||||
selectedOption[0]=durationOptions[item];
|
||||
})
|
||||
.setPositiveButton(R.string.ok, (dlg, item)->AccountSessionManager.get(accountID).getLocalPreferences().setNotificationsPauseEndTime(System.currentTimeMillis()+selectedOption[0]*1000L))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
alert.setOnDismissListener(dialog->updatePauseItem());
|
||||
alert.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
|
||||
}
|
||||
|
||||
private void onNotificationsPolicyClick(){
|
||||
String[] items=Stream.of(
|
||||
R.string.notifications_policy_anyone,
|
||||
R.string.notifications_policy_followed,
|
||||
R.string.notifications_policy_follower,
|
||||
R.string.notifications_policy_no_one
|
||||
).map(this::getString).toArray(String[]::new);
|
||||
int[] selectedItem={getPushSubscription().policy.ordinal()};
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.settings_notifications_policy)
|
||||
.setSingleChoiceItems(items, selectedItem[0], (dlg, which)->selectedItem[0]=which)
|
||||
.setPositiveButton(R.string.ok, (dlg, which)->{
|
||||
PushSubscription.Policy prevValue=getPushSubscription().policy;
|
||||
PushSubscription.Policy newValue=PushSubscription.Policy.values()[selectedItem[0]];
|
||||
if(prevValue==newValue)
|
||||
return;
|
||||
getPushSubscription().policy=newValue;
|
||||
updatePolicyItem(prevValue);
|
||||
needUpdateNotificationSettings=true;
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void updatePolicyItem(PushSubscription.Policy prevValue){
|
||||
policyItem.subtitleRes=switch(getPushSubscription().policy){
|
||||
case ALL -> R.string.notifications_policy_anyone;
|
||||
case FOLLOWED -> R.string.notifications_policy_followed;
|
||||
case FOLLOWER -> R.string.notifications_policy_follower;
|
||||
case NONE -> R.string.notifications_policy_no_one;
|
||||
};
|
||||
rebindItem(policyItem);
|
||||
if(pushSubscription.policy==PushSubscription.Policy.NONE || prevValue==PushSubscription.Policy.NONE){
|
||||
for(CheckableListItem<Void> item:typeItems){
|
||||
item.checked=item.isEnabled=prevValue==PushSubscription.Policy.NONE;
|
||||
rebindItem(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePauseItem(){
|
||||
long time=AccountSessionManager.get(accountID).getLocalPreferences().getNotificationsPauseEndTime();
|
||||
if(time<System.currentTimeMillis()){
|
||||
pauseItem.subtitle=getString(R.string.pause_notifications_off);
|
||||
pauseItem.checked=false;
|
||||
}else{
|
||||
pauseItem.subtitle=getString(R.string.pause_notifications_ends, UiUtils.formatRelativeTimestampAsMinutesAgo(getActivity(), Instant.ofEpochMilli(time), false));
|
||||
pauseItem.checked=true;
|
||||
}
|
||||
rebindItem(pauseItem);
|
||||
updateBanner();
|
||||
}
|
||||
|
||||
private void updateBanner(){
|
||||
if(bannerAdapter==null)
|
||||
return;
|
||||
long pauseTime=AccountSessionManager.get(accountID).getLocalPreferences().getNotificationsPauseEndTime();
|
||||
if(!areNotificationsAllowed()){
|
||||
bannerAdapter.setVisible(true);
|
||||
bannerIcon.setImageResource(R.drawable.ic_app_badging_24px);
|
||||
bannerText.setText(R.string.notifications_disabled_in_system);
|
||||
bannerButton.setText(R.string.open_system_notification_settings);
|
||||
bannerButton.setOnClickListener(v->openSystemNotificationSettings());
|
||||
}else if(pauseTime>System.currentTimeMillis()){
|
||||
bannerAdapter.setVisible(true);
|
||||
bannerIcon.setImageResource(R.drawable.ic_notifications_paused_24px);
|
||||
bannerText.setText(getString(R.string.pause_notifications_banner, UiUtils.formatRelativeTimestampAsMinutesAgo(getActivity(), Instant.ofEpochMilli(pauseTime), false)));
|
||||
bannerButton.setText(R.string.resume_notifications_now);
|
||||
bannerButton.setOnClickListener(v->resumePausedNotifications());
|
||||
}else{
|
||||
bannerAdapter.setVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,238 @@
|
|||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.requests.instance.GetInstanceExtendedDescription;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.SimpleListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.views.FixedAspectRatioImageView;
|
||||
import org.joinmastodon.android.utils.ViewImageLoaderHolderTarget;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.fragments.LoaderFragment;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class SettingsServerAboutFragment extends LoaderFragment{
|
||||
private String accountID;
|
||||
private Instance instance;
|
||||
|
||||
private WebView webView;
|
||||
private LinearLayout scrollingLayout;
|
||||
public ScrollView scroller;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
accountID=getArguments().getString("account");
|
||||
instance=AccountSessionManager.getInstance().getInstanceInfo(AccountSessionManager.get(accountID).domain);
|
||||
loadData();
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@Override
|
||||
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||
webView=new WebView(getActivity());
|
||||
webView.getSettings().setJavaScriptEnabled(true);
|
||||
webView.setWebViewClient(new WebViewClient(){
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url){
|
||||
dataLoaded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url){
|
||||
Uri uri=Uri.parse(url);
|
||||
if(uri.getScheme().equals("http") || uri.getScheme().equals("https")){
|
||||
UiUtils.launchWebBrowser(getActivity(), url);
|
||||
}else{
|
||||
Intent intent=new Intent(Intent.ACTION_VIEW, uri);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
try{
|
||||
startActivity(intent);
|
||||
}catch(ActivityNotFoundException x){
|
||||
Toast.makeText(getActivity(), R.string.no_app_to_handle_action, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
scrollingLayout=new LinearLayout(getActivity());
|
||||
scrollingLayout.setOrientation(LinearLayout.VERTICAL);
|
||||
scroller=new ScrollView(getActivity());
|
||||
scroller.setNestedScrollingEnabled(true);
|
||||
scroller.setClipToPadding(false);
|
||||
scroller.addView(scrollingLayout);
|
||||
|
||||
FixedAspectRatioImageView banner=new FixedAspectRatioImageView(getActivity());
|
||||
banner.setAspectRatio(1.914893617f);
|
||||
banner.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
||||
banner.setOutlineProvider(OutlineProviders.bottomRoundedRect(16));
|
||||
banner.setClipToOutline(true);
|
||||
ViewImageLoader.loadWithoutAnimation(banner, getResources().getDrawable(R.drawable.image_placeholder, getActivity().getTheme()), new UrlImageLoaderRequest(instance.thumbnail));
|
||||
LinearLayout.LayoutParams blp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
blp.bottomMargin=V.dp(24);
|
||||
scrollingLayout.addView(banner, blp);
|
||||
|
||||
boolean needDivider=false;
|
||||
if(instance.contactAccount!=null){
|
||||
needDivider=true;
|
||||
TextView heading=new TextView(getActivity());
|
||||
heading.setTextAppearance(R.style.m3_title_small);
|
||||
heading.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant));
|
||||
heading.setSingleLine();
|
||||
heading.setText(R.string.server_administrator);
|
||||
heading.setGravity(Gravity.CENTER_VERTICAL);
|
||||
LinearLayout.LayoutParams hlp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(20));
|
||||
hlp.bottomMargin=V.dp(4);
|
||||
hlp.leftMargin=hlp.rightMargin=V.dp(16);
|
||||
scrollingLayout.addView(heading, hlp);
|
||||
|
||||
AccountViewModel model=new AccountViewModel(instance.contactAccount, accountID);
|
||||
AccountViewHolder holder=new AccountViewHolder(this, scrollingLayout, null);
|
||||
holder.bind(model);
|
||||
holder.itemView.setBackground(UiUtils.getThemeDrawable(getActivity(), android.R.attr.selectableItemBackground));
|
||||
holder.itemView.setOnClickListener(v->holder.onClick());
|
||||
scrollingLayout.addView(holder.itemView);
|
||||
ViewImageLoader.load(new ViewImageLoaderHolderTarget(holder, 0), null, model.avaRequest, false);
|
||||
for(int i=0;i<model.emojiHelper.getImageCount();i++){
|
||||
ViewImageLoader.load(new ViewImageLoaderHolderTarget(holder, i+1), null, model.emojiHelper.getImageRequest(i), false);
|
||||
}
|
||||
}
|
||||
if(!TextUtils.isEmpty(instance.email)){
|
||||
needDivider=true;
|
||||
SimpleListItemViewHolder holder=new SimpleListItemViewHolder(getActivity(), scrollingLayout);
|
||||
ListItem<Void> item=new ListItem<>(R.string.send_email_to_server_admin, 0, R.drawable.ic_mail_24px, ()->{});
|
||||
holder.bind(item);
|
||||
holder.itemView.setBackground(UiUtils.getThemeDrawable(getActivity(), android.R.attr.selectableItemBackground));
|
||||
holder.itemView.setOnClickListener(v->openAdminEmail());
|
||||
scrollingLayout.addView(holder.itemView);
|
||||
}
|
||||
if(needDivider){
|
||||
View divider=new View(getActivity());
|
||||
divider.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OutlineVariant));
|
||||
LinearLayout.LayoutParams dlp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(1));
|
||||
dlp.leftMargin=dlp.rightMargin=V.dp(16);
|
||||
scrollingLayout.addView(divider, dlp);
|
||||
}
|
||||
scrollingLayout.addView(webView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
|
||||
return scroller;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(){
|
||||
new GetInstanceExtendedDescription()
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(GetInstanceExtendedDescription.Response result){
|
||||
MastodonAPIController.runInBackground(()->{
|
||||
Activity activity=getActivity();
|
||||
if(activity==null)
|
||||
return;
|
||||
String template;
|
||||
try(BufferedReader reader=new BufferedReader(new InputStreamReader(getActivity().getAssets().open("server_about_template.htm")))){
|
||||
StringBuilder sb=new StringBuilder();
|
||||
String line;
|
||||
while((line=reader.readLine())!=null){
|
||||
sb.append(line);
|
||||
sb.append('\n');
|
||||
}
|
||||
template=sb.toString();
|
||||
}catch(IOException x){
|
||||
throw new RuntimeException(x);
|
||||
}
|
||||
|
||||
HashMap<String, String> templateParams=new HashMap<>();
|
||||
templateParams.put("content", result.content);
|
||||
templateParams.put("colorSurface", getThemeColorAsCss(R.attr.colorM3Surface, 1));
|
||||
templateParams.put("colorOnSurface", getThemeColorAsCss(R.attr.colorM3OnSurface, 1));
|
||||
templateParams.put("colorPrimary", getThemeColorAsCss(R.attr.colorM3Primary, 1));
|
||||
templateParams.put("colorPrimaryTransparent", getThemeColorAsCss(R.attr.colorM3Primary, 0.2f));
|
||||
for(Map.Entry<String, String> param:templateParams.entrySet()){
|
||||
template=template.replace("{{"+param.getKey()+"}}", param.getValue());
|
||||
}
|
||||
|
||||
final String html=template;
|
||||
activity.runOnUiThread(()->{
|
||||
webView.loadDataWithBaseURL(null, html, "text/html; charset=utf-8", null, null);
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRefresh(){}
|
||||
|
||||
private void openAdminEmail(){
|
||||
Intent intent=new Intent(Intent.ACTION_VIEW, Uri.fromParts("mailto", instance.email, null));
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
try{
|
||||
startActivity(intent);
|
||||
}catch(ActivityNotFoundException x){
|
||||
Toast.makeText(getActivity(), R.string.no_app_to_handle_action, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
|
||||
scroller.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
progress.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom());
|
||||
}else{
|
||||
scroller.setPadding(0, 0, 0, 0);
|
||||
}
|
||||
super.onApplyWindowInsets(insets);
|
||||
}
|
||||
|
||||
private String getThemeColorAsCss(int attr, float alpha){
|
||||
int color=UiUtils.getThemeColor(getActivity(), attr);
|
||||
if(alpha==1f){
|
||||
return String.format(Locale.US, "#%06X", color & 0xFFFFFF);
|
||||
}else{
|
||||
int r=(color >> 16) & 0xFF;
|
||||
int g=(color >> 8) & 0xFF;
|
||||
int b=color & 0xFF;
|
||||
return "rgba("+r+","+g+","+b+","+alpha+")";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,185 @@
|
|||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.app.Fragment;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayout;
|
||||
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
import me.grishka.appkit.fragments.AppKitFragment;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class SettingsServerFragment extends AppKitFragment{
|
||||
private String accountID;
|
||||
private Instance instance;
|
||||
private TabLayout tabBar;
|
||||
private TabLayoutMediator tabLayoutMediator;
|
||||
private ViewPager2 pager;
|
||||
private FrameLayout[] tabViews;
|
||||
private View contentView;
|
||||
private WindowInsets childInsets;
|
||||
|
||||
private SettingsServerAboutFragment aboutFragment;
|
||||
private SettingsServerRulesFragment rulesFragment;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
accountID=getArguments().getString("account");
|
||||
setTitle(AccountSessionManager.get(accountID).domain);
|
||||
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putBoolean("__is_tab", true);
|
||||
aboutFragment=new SettingsServerAboutFragment();
|
||||
aboutFragment.setArguments(args);
|
||||
rulesFragment=new SettingsServerRulesFragment();
|
||||
rulesFragment.setArguments(args);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){
|
||||
View view=inflater.inflate(R.layout.fragment_settings_server, container, false);
|
||||
|
||||
TextView realTitle=view.findViewById(R.id.real_title);
|
||||
realTitle.setText(getTitle());
|
||||
realTitle.setSelected(true);
|
||||
|
||||
pager=view.findViewById(R.id.pager);
|
||||
pager.setAdapter(new ServerPagerAdapter());
|
||||
|
||||
FrameLayout sizeWrapper=new FrameLayout(getActivity()){
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
|
||||
pager.getLayoutParams().height=MeasureSpec.getSize(heightMeasureSpec)-getPaddingTop()-getPaddingBottom()-V.dp(48);
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
}
|
||||
};
|
||||
|
||||
tabViews=new FrameLayout[2];
|
||||
for(int i=0;i<tabViews.length;i++){
|
||||
FrameLayout tabView=new FrameLayout(getActivity());
|
||||
tabView.setId(switch(i){
|
||||
case 0 -> R.id.server_about;
|
||||
case 1 -> R.id.server_rules;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+i);
|
||||
});
|
||||
tabView.setVisibility(View.GONE);
|
||||
sizeWrapper.addView(tabView); // needed so the fragment manager will have somewhere to restore the tab fragment
|
||||
tabViews[i]=tabView;
|
||||
}
|
||||
|
||||
tabBar=view.findViewById(R.id.tabbar);
|
||||
tabBar.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
|
||||
tabBar.setTabTextSize(V.dp(16));
|
||||
tabLayoutMediator=new TabLayoutMediator(tabBar, pager, (tab, position)->tab.setText(switch(position){
|
||||
case 0 -> R.string.about_server;
|
||||
case 1 -> R.string.server_rules;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+position);
|
||||
}));
|
||||
tabLayoutMediator.attach();
|
||||
|
||||
NestedRecyclerScrollView scrollView=view.findViewById(R.id.scroller);
|
||||
scrollView.setScrollableChildSupplier(()->switch(pager.getCurrentItem()){
|
||||
case 0 -> aboutFragment.scroller;
|
||||
case 1 -> rulesFragment.getList();
|
||||
default -> throw new IllegalStateException("Unexpected value: "+pager.getCurrentItem());
|
||||
});
|
||||
|
||||
return contentView=view;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
getToolbar().setTitle(null);
|
||||
}
|
||||
|
||||
private Fragment getFragmentForPage(int page){
|
||||
return switch(page){
|
||||
case 0 -> aboutFragment;
|
||||
case 1 -> rulesFragment;
|
||||
default -> throw new IllegalStateException();
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
if(contentView!=null){
|
||||
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
|
||||
int insetBottom=insets.getSystemWindowInsetBottom();
|
||||
childInsets=insets.inset(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0);
|
||||
applyChildWindowInsets();
|
||||
insets=insets.inset(0, 0, 0, insetBottom);
|
||||
}
|
||||
}
|
||||
super.onApplyWindowInsets(insets);
|
||||
}
|
||||
|
||||
private void applyChildWindowInsets(){
|
||||
if(aboutFragment!=null && aboutFragment.isAdded() && childInsets!=null){
|
||||
aboutFragment.onApplyWindowInsets(childInsets);
|
||||
rulesFragment.onApplyWindowInsets(childInsets);
|
||||
}
|
||||
}
|
||||
|
||||
private class ServerPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
|
||||
@NonNull
|
||||
@Override
|
||||
public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
FrameLayout view=tabViews[viewType];
|
||||
((ViewGroup)view.getParent()).removeView(view);
|
||||
view.setVisibility(View.VISIBLE);
|
||||
view.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
return new SimpleViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position){
|
||||
Fragment fragment=getFragmentForPage(position);
|
||||
if(!fragment.isAdded()){
|
||||
getChildFragmentManager().beginTransaction().add(holder.itemView.getId(), fragment).commit();
|
||||
holder.itemView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
getChildFragmentManager().executePendingTransactions();
|
||||
if(fragment.isAdded()){
|
||||
holder.itemView.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
applyChildWindowInsets();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position){
|
||||
return position;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.MastodonRecyclerFragment;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.ui.adapters.InstanceRulesAdapter;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class SettingsServerRulesFragment extends MastodonRecyclerFragment<Instance.Rule>{
|
||||
private String accountID;
|
||||
|
||||
public SettingsServerRulesFragment(){
|
||||
super(20);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
accountID=getArguments().getString("account");
|
||||
Instance instance=AccountSessionManager.getInstance().getInstanceInfo(AccountSessionManager.get(accountID).domain);
|
||||
onDataLoaded(instance.rules);
|
||||
setRefreshEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
return new InstanceRulesAdapter(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View onCreateFooterView(LayoutInflater inflater){
|
||||
return inflater.inflate(R.layout.load_more_with_end_mark, null);
|
||||
}
|
||||
|
||||
public RecyclerView getList(){
|
||||
return list;
|
||||
}
|
||||
}
|
|
@ -1,79 +1,56 @@
|
|||
package org.joinmastodon.android.model;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import org.joinmastodon.android.api.ObjectValidationException;
|
||||
import org.joinmastodon.android.api.RequiredField;
|
||||
import org.parceler.Parcel;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Parcel
|
||||
public class Filter extends BaseModel{
|
||||
@RequiredField
|
||||
public String id;
|
||||
|
||||
@RequiredField
|
||||
public String phrase;
|
||||
public transient EnumSet<FilterContext> context=EnumSet.noneOf(FilterContext.class);
|
||||
public String title;
|
||||
|
||||
@RequiredField
|
||||
public EnumSet<FilterContext> context;
|
||||
|
||||
public Instant expiresAt;
|
||||
public boolean irreversible;
|
||||
public boolean wholeWord;
|
||||
public FilterAction filterAction;
|
||||
|
||||
@SerializedName("context")
|
||||
private List<FilterContext> _context;
|
||||
@RequiredField
|
||||
public List<FilterKeyword> keywords;
|
||||
|
||||
private transient Pattern pattern;
|
||||
@RequiredField
|
||||
public List<FilterStatus> statuses;
|
||||
|
||||
@Override
|
||||
public void postprocess() throws ObjectValidationException{
|
||||
super.postprocess();
|
||||
if(_context==null)
|
||||
throw new ObjectValidationException();
|
||||
for(FilterContext c:_context){
|
||||
if(c!=null)
|
||||
context.add(c);
|
||||
}
|
||||
for(FilterKeyword keyword:keywords)
|
||||
keyword.postprocess();
|
||||
for(FilterStatus status:statuses)
|
||||
status.postprocess();
|
||||
}
|
||||
|
||||
public boolean matches(CharSequence text){
|
||||
if(TextUtils.isEmpty(text))
|
||||
return false;
|
||||
if(pattern==null){
|
||||
if(wholeWord)
|
||||
pattern=Pattern.compile("\\b"+Pattern.quote(phrase)+"\\b", Pattern.CASE_INSENSITIVE);
|
||||
else
|
||||
pattern=Pattern.compile(Pattern.quote(phrase), Pattern.CASE_INSENSITIVE);
|
||||
}
|
||||
return pattern.matcher(text).find();
|
||||
}
|
||||
|
||||
public boolean matches(Status status){
|
||||
return matches(status.getContentStatus().getStrippedText());
|
||||
public boolean isActive(){
|
||||
return expiresAt==null || expiresAt.isAfter(Instant.now());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "Filter{"+
|
||||
"id='"+id+'\''+
|
||||
", phrase='"+phrase+'\''+
|
||||
", title='"+title+'\''+
|
||||
", context="+context+
|
||||
", expiresAt="+expiresAt+
|
||||
", irreversible="+irreversible+
|
||||
", wholeWord="+wholeWord+
|
||||
", filterAction="+filterAction+
|
||||
", keywords="+keywords+
|
||||
", statuses="+statuses+
|
||||
'}';
|
||||
}
|
||||
|
||||
public enum FilterContext{
|
||||
@SerializedName("home")
|
||||
HOME,
|
||||
@SerializedName("notifications")
|
||||
NOTIFICATIONS,
|
||||
@SerializedName("public")
|
||||
PUBLIC,
|
||||
@SerializedName("thread")
|
||||
THREAD
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
package org.joinmastodon.android.model;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public enum FilterAction{
|
||||
@SerializedName("warn")
|
||||
WARN,
|
||||
@SerializedName("hide")
|
||||
HIDE
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package org.joinmastodon.android.model;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
public enum FilterContext{
|
||||
@SerializedName("home")
|
||||
HOME,
|
||||
@SerializedName("notifications")
|
||||
NOTIFICATIONS,
|
||||
@SerializedName("public")
|
||||
PUBLIC,
|
||||
@SerializedName("thread")
|
||||
THREAD,
|
||||
@SerializedName("account")
|
||||
ACCOUNT;
|
||||
|
||||
@StringRes
|
||||
public int getDisplayNameRes(){
|
||||
return switch(this){
|
||||
case HOME -> R.string.filter_context_home_lists;
|
||||
case NOTIFICATIONS -> R.string.filter_context_notifications;
|
||||
case PUBLIC -> R.string.filter_context_public_timelines;
|
||||
case THREAD -> R.string.filter_context_threads_replies;
|
||||
case ACCOUNT -> R.string.filter_context_profiles;
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package org.joinmastodon.android.model;
|
||||
|
||||
import org.joinmastodon.android.api.AllFieldsAreRequired;
|
||||
import org.parceler.Parcel;
|
||||
|
||||
@AllFieldsAreRequired
|
||||
@Parcel
|
||||
public class FilterKeyword extends BaseModel{
|
||||
public String id;
|
||||
public String keyword;
|
||||
public boolean wholeWord;
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "FilterKeyword{"+
|
||||
"id='"+id+'\''+
|
||||
", keyword='"+keyword+'\''+
|
||||
", wholeWord="+wholeWord+
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package org.joinmastodon.android.model;
|
||||
|
||||
import org.joinmastodon.android.api.AllFieldsAreRequired;
|
||||
import org.parceler.Parcel;
|
||||
|
||||
@AllFieldsAreRequired
|
||||
@Parcel
|
||||
public class FilterStatus extends BaseModel{
|
||||
public String id;
|
||||
public String statusId;
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package org.joinmastodon.android.model;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import org.joinmastodon.android.api.ObjectValidationException;
|
||||
import org.joinmastodon.android.api.RequiredField;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class LegacyFilter extends BaseModel{
|
||||
@RequiredField
|
||||
public String id;
|
||||
@RequiredField
|
||||
public String phrase;
|
||||
public transient EnumSet<FilterContext> context=EnumSet.noneOf(FilterContext.class);
|
||||
public Instant expiresAt;
|
||||
public boolean irreversible;
|
||||
public boolean wholeWord;
|
||||
|
||||
@SerializedName("context")
|
||||
private List<FilterContext> _context;
|
||||
|
||||
private transient Pattern pattern;
|
||||
|
||||
@Override
|
||||
public void postprocess() throws ObjectValidationException{
|
||||
super.postprocess();
|
||||
if(_context==null)
|
||||
throw new ObjectValidationException();
|
||||
for(FilterContext c:_context){
|
||||
if(c!=null)
|
||||
context.add(c);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean matches(CharSequence text){
|
||||
if(TextUtils.isEmpty(text))
|
||||
return false;
|
||||
if(pattern==null){
|
||||
if(wholeWord)
|
||||
pattern=Pattern.compile("\\b"+Pattern.quote(phrase)+"\\b", Pattern.CASE_INSENSITIVE);
|
||||
else
|
||||
pattern=Pattern.compile(Pattern.quote(phrase), Pattern.CASE_INSENSITIVE);
|
||||
}
|
||||
return pattern.matcher(text).find();
|
||||
}
|
||||
|
||||
public boolean matches(Status status){
|
||||
return matches(status.getContentStatus().getStrippedText());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "Filter{"+
|
||||
"id='"+id+'\''+
|
||||
", phrase='"+phrase+'\''+
|
||||
", context="+context+
|
||||
", expiresAt="+expiresAt+
|
||||
", irreversible="+irreversible+
|
||||
", wholeWord="+wholeWord+
|
||||
'}';
|
||||
}
|
||||
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
package org.joinmastodon.android.model;
|
||||
|
||||
import android.text.SpannableStringBuilder;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ParsedAccount{
|
||||
public Account account;
|
||||
public CharSequence parsedName, parsedBio;
|
||||
public CustomEmojiHelper emojiHelper;
|
||||
public ImageLoaderRequest avatarRequest;
|
||||
|
||||
public ParsedAccount(Account account, String accountID){
|
||||
this.account=account;
|
||||
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
|
||||
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
|
||||
|
||||
emojiHelper=new CustomEmojiHelper();
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder(parsedName);
|
||||
ssb.append(parsedBio);
|
||||
emojiHelper.setText(ssb);
|
||||
|
||||
avatarRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(40), V.dp(40));
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ public class Poll extends BaseModel{
|
|||
@RequiredField
|
||||
public String id;
|
||||
public Instant expiresAt;
|
||||
private boolean expired;
|
||||
public boolean expired;
|
||||
public boolean multiple;
|
||||
public int votersCount;
|
||||
public int votesCount;
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
package org.joinmastodon.android.model.viewmodel;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.AccountField;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
@ -14,14 +17,19 @@ public class AccountViewModel{
|
|||
public final Account account;
|
||||
public final ImageLoaderRequest avaRequest;
|
||||
public final CustomEmojiHelper emojiHelper;
|
||||
public final CharSequence parsedName;
|
||||
public final CharSequence parsedName, parsedBio;
|
||||
public final String verifiedLink;
|
||||
|
||||
public AccountViewModel(Account account){
|
||||
public AccountViewModel(Account account, String accountID){
|
||||
this.account=account;
|
||||
avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(50), V.dp(50));
|
||||
emojiHelper=new CustomEmojiHelper();
|
||||
emojiHelper.setText(parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis));
|
||||
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
|
||||
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
|
||||
else
|
||||
parsedName=account.displayName;
|
||||
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
|
||||
emojiHelper.setText(parsedName);
|
||||
String verifiedLink=null;
|
||||
for(AccountField fld:account.fields){
|
||||
if(fld.verifiedAt!=null){
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
package org.joinmastodon.android.model.viewmodel;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class CheckableListItem<T> extends ListItem<T>{
|
||||
public Style style;
|
||||
public boolean checked;
|
||||
public Consumer<Boolean> checkedChangeListener;
|
||||
|
||||
public CheckableListItem(String title, String subtitle, Style style, boolean checked, int iconRes, Runnable onClick, T parentObject, boolean dividerAfter){
|
||||
super(title, subtitle, iconRes, onClick, parentObject, 0, dividerAfter);
|
||||
this.style=style;
|
||||
this.checked=checked;
|
||||
}
|
||||
|
||||
public CheckableListItem(String title, String subtitle, Style style, boolean checked, Runnable onClick){
|
||||
this(title, subtitle, style, checked, 0, onClick, null, false);
|
||||
}
|
||||
|
||||
public CheckableListItem(String title, String subtitle, Style style, boolean checked, Runnable onClick, T parentObject){
|
||||
this(title, subtitle, style, checked, 0, onClick, parentObject, false);
|
||||
}
|
||||
|
||||
public CheckableListItem(String title, String subtitle, Style style, boolean checked, int iconRes, Runnable onClick){
|
||||
this(title, subtitle, style, checked, iconRes, onClick, null, false);
|
||||
}
|
||||
|
||||
public CheckableListItem(String title, String subtitle, Style style, boolean checked, int iconRes, Runnable onClick, T parentObject){
|
||||
this(title, subtitle, style, checked, iconRes, onClick, parentObject, false);
|
||||
}
|
||||
|
||||
public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, Runnable onClick){
|
||||
this(titleRes, subtitleRes, style, checked, 0, onClick, false);
|
||||
}
|
||||
|
||||
public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, Runnable onClick, boolean dividerAfter){
|
||||
this(titleRes, subtitleRes, style, checked, 0, onClick, dividerAfter);
|
||||
}
|
||||
|
||||
public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, int iconRes, Runnable onClick){
|
||||
this(titleRes, subtitleRes, style, checked, iconRes, onClick, false);
|
||||
}
|
||||
|
||||
public CheckableListItem(int titleRes, int subtitleRes, Style style, boolean checked, int iconRes, Runnable onClick, boolean dividerAfter){
|
||||
super(titleRes, subtitleRes, iconRes, onClick, 0, dividerAfter);
|
||||
this.style=style;
|
||||
this.checked=checked;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(){
|
||||
return switch(style){
|
||||
case CHECKBOX -> R.id.list_item_checkbox;
|
||||
case RADIO -> R.id.list_item_radio;
|
||||
case SWITCH -> R.id.list_item_switch;
|
||||
};
|
||||
}
|
||||
|
||||
public void setChecked(boolean checked){
|
||||
this.checked=checked;
|
||||
}
|
||||
|
||||
public void toggle(){
|
||||
checked=!checked;
|
||||
}
|
||||
|
||||
public enum Style{
|
||||
CHECKBOX,
|
||||
RADIO,
|
||||
SWITCH
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package org.joinmastodon.android.model.viewmodel;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
public class ListItem<T>{
|
||||
public String title;
|
||||
public String subtitle;
|
||||
@StringRes
|
||||
public int titleRes;
|
||||
@StringRes
|
||||
public int subtitleRes;
|
||||
@DrawableRes
|
||||
public int iconRes;
|
||||
public int colorOverrideAttr;
|
||||
public boolean dividerAfter;
|
||||
public Runnable onClick;
|
||||
public boolean isEnabled=true;
|
||||
public T parentObject;
|
||||
|
||||
public ListItem(String title, String subtitle, int iconRes, Runnable onClick, T parentObject, int colorOverrideAttr, boolean dividerAfter){
|
||||
this.title=title;
|
||||
this.subtitle=subtitle;
|
||||
this.iconRes=iconRes;
|
||||
this.colorOverrideAttr=colorOverrideAttr;
|
||||
this.dividerAfter=dividerAfter;
|
||||
this.onClick=onClick;
|
||||
this.parentObject=parentObject;
|
||||
if(onClick==null)
|
||||
isEnabled=false;
|
||||
}
|
||||
|
||||
public ListItem(String title, String subtitle, Runnable onClick){
|
||||
this(title, subtitle, 0, onClick, null, 0, false);
|
||||
}
|
||||
|
||||
public ListItem(String title, String subtitle, Runnable onClick, T parentObject){
|
||||
this(title, subtitle, 0, onClick, parentObject, 0, false);
|
||||
}
|
||||
|
||||
public ListItem(String title, String subtitle, @DrawableRes int iconRes, Runnable onClick){
|
||||
this(title, subtitle, iconRes, onClick, null, 0, false);
|
||||
}
|
||||
|
||||
public ListItem(String title, String subtitle, @DrawableRes int iconRes, Runnable onClick, T parentObject){
|
||||
this(title, subtitle, iconRes, onClick, parentObject, 0, false);
|
||||
}
|
||||
|
||||
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, Runnable onClick){
|
||||
this(null, null, 0, onClick, null, 0, false);
|
||||
this.titleRes=titleRes;
|
||||
this.subtitleRes=subtitleRes;
|
||||
}
|
||||
|
||||
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, Runnable onClick, int colorOverrideAttr, boolean dividerAfter){
|
||||
this(null, null, 0, onClick, null, colorOverrideAttr, dividerAfter);
|
||||
this.titleRes=titleRes;
|
||||
this.subtitleRes=subtitleRes;
|
||||
}
|
||||
|
||||
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, @DrawableRes int iconRes, Runnable onClick){
|
||||
this(null, null, iconRes, onClick, null, 0, false);
|
||||
this.titleRes=titleRes;
|
||||
this.subtitleRes=subtitleRes;
|
||||
}
|
||||
|
||||
public ListItem(@StringRes int titleRes, @StringRes int subtitleRes, @DrawableRes int iconRes, Runnable onClick, int colorOverrideAttr, boolean dividerAfter){
|
||||
this(null, null, iconRes, onClick, null, colorOverrideAttr, dividerAfter);
|
||||
this.titleRes=titleRes;
|
||||
this.subtitleRes=subtitleRes;
|
||||
}
|
||||
|
||||
public int getItemViewType(){
|
||||
return colorOverrideAttr==0 ? R.id.list_item_simple : R.id.list_item_simple_tinted;
|
||||
}
|
||||
}
|
|
@ -111,21 +111,12 @@ public class AccountSwitcherSheet extends BottomSheet{
|
|||
}
|
||||
|
||||
private void logOut(String accountID){
|
||||
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(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
onLoggedOut(accountID);
|
||||
}
|
||||
})
|
||||
.wrapProgress(activity, R.string.loading, false)
|
||||
.exec(accountID);
|
||||
AccountSessionManager.get(accountID).logOut(activity, ()->{
|
||||
dismiss();
|
||||
activity.finish();
|
||||
Intent intent=new Intent(activity, MainActivity.class);
|
||||
activity.startActivity(intent);
|
||||
});
|
||||
}
|
||||
|
||||
private void logOutAll(){
|
||||
|
@ -163,11 +154,6 @@ public class AccountSwitcherSheet extends BottomSheet{
|
|||
}
|
||||
}
|
||||
|
||||
private void onLoggedOut(String accountID){
|
||||
AccountSessionManager.getInstance().removeAccount(accountID);
|
||||
dismiss();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onWindowInsetsUpdated(WindowInsets insets){
|
||||
if(Build.VERSION.SDK_INT>=29){
|
||||
|
|
|
@ -2,12 +2,21 @@ package org.joinmastodon.android.ui;
|
|||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class M3AlertDialogBuilder extends AlertDialog.Builder{
|
||||
private CharSequence supportingText, title, helpText;
|
||||
private AlertDialog alert;
|
||||
|
||||
public M3AlertDialogBuilder(Context context){
|
||||
super(context);
|
||||
}
|
||||
|
@ -18,12 +27,36 @@ public class M3AlertDialogBuilder extends AlertDialog.Builder{
|
|||
|
||||
@Override
|
||||
public AlertDialog create(){
|
||||
AlertDialog alert=super.create();
|
||||
if(!TextUtils.isEmpty(helpText) && !TextUtils.isEmpty(supportingText))
|
||||
throw new IllegalStateException("You can't have both help text and supporting text in the same alert");
|
||||
|
||||
if(!TextUtils.isEmpty(supportingText)){
|
||||
View titleLayout=getContext().getSystemService(LayoutInflater.class).inflate(R.layout.alert_title_with_supporting_text, null);
|
||||
TextView title=titleLayout.findViewById(R.id.title);
|
||||
TextView subtitle=titleLayout.findViewById(R.id.subtitle);
|
||||
title.setText(this.title);
|
||||
subtitle.setText(supportingText);
|
||||
setCustomTitle(titleLayout);
|
||||
}else if(!TextUtils.isEmpty(helpText)){
|
||||
View titleLayout=getContext().getSystemService(LayoutInflater.class).inflate(R.layout.alert_title_with_help, null);
|
||||
TextView title=titleLayout.findViewById(R.id.title);
|
||||
TextView helpText=titleLayout.findViewById(R.id.help_text);
|
||||
View helpButton=titleLayout.findViewById(R.id.help);
|
||||
title.setText(this.title);
|
||||
helpText.setText(this.helpText);
|
||||
helpButton.setOnClickListener(v->{
|
||||
helpText.setVisibility(helpText.getVisibility()==View.VISIBLE ? View.GONE : View.VISIBLE);
|
||||
helpButton.setSelected(helpText.getVisibility()==View.VISIBLE);
|
||||
});
|
||||
setCustomTitle(titleLayout);
|
||||
}
|
||||
|
||||
alert=super.create();
|
||||
alert.create();
|
||||
Button btn=alert.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
if(btn!=null){
|
||||
View buttonBar=(View) btn.getParent();
|
||||
buttonBar.setPadding(V.dp(16), 0, V.dp(16), V.dp(24));
|
||||
buttonBar.setPadding(V.dp(16), V.dp(16), V.dp(16), V.dp(16));
|
||||
((View)buttonBar.getParent()).setPadding(0, 0, 0, 0);
|
||||
}
|
||||
// hacc
|
||||
|
@ -49,13 +82,40 @@ public class M3AlertDialogBuilder extends AlertDialog.Builder{
|
|||
scrollView.setPadding(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
int messageID=getContext().getResources().getIdentifier("message", "id", "android");
|
||||
if(messageID!=0){
|
||||
View message=alert.findViewById(messageID);
|
||||
if(message!=null){
|
||||
message.setPadding(message.getPaddingLeft(), message.getPaddingTop(), message.getPaddingRight(), V.dp(24));
|
||||
}
|
||||
}
|
||||
return alert;
|
||||
}
|
||||
|
||||
public M3AlertDialogBuilder setSupportingText(CharSequence text){
|
||||
supportingText=text;
|
||||
return this;
|
||||
}
|
||||
|
||||
public M3AlertDialogBuilder setSupportingText(@StringRes int text){
|
||||
supportingText=getContext().getString(text);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public M3AlertDialogBuilder setTitle(CharSequence title){
|
||||
super.setTitle(title);
|
||||
this.title=title;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public M3AlertDialogBuilder setTitle(@StringRes int title){
|
||||
super.setTitle(title);
|
||||
this.title=getContext().getString(title);
|
||||
return this;
|
||||
}
|
||||
|
||||
public M3AlertDialogBuilder setHelpText(CharSequence text){
|
||||
helpText=text;
|
||||
return this;
|
||||
}
|
||||
|
||||
public M3AlertDialogBuilder setHelpText(@StringRes int text){
|
||||
helpText=getContext().getString(text);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import me.grishka.appkit.utils.V;
|
|||
public class OutlineProviders{
|
||||
private static final SparseArray<ViewOutlineProvider> roundedRects=new SparseArray<>();
|
||||
private static final SparseArray<ViewOutlineProvider> topRoundedRects=new SparseArray<>();
|
||||
private static final SparseArray<ViewOutlineProvider> bottomRoundedRects=new SparseArray<>();
|
||||
private static final SparseArray<ViewOutlineProvider> endRoundedRects=new SparseArray<>();
|
||||
|
||||
public static final int RADIUS_XSMALL=4;
|
||||
|
@ -54,6 +55,15 @@ public class OutlineProviders{
|
|||
return provider;
|
||||
}
|
||||
|
||||
public static ViewOutlineProvider bottomRoundedRect(int dp){
|
||||
ViewOutlineProvider provider=bottomRoundedRects.get(dp);
|
||||
if(provider!=null)
|
||||
return provider;
|
||||
provider=new BottomRoundRectOutlineProvider(V.dp(dp));
|
||||
bottomRoundedRects.put(dp, provider);
|
||||
return provider;
|
||||
}
|
||||
|
||||
public static ViewOutlineProvider endRoundedRect(int dp){
|
||||
ViewOutlineProvider provider=endRoundedRects.get(dp);
|
||||
if(provider!=null)
|
||||
|
@ -89,6 +99,19 @@ public class OutlineProviders{
|
|||
}
|
||||
}
|
||||
|
||||
private static class BottomRoundRectOutlineProvider extends ViewOutlineProvider{
|
||||
private final int radius;
|
||||
|
||||
private BottomRoundRectOutlineProvider(int radius){
|
||||
this.radius=radius;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getOutline(View view, Outline outline){
|
||||
outline.setRoundRect(0, -radius, view.getWidth(), view.getHeight(), radius);
|
||||
}
|
||||
}
|
||||
|
||||
private static class EndRoundRectOutlineProvider extends ViewOutlineProvider{
|
||||
private final int radius;
|
||||
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
package org.joinmastodon.android.ui.adapters;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.viewholders.CheckboxOrRadioListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.ListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.SimpleListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.SwitchListItemViewHolder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class GenericListItemsAdapter<T> extends RecyclerView.Adapter<ListItemViewHolder<?>>{
|
||||
private List<ListItem<T>> items;
|
||||
|
||||
public GenericListItemsAdapter(List<ListItem<T>> items){
|
||||
this.items=items;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ListItemViewHolder<?> onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
if(viewType==R.id.list_item_simple || viewType==R.id.list_item_simple_tinted)
|
||||
return new SimpleListItemViewHolder(parent.getContext(), parent);
|
||||
if(viewType==R.id.list_item_switch)
|
||||
return new SwitchListItemViewHolder(parent.getContext(), parent);
|
||||
if(viewType==R.id.list_item_checkbox)
|
||||
return new CheckboxOrRadioListItemViewHolder(parent.getContext(), parent, false);
|
||||
if(viewType==R.id.list_item_radio)
|
||||
return new CheckboxOrRadioListItemViewHolder(parent.getContext(), parent, true);
|
||||
|
||||
throw new IllegalArgumentException("Unexpected view type "+viewType);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ListItemViewHolder<?> holder, int position){
|
||||
((ListItemViewHolder<ListItem<T>>)holder).bind(items.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return items.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position){
|
||||
return items.get(position).getItemViewType();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package org.joinmastodon.android.ui.adapters;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.ui.viewholders.InstanceRuleViewHolder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class InstanceRulesAdapter extends RecyclerView.Adapter<InstanceRuleViewHolder>{
|
||||
private final List<Instance.Rule> rules;
|
||||
|
||||
public InstanceRulesAdapter(List<Instance.Rule> rules){
|
||||
this.rules=rules;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public InstanceRuleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new InstanceRuleViewHolder(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull InstanceRuleViewHolder holder, int position){
|
||||
holder.setPosition(position);
|
||||
holder.bind(rules.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return rules.size();
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ import android.widget.ImageView;
|
|||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
|
@ -29,7 +30,10 @@ public class AccountStatusDisplayItem extends StatusDisplayItem{
|
|||
public AccountStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Account account){
|
||||
super(parentID, parentFragment);
|
||||
this.account=account;
|
||||
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
|
||||
if(AccountSessionManager.get(parentFragment.getAccountID()).getLocalPreferences().customEmojiInNames)
|
||||
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
|
||||
else
|
||||
parsedName=account.displayName;
|
||||
emojiHelper.setText(parsedName);
|
||||
if(!TextUtils.isEmpty(account.avatar))
|
||||
avaRequest=new UrlImageLoaderRequest(account.avatar, V.dp(50), V.dp(50));
|
||||
|
|
|
@ -103,7 +103,7 @@ public class AudioStatusDisplayItem extends StatusDisplayItem{
|
|||
@Override
|
||||
public void onBind(AudioStatusDisplayItem item){
|
||||
int seconds=(int)item.attachment.getDuration();
|
||||
String duration=UiUtils.formatDuration(seconds);
|
||||
String duration=UiUtils.formatMediaDuration(seconds);
|
||||
AudioPlayerService service=AudioPlayerService.getInstance();
|
||||
if(service!=null && service.getAttachmentID().equals(item.attachment.id)){
|
||||
forwardBtn.setVisibility(View.VISIBLE);
|
||||
|
@ -168,7 +168,7 @@ public class AudioStatusDisplayItem extends StatusDisplayItem{
|
|||
setPlayButtonPlaying(false, true);
|
||||
forwardBtn.setVisibility(View.INVISIBLE);
|
||||
rewindBtn.setVisibility(View.INVISIBLE);
|
||||
time.setText(UiUtils.formatDuration((int)item.attachment.getDuration()));
|
||||
time.setText(UiUtils.formatMediaDuration((int)item.attachment.getDuration()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -187,7 +187,7 @@ public class AudioStatusDisplayItem extends StatusDisplayItem{
|
|||
int posSeconds=(int)pos;
|
||||
if(posSeconds!=lastPosSeconds){
|
||||
lastPosSeconds=posSeconds;
|
||||
time.setText(UiUtils.formatDuration(posSeconds)+"/"+UiUtils.formatDuration((int)item.attachment.getDuration()));
|
||||
time.setText(UiUtils.formatMediaDuration(posSeconds)+"/"+UiUtils.formatMediaDuration((int)item.attachment.getDuration()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -68,7 +68,7 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
|
|||
reblogs.setText(itemView.getResources().getQuantityString(R.plurals.x_reblogs, (int)item.status.reblogsCount, item.status.reblogsCount));
|
||||
if(s.editedAt!=null){
|
||||
editHistory.setVisibility(View.VISIBLE);
|
||||
editHistory.setText(item.parentFragment.getString(R.string.last_edit_at_x, UiUtils.formatRelativeTimestampAsMinutesAgo(itemView.getContext(), s.editedAt)));
|
||||
editHistory.setText(item.parentFragment.getString(R.string.last_edit_at_x, UiUtils.formatRelativeTimestampAsMinutesAgo(itemView.getContext(), s.editedAt, false)));
|
||||
}else{
|
||||
editHistory.setVisibility(View.GONE);
|
||||
}
|
||||
|
|
|
@ -6,13 +6,16 @@ import android.content.res.ColorStateList;
|
|||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
|
@ -133,6 +136,20 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
|||
}
|
||||
|
||||
private void onBoostClick(View v){
|
||||
if(GlobalUserPreferences.confirmBoost){
|
||||
PopupMenu menu=new PopupMenu(itemView.getContext(), boost);
|
||||
menu.getMenu().add(R.string.button_reblog);
|
||||
menu.setOnMenuItemClickListener(item->{
|
||||
doBoost();
|
||||
return true;
|
||||
});
|
||||
menu.show();
|
||||
}else{
|
||||
doBoost();
|
||||
}
|
||||
}
|
||||
|
||||
private void doBoost(){
|
||||
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setReblogged(item.status, !item.status.reblogged);
|
||||
boost.setSelected(item.status.reblogged);
|
||||
bindButton(boost, item.status.reblogsCount);
|
||||
|
|
|
@ -71,7 +71,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
|||
this.accountID=accountID;
|
||||
parsedName=new SpannableStringBuilder(user.displayName);
|
||||
this.status=status;
|
||||
HtmlParser.parseCustomEmoji(parsedName, user.emojis);
|
||||
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
|
||||
HtmlParser.parseCustomEmoji(parsedName, user.emojis);
|
||||
emojiHelper.setText(parsedName);
|
||||
if(status!=null){
|
||||
hasVisibilityToggle=status.sensitive || !TextUtils.isEmpty(status.spoilerText);
|
||||
|
|
|
@ -6,7 +6,9 @@ import android.text.TextUtils;
|
|||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
|
@ -90,6 +92,7 @@ public abstract class StatusDisplayItem{
|
|||
ArrayList<StatusDisplayItem> items=new ArrayList<>();
|
||||
Status statusForContent=status.getContentStatus();
|
||||
HeaderStatusDisplayItem header=null;
|
||||
boolean hideCounts=!AccountSessionManager.get(accountID).getLocalPreferences().showInteractionCounts;
|
||||
if((flags & FLAG_NO_HEADER)==0){
|
||||
if(status.reblog!=null){
|
||||
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.user_boosted, status.account.displayName), status.account.emojis, R.drawable.ic_repeat_20px));
|
||||
|
@ -104,7 +107,7 @@ public abstract class StatusDisplayItem{
|
|||
}
|
||||
|
||||
ArrayList<StatusDisplayItem> contentItems;
|
||||
if(!TextUtils.isEmpty(statusForContent.spoilerText)){
|
||||
if(!TextUtils.isEmpty(statusForContent.spoilerText) && AccountSessionManager.get(accountID).getLocalPreferences().showCWs){
|
||||
SpoilerStatusDisplayItem spoilerItem=new SpoilerStatusDisplayItem(parentID, fragment, statusForContent);
|
||||
items.add(spoilerItem);
|
||||
contentItems=spoilerItem.contentItems;
|
||||
|
@ -126,6 +129,8 @@ public abstract class StatusDisplayItem{
|
|||
MediaGridStatusDisplayItem mediaGrid=new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent);
|
||||
if((flags & FLAG_MEDIA_FORCE_HIDDEN)!=0)
|
||||
mediaGrid.sensitiveTitle=fragment.getString(R.string.media_hidden);
|
||||
else if(statusForContent.sensitive && !AccountSessionManager.get(accountID).getLocalPreferences().hideSensitiveMedia)
|
||||
mediaGrid.sensitiveRevealed=true;
|
||||
contentItems.add(mediaGrid);
|
||||
}
|
||||
for(Attachment att:statusForContent.mediaAttachments){
|
||||
|
@ -140,7 +145,9 @@ public abstract class StatusDisplayItem{
|
|||
contentItems.add(new LinkCardStatusDisplayItem(parentID, fragment, statusForContent));
|
||||
}
|
||||
if((flags & FLAG_NO_FOOTER)==0){
|
||||
items.add(new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID));
|
||||
FooterStatusDisplayItem footer=new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID);
|
||||
footer.hideCounts=hideCounts;
|
||||
items.add(footer);
|
||||
if(status.hasGapAfter && !(fragment instanceof ThreadFragment))
|
||||
items.add(new GapStatusDisplayItem(parentID, fragment));
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ public class MediaAttachmentViewController{
|
|||
altButton.setVisibility(TextUtils.isEmpty(attachment.description) ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
if(type==MediaGridStatusDisplayItem.GridItemType.VIDEO){
|
||||
duration.setText(UiUtils.formatDuration((int)attachment.getDuration()));
|
||||
duration.setText(UiUtils.formatMediaDuration((int)attachment.getDuration()));
|
||||
}
|
||||
didClear=false;
|
||||
}
|
||||
|
|
|
@ -71,6 +71,7 @@ import org.parceler.Parcels;
|
|||
import java.io.File;
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
@ -99,6 +100,7 @@ import okhttp3.MediaType;
|
|||
public class UiUtils{
|
||||
private static Handler mainHandler=new Handler(Looper.getMainLooper());
|
||||
private static final DateTimeFormatter DATE_FORMATTER_SHORT_WITH_YEAR=DateTimeFormatter.ofPattern("d MMM uuuu"), DATE_FORMATTER_SHORT=DateTimeFormatter.ofPattern("d MMM");
|
||||
private static final DateTimeFormatter TIME_FORMATTER=DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT);
|
||||
public static final DateTimeFormatter DATE_TIME_FORMATTER=DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
|
||||
|
||||
private UiUtils(){}
|
||||
|
@ -144,21 +146,52 @@ public class UiUtils{
|
|||
}
|
||||
}
|
||||
|
||||
public static String formatRelativeTimestampAsMinutesAgo(Context context, Instant instant){
|
||||
public static String formatRelativeTimestampAsMinutesAgo(Context context, Instant instant, boolean relativeHours){
|
||||
long t=instant.toEpochMilli();
|
||||
long now=System.currentTimeMillis();
|
||||
long diff=now-t;
|
||||
if(diff<1000L){
|
||||
long diff=System.currentTimeMillis()-t;
|
||||
if(diff<1000L && diff>-1000L){
|
||||
return context.getString(R.string.time_just_now);
|
||||
}else if(diff<60_000L){
|
||||
int secs=(int)(diff/1000L);
|
||||
return context.getResources().getQuantityString(R.plurals.x_seconds_ago, secs, secs);
|
||||
}else if(diff<3600_000L){
|
||||
int mins=(int)(diff/60_000L);
|
||||
return context.getResources().getQuantityString(R.plurals.x_minutes_ago, mins, mins);
|
||||
}else if(diff>0){
|
||||
if(diff<60_000L){
|
||||
int secs=(int)(diff/1000L);
|
||||
return context.getResources().getQuantityString(R.plurals.x_seconds_ago, secs, secs);
|
||||
}else if(diff<3600_000L){
|
||||
int mins=(int)(diff/60_000L);
|
||||
return context.getResources().getQuantityString(R.plurals.x_minutes_ago, mins, mins);
|
||||
}else if(relativeHours && diff<24*3600_000L){
|
||||
int hours=(int)(diff/3600_000L);
|
||||
return context.getResources().getQuantityString(R.plurals.x_hours_ago, hours, hours);
|
||||
}
|
||||
}else{
|
||||
return DATE_TIME_FORMATTER.format(instant.atZone(ZoneId.systemDefault()));
|
||||
if(diff>-60_000L){
|
||||
int secs=-(int)(diff/1000L);
|
||||
return context.getResources().getQuantityString(R.plurals.in_x_seconds, secs, secs);
|
||||
}else if(diff>-3600_000L){
|
||||
int mins=-(int)(diff/60_000L);
|
||||
return context.getResources().getQuantityString(R.plurals.in_x_minutes, mins, mins);
|
||||
}else if(relativeHours && diff>-24*3600_000L){
|
||||
int hours=-(int)(diff/3600_000L);
|
||||
return context.getResources().getQuantityString(R.plurals.in_x_hours, hours, hours);
|
||||
}
|
||||
}
|
||||
ZonedDateTime dt=instant.atZone(ZoneId.systemDefault());
|
||||
ZonedDateTime now=ZonedDateTime.now();
|
||||
String formattedTime=TIME_FORMATTER.format(dt);
|
||||
String formattedDate;
|
||||
LocalDate today=now.toLocalDate();
|
||||
LocalDate date=dt.toLocalDate();
|
||||
if(date.equals(today)){
|
||||
formattedDate=context.getString(R.string.today);
|
||||
}else if(date.equals(today.minusDays(1))){
|
||||
formattedDate=context.getString(R.string.yesterday);
|
||||
}else if(date.equals(today.plusDays(1))){
|
||||
formattedDate=context.getString(R.string.tomorrow);
|
||||
}else if(date.getYear()==today.getYear()){
|
||||
formattedDate=DATE_FORMATTER_SHORT.format(dt);
|
||||
}else{
|
||||
formattedDate=DATE_FORMATTER_SHORT_WITH_YEAR.format(dt);
|
||||
}
|
||||
return context.getString(R.string.date_at_time, formattedDate, formattedTime);
|
||||
}
|
||||
|
||||
public static String formatTimeLeft(Context context, Instant instant){
|
||||
|
@ -317,7 +350,7 @@ public class UiUtils{
|
|||
}
|
||||
|
||||
public static void showConfirmationAlert(Context context, @StringRes int title, @StringRes int message, @StringRes int confirmButton, Runnable onConfirmed){
|
||||
showConfirmationAlert(context, context.getString(title), context.getString(message), context.getString(confirmButton), onConfirmed);
|
||||
showConfirmationAlert(context, context.getString(title), message==0 ? null : context.getString(message), context.getString(confirmButton), onConfirmed);
|
||||
}
|
||||
|
||||
public static void showConfirmationAlert(Context context, CharSequence title, CharSequence message, CharSequence confirmButton, Runnable onConfirmed){
|
||||
|
@ -399,24 +432,26 @@ public class UiUtils{
|
|||
}
|
||||
|
||||
public static void confirmDeletePost(Activity activity, String accountID, Status status, Consumer<Status> resultCallback){
|
||||
showConfirmationAlert(activity, R.string.confirm_delete_title, R.string.confirm_delete, R.string.delete, ()->{
|
||||
new DeleteStatus(status.id)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Status result){
|
||||
resultCallback.accept(result);
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().deleteStatus(status.id);
|
||||
E.post(new StatusDeletedEvent(status.id, accountID));
|
||||
}
|
||||
Runnable delete=()->new DeleteStatus(status.id)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Status result){
|
||||
resultCallback.accept(result);
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().deleteStatus(status.id);
|
||||
E.post(new StatusDeletedEvent(status.id, accountID));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(activity);
|
||||
}
|
||||
})
|
||||
.wrapProgress(activity, R.string.deleting, false)
|
||||
.exec(accountID);
|
||||
});
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(activity);
|
||||
}
|
||||
})
|
||||
.wrapProgress(activity, R.string.deleting, false)
|
||||
.exec(accountID);
|
||||
if(GlobalUserPreferences.confirmDeletePost)
|
||||
showConfirmationAlert(activity, R.string.confirm_delete_title, R.string.confirm_delete, R.string.delete, delete);
|
||||
else
|
||||
delete.run();
|
||||
}
|
||||
|
||||
public static void setRelationshipToActionButton(Relationship relationship, Button button){
|
||||
|
@ -488,25 +523,32 @@ public class UiUtils{
|
|||
}else if(relationship.muting){
|
||||
confirmToggleMuteUser(activity, accountID, account, true, resultCallback);
|
||||
}else{
|
||||
progressCallback.accept(true);
|
||||
new SetAccountFollowed(account.id, !relationship.following && !relationship.requested, true)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Relationship result){
|
||||
resultCallback.accept(result);
|
||||
progressCallback.accept(false);
|
||||
if(!result.following && !result.requested){
|
||||
E.post(new RemoveAccountPostsEvent(accountID, account.id, true));
|
||||
Runnable action=()->{
|
||||
progressCallback.accept(true);
|
||||
new SetAccountFollowed(account.id, !relationship.following && !relationship.requested, true)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Relationship result){
|
||||
resultCallback.accept(result);
|
||||
progressCallback.accept(false);
|
||||
if(!result.following && !result.requested){
|
||||
E.post(new RemoveAccountPostsEvent(accountID, account.id, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(activity);
|
||||
progressCallback.accept(false);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(activity);
|
||||
progressCallback.accept(false);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
};
|
||||
if(relationship.following && GlobalUserPreferences.confirmUnfollow){
|
||||
showConfirmationAlert(activity, null, activity.getString(R.string.unfollow_confirmation, account.getDisplayUsername()), activity.getString(R.string.unfollow), action);
|
||||
}else{
|
||||
action.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -586,9 +628,9 @@ public class UiUtils{
|
|||
|
||||
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 AUTO -> 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;
|
||||
case DARK -> R.style.Theme_Mastodon_Dark;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -718,7 +760,7 @@ public class UiUtils{
|
|||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
public static String formatDuration(int seconds){
|
||||
public static String formatMediaDuration(int seconds){
|
||||
if(seconds>=3600)
|
||||
return String.format("%d:%02d:%02d", seconds/3600, seconds%3600/60, seconds%60);
|
||||
else
|
||||
|
@ -750,4 +792,20 @@ public class UiUtils{
|
|||
}
|
||||
return insets;
|
||||
}
|
||||
|
||||
public static String formatDuration(Context context, int seconds){
|
||||
if(seconds<3600){
|
||||
int minutes=seconds/60;
|
||||
return context.getResources().getQuantityString(R.plurals.x_minutes, minutes, minutes);
|
||||
}else if(seconds<24*3600){
|
||||
int hours=seconds/3600;
|
||||
return context.getResources().getQuantityString(R.plurals.x_hours, hours, hours);
|
||||
}else if(seconds>=7*24*3600 && seconds%(7*24*3600)<24*3600){
|
||||
int weeks=seconds/(7*24*3600);
|
||||
return context.getResources().getQuantityString(R.plurals.x_weeks, weeks, weeks);
|
||||
}else{
|
||||
int days=seconds/(24*3600);
|
||||
return context.getResources().getQuantityString(R.plurals.x_days, days, days);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import org.joinmastodon.android.model.Account;
|
|||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.SearchResults;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
|
@ -58,7 +59,7 @@ public class ComposeAutocompleteViewController{
|
|||
private FrameLayout contentView;
|
||||
private UsableRecyclerView list;
|
||||
private ListImageLoaderWrapper imgLoader;
|
||||
private List<WrappedAccount> users=Collections.emptyList();
|
||||
private List<AccountViewModel> users=Collections.emptyList();
|
||||
private List<Hashtag> hashtags=Collections.emptyList();
|
||||
private List<WrappedEmoji> emojis=Collections.emptyList();
|
||||
private Mode mode;
|
||||
|
@ -226,8 +227,8 @@ public class ComposeAutocompleteViewController{
|
|||
@Override
|
||||
public void onSuccess(SearchResults result){
|
||||
currentRequest=null;
|
||||
List<WrappedAccount> oldList=users;
|
||||
users=result.accounts.stream().map(WrappedAccount::new).collect(Collectors.toList());
|
||||
List<AccountViewModel> oldList=users;
|
||||
users=result.accounts.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList());
|
||||
if(isLoading){
|
||||
isLoading=false;
|
||||
if(users.size()>=LOADING_FAKE_USER_COUNT){
|
||||
|
@ -313,7 +314,7 @@ public class ComposeAutocompleteViewController{
|
|||
|
||||
@Override
|
||||
public ImageLoaderRequest getImageRequest(int position, int image){
|
||||
WrappedAccount a=users.get(position);
|
||||
AccountViewModel a=users.get(position);
|
||||
if(image==0)
|
||||
return a.avaRequest;
|
||||
return a.emojiHelper.getImageRequest(image-1);
|
||||
|
@ -325,7 +326,7 @@ public class ComposeAutocompleteViewController{
|
|||
}
|
||||
}
|
||||
|
||||
private class UserViewHolder extends BindableViewHolder<WrappedAccount> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
|
||||
private class UserViewHolder extends BindableViewHolder<AccountViewModel> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
|
||||
protected final ImageView ava;
|
||||
protected final TextView username;
|
||||
|
||||
|
@ -338,7 +339,7 @@ public class ComposeAutocompleteViewController{
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onBind(WrappedAccount item){
|
||||
public void onBind(AccountViewModel item){
|
||||
username.setText("@"+item.account.acct);
|
||||
}
|
||||
|
||||
|
@ -483,21 +484,6 @@ public class ComposeAutocompleteViewController{
|
|||
}
|
||||
}
|
||||
|
||||
private static class WrappedAccount{
|
||||
private Account account;
|
||||
private CharSequence parsedName;
|
||||
private CustomEmojiHelper emojiHelper;
|
||||
private ImageLoaderRequest avaRequest;
|
||||
|
||||
public WrappedAccount(Account account){
|
||||
this.account=account;
|
||||
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
|
||||
emojiHelper=new CustomEmojiHelper();
|
||||
emojiHelper.setText(parsedName);
|
||||
avaRequest=new UrlImageLoaderRequest(account.avatar, V.dp(50), V.dp(50));
|
||||
}
|
||||
}
|
||||
|
||||
private static class WrappedEmoji{
|
||||
private Emoji emoji;
|
||||
private ImageLoaderRequest request;
|
||||
|
|
|
@ -89,10 +89,32 @@ public class ComposeLanguageAlertViewController{
|
|||
}
|
||||
|
||||
if(previouslySelected!=null){
|
||||
if((previouslySelected.index<specialLocales.size() && Objects.equals(previouslySelected.locale, specialLocales.get(previouslySelected.index).locale)) ||
|
||||
(previouslySelected.index<specialLocales.size()+allLocales.size() && Objects.equals(previouslySelected.locale, allLocales.get(previouslySelected.index-specialLocales.size()).locale))){
|
||||
if(previouslySelected.index!=-1 && ((previouslySelected.index<specialLocales.size() && Objects.equals(previouslySelected.locale, specialLocales.get(previouslySelected.index).locale)) ||
|
||||
(previouslySelected.index<specialLocales.size()+allLocales.size() && Objects.equals(previouslySelected.locale, allLocales.get(previouslySelected.index-specialLocales.size()).locale)))){
|
||||
selectedIndex=previouslySelected.index;
|
||||
selectedLocale=previouslySelected.locale;
|
||||
}else{
|
||||
int i=0;
|
||||
boolean found=false;
|
||||
for(SpecialLocaleInfo li:specialLocales){
|
||||
if(li.locale.equals(previouslySelected.locale)){
|
||||
selectedLocale=li.locale;
|
||||
selectedIndex=i;
|
||||
found=true;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if(!found){
|
||||
for(LocaleInfo li:allLocales){
|
||||
if(li.locale.equals(previouslySelected.locale)){
|
||||
selectedLocale=li.locale;
|
||||
selectedIndex=i;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}else{
|
||||
selectedLocale=specialLocales.get(0).locale;
|
||||
|
|
|
@ -543,6 +543,23 @@ public class ComposeMediaViewController{
|
|||
return attachments.size()<MAX_ATTACHMENTS;
|
||||
}
|
||||
|
||||
public int getMissingAltTextAttachmentCount(){
|
||||
int count=0;
|
||||
for(DraftMediaAttachment att:attachments){
|
||||
if(TextUtils.isEmpty(att.description))
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public boolean areAllAttachmentsImages(){
|
||||
for(DraftMediaAttachment att:attachments){
|
||||
if(!att.mimeType.startsWith("image/"))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public int getMaxAttachments(){
|
||||
return MAX_ATTACHMENTS;
|
||||
}
|
||||
|
|
|
@ -108,7 +108,7 @@ public class ComposePollViewController{
|
|||
updatePollOptionHints();
|
||||
pollDuration=savedInstanceState.getInt("pollDuration");
|
||||
pollIsMultipleChoice=savedInstanceState.getBoolean("pollMultiple");
|
||||
pollDurationValue.setText(formatPollDuration(pollDuration));
|
||||
pollDurationValue.setText(UiUtils.formatDuration(fragment.getContext(), pollDuration));
|
||||
pollStyleValue.setText(pollIsMultipleChoice ? R.string.compose_poll_multiple_choice : R.string.compose_poll_single_choice);
|
||||
}else if(savedInstanceState!=null && !pollOptions.isEmpty()){ // Fragment was recreated but instance was retained
|
||||
pollWrap.setVisibility(View.VISIBLE);
|
||||
|
@ -119,7 +119,7 @@ public class ComposePollViewController{
|
|||
opt.edit.setText(oldOpt.edit.getText());
|
||||
}
|
||||
updatePollOptionHints();
|
||||
pollDurationValue.setText(formatPollDuration(pollDuration));
|
||||
pollDurationValue.setText(UiUtils.formatDuration(fragment.getContext(), pollDuration));
|
||||
pollStyleValue.setText(pollIsMultipleChoice ? R.string.compose_poll_multiple_choice : R.string.compose_poll_single_choice);
|
||||
}else if(savedInstanceState==null && fragment.editingStatus!=null && fragment.editingStatus.poll!=null){
|
||||
pollWrap.setVisibility(View.VISIBLE);
|
||||
|
@ -129,11 +129,11 @@ public class ComposePollViewController{
|
|||
}
|
||||
pollDuration=(int)fragment.editingStatus.poll.expiresAt.minus(fragment.editingStatus.createdAt.toEpochMilli(), ChronoUnit.MILLIS).getEpochSecond();
|
||||
updatePollOptionHints();
|
||||
pollDurationValue.setText(formatPollDuration(pollDuration));
|
||||
pollDurationValue.setText(UiUtils.formatDuration(fragment.getContext(), pollDuration));
|
||||
pollIsMultipleChoice=fragment.editingStatus.poll.multiple;
|
||||
pollStyleValue.setText(pollIsMultipleChoice ? R.string.compose_poll_multiple_choice : R.string.compose_poll_single_choice);
|
||||
}else{
|
||||
pollDurationValue.setText(formatPollDuration(24*3600));
|
||||
pollDurationValue.setText(UiUtils.formatDuration(fragment.getContext(), 24*3600));
|
||||
pollStyleValue.setText(R.string.compose_poll_single_choice);
|
||||
}
|
||||
}
|
||||
|
@ -186,7 +186,7 @@ public class ComposePollViewController{
|
|||
int selectedOption=-1;
|
||||
for(int i=0;i<POLL_LENGTH_OPTIONS.length;i++){
|
||||
int l=POLL_LENGTH_OPTIONS[i];
|
||||
options[i]=formatPollDuration(l);
|
||||
options[i]=UiUtils.formatDuration(fragment.getContext(), l);
|
||||
if(l==pollDuration)
|
||||
selectedOption=i;
|
||||
}
|
||||
|
@ -196,25 +196,12 @@ public class ComposePollViewController{
|
|||
.setTitle(R.string.poll_length)
|
||||
.setPositiveButton(R.string.ok, (dialog, which)->{
|
||||
pollDuration=POLL_LENGTH_OPTIONS[chosenOption[0]];
|
||||
pollDurationValue.setText(formatPollDuration(pollDuration));
|
||||
pollDurationValue.setText(UiUtils.formatDuration(fragment.getContext(), pollDuration));
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private String formatPollDuration(int seconds){
|
||||
if(seconds<3600){
|
||||
int minutes=seconds/60;
|
||||
return fragment.getResources().getQuantityString(R.plurals.x_minutes, minutes, minutes);
|
||||
}else if(seconds<24*3600){
|
||||
int hours=seconds/3600;
|
||||
return fragment.getResources().getQuantityString(R.plurals.x_hours, hours, hours);
|
||||
}else{
|
||||
int days=seconds/(24*3600);
|
||||
return fragment.getResources().getQuantityString(R.plurals.x_days, days, days);
|
||||
}
|
||||
}
|
||||
|
||||
private void showPollStyleAlert(){
|
||||
final int[] option={pollIsMultipleChoice ? R.id.multiple_choice : R.id.single_choice};
|
||||
AlertDialog alert=new M3AlertDialogBuilder(fragment.getActivity())
|
||||
|
|
|
@ -110,6 +110,8 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
|||
}
|
||||
|
||||
public void bindRelationship(){
|
||||
if(relationships==null)
|
||||
return;
|
||||
Relationship rel=relationships.get(item.account.id);
|
||||
if(rel==null || AccountSessionManager.getInstance().isSelf(accountID, item.account)){
|
||||
button.setVisibility(View.GONE);
|
||||
|
@ -193,6 +195,8 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
|||
}
|
||||
|
||||
private void onButtonClick(View v){
|
||||
if(relationships==null)
|
||||
return;
|
||||
ProgressDialog progress=new ProgressDialog(fragment.getActivity());
|
||||
progress.setMessage(fragment.getString(R.string.loading));
|
||||
progress.setCancelable(false);
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package org.joinmastodon.android.ui.viewholders;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.ui.views.CheckableLinearLayout;
|
||||
|
||||
public abstract class CheckableListItemViewHolder extends ListItemViewHolder<CheckableListItem<?>>{
|
||||
protected final CheckableLinearLayout checkableLayout;
|
||||
|
||||
public CheckableListItemViewHolder(Context context, ViewGroup parent){
|
||||
super(context, R.layout.item_generic_list_checkable, parent);
|
||||
checkableLayout=(CheckableLinearLayout) itemView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(CheckableListItem<?> item){
|
||||
super.onBind(item);
|
||||
checkableLayout.setChecked(item.checked);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package org.joinmastodon.android.ui.viewholders;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RadioButton;
|
||||
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class CheckboxOrRadioListItemViewHolder extends CheckableListItemViewHolder{
|
||||
public CheckboxOrRadioListItemViewHolder(Context context, ViewGroup parent, boolean radio){
|
||||
super(context, parent);
|
||||
View iconView=new View(context);
|
||||
iconView.setDuplicateParentStateEnabled(true);
|
||||
CompoundButton terribleHack=radio ? new RadioButton(context) : new CheckBox(context);
|
||||
iconView.setBackground(terribleHack.getButtonDrawable());
|
||||
LinearLayout.LayoutParams lp=new LinearLayout.LayoutParams(V.dp(32), V.dp(32));
|
||||
lp.setMarginStart(V.dp(12));
|
||||
lp.setMarginEnd(V.dp(4));
|
||||
checkableLayout.addView(iconView, lp);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package org.joinmastodon.android.ui.viewholders;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
|
||||
public class InstanceRuleViewHolder extends BindableViewHolder<Instance.Rule>{
|
||||
private final TextView text, number;
|
||||
private int position;
|
||||
|
||||
public InstanceRuleViewHolder(ViewGroup parent){
|
||||
super(parent.getContext(), R.layout.item_server_rule, parent);
|
||||
text=findViewById(R.id.text);
|
||||
number=findViewById(R.id.number);
|
||||
}
|
||||
|
||||
public void setPosition(int position){
|
||||
this.position=position;
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
@Override
|
||||
public void onBind(Instance.Rule item){
|
||||
if(item.parsedText==null){
|
||||
item.parsedText=HtmlParser.parseLinks(item.text);
|
||||
}
|
||||
text.setText(item.parsedText);
|
||||
number.setText(String.format("%d", position+1));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package org.joinmastodon.android.ui.viewholders;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public abstract class ListItemViewHolder<T extends ListItem<?>> extends BindableViewHolder<T> implements UsableRecyclerView.DisableableClickable{
|
||||
protected final TextView title;
|
||||
protected final TextView subtitle;
|
||||
protected final ImageView icon;
|
||||
protected final LinearLayout view;
|
||||
|
||||
public ListItemViewHolder(Context context, int layout, ViewGroup parent){
|
||||
super(context, layout, parent);
|
||||
title=findViewById(R.id.title);
|
||||
subtitle=findViewById(R.id.subtitle);
|
||||
icon=findViewById(R.id.icon);
|
||||
view=(LinearLayout) itemView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(T item){
|
||||
if(TextUtils.isEmpty(item.title))
|
||||
title.setText(item.titleRes);
|
||||
else
|
||||
title.setText(item.title);
|
||||
|
||||
if(TextUtils.isEmpty(item.subtitle) && item.subtitleRes==0){
|
||||
subtitle.setVisibility(View.GONE);
|
||||
title.setMaxLines(2);
|
||||
view.setMinimumHeight(V.dp(56));
|
||||
}else{
|
||||
subtitle.setVisibility(View.VISIBLE);
|
||||
title.setMaxLines(1);
|
||||
view.setMinimumHeight(V.dp(72));
|
||||
if(TextUtils.isEmpty(item.subtitle))
|
||||
subtitle.setText(item.subtitleRes);
|
||||
else
|
||||
subtitle.setText(item.subtitle);
|
||||
}
|
||||
|
||||
if(item.iconRes!=0){
|
||||
icon.setVisibility(View.VISIBLE);
|
||||
icon.setImageResource(item.iconRes);
|
||||
}else{
|
||||
icon.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if(item.colorOverrideAttr!=0){
|
||||
int color=UiUtils.getThemeColor(view.getContext(), item.colorOverrideAttr);
|
||||
title.setTextColor(color);
|
||||
icon.setImageTintList(ColorStateList.valueOf(color));
|
||||
}
|
||||
|
||||
view.setAlpha(item.isEnabled ? 1 : .4f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled(){
|
||||
return item.isEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
item.onClick.run();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package org.joinmastodon.android.ui.viewholders;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
|
||||
public class SimpleListItemViewHolder extends ListItemViewHolder<ListItem<?>>{
|
||||
public SimpleListItemViewHolder(Context context, ViewGroup parent){
|
||||
super(context, R.layout.item_generic_list, parent);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package org.joinmastodon.android.ui.viewholders;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.Gravity;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.ui.views.M3Switch;
|
||||
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class SwitchListItemViewHolder extends CheckableListItemViewHolder{
|
||||
private final M3Switch sw;
|
||||
private boolean ignoreListener;
|
||||
|
||||
public SwitchListItemViewHolder(Context context, ViewGroup parent){
|
||||
super(context, parent);
|
||||
sw=new M3Switch(context);
|
||||
LinearLayout.LayoutParams lp=new LinearLayout.LayoutParams(V.dp(52), V.dp(32));
|
||||
lp.gravity=Gravity.TOP;
|
||||
lp.setMarginStart(V.dp(16));
|
||||
checkableLayout.addView(sw, lp);
|
||||
sw.setOnCheckedChangeListener((buttonView, isChecked)->{
|
||||
if(ignoreListener)
|
||||
return;
|
||||
if(item.checkedChangeListener!=null)
|
||||
item.checkedChangeListener.accept(isChecked);
|
||||
else
|
||||
item.checked=isChecked;
|
||||
});
|
||||
sw.setClickable(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(CheckableListItem<?> item){
|
||||
super.onBind(item);
|
||||
ignoreListener=true;
|
||||
sw.setChecked(item.checked);
|
||||
sw.setEnabled(item.isEnabled);
|
||||
ignoreListener=false;
|
||||
}
|
||||
}
|
|
@ -98,7 +98,7 @@ public class FloatingHintEditTextLayout extends FrameLayout implements CustomVie
|
|||
|
||||
errorView=new LinkedTextView(getContext());
|
||||
errorView.setTextAppearance(R.style.m3_body_small);
|
||||
errorView.setTextColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3OnSurfaceVariant));
|
||||
errorView.setTextColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3Error));
|
||||
errorView.setLinkTextColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3Primary));
|
||||
errorView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
errorView.setPadding(dp(16), dp(4), dp(16), 0);
|
||||
|
@ -106,6 +106,10 @@ public class FloatingHintEditTextLayout extends FrameLayout implements CustomVie
|
|||
addView(errorView);
|
||||
}
|
||||
|
||||
public void updateHint(){
|
||||
label.setText(edit.getHint());
|
||||
}
|
||||
|
||||
private void onTextChanged(Editable text){
|
||||
if(errorState){
|
||||
errorView.setVisibility(View.GONE);
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
package org.joinmastodon.android.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.webkit.WebView;
|
||||
import android.widget.ScrollView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
|
@ -10,19 +15,22 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
|||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
public class NestedRecyclerScrollView extends CustomScrollView{
|
||||
private Supplier<RecyclerView> scrollableChildSupplier;
|
||||
private Supplier<View> scrollableChildSupplier;
|
||||
private boolean takePriorityOverChildViews;
|
||||
|
||||
public NestedRecyclerScrollView(Context context){
|
||||
super(context);
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public NestedRecyclerScrollView(Context context, AttributeSet attrs){
|
||||
super(context, attrs);
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public NestedRecyclerScrollView(Context context, AttributeSet attrs, int defStyleAttr){
|
||||
super(context, attrs, defStyleAttr);
|
||||
public NestedRecyclerScrollView(Context context, AttributeSet attrs, int defStyle){
|
||||
super(context, attrs, defStyle);
|
||||
TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.NestedRecyclerScrollView);
|
||||
takePriorityOverChildViews=ta.getBoolean(R.styleable.NestedRecyclerScrollView_takePriorityOverChildViews, false);
|
||||
ta.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -33,7 +41,7 @@ public class NestedRecyclerScrollView extends CustomScrollView{
|
|||
consumed[1]=dy;
|
||||
return;
|
||||
}
|
||||
}else if((dy<0 && target instanceof RecyclerView rv && isScrolledToTop(rv)) || (dy>0 && !isScrolledToBottom())){
|
||||
}else if((dy<0 && isScrolledToTop(target)) || (dy>0 && !isScrolledToBottom())){
|
||||
scrollBy(0, dy);
|
||||
consumed[1]=dy;
|
||||
return;
|
||||
|
@ -48,7 +56,7 @@ public class NestedRecyclerScrollView extends CustomScrollView{
|
|||
fling((int)velY);
|
||||
return true;
|
||||
}
|
||||
}else if((velY<0 && target instanceof RecyclerView rv && isScrolledToTop(rv)) || (velY>0 && !isScrolledToBottom())){
|
||||
}else if((velY<0 && isScrolledToTop(target)) || (velY>0 && !isScrolledToBottom())){
|
||||
fling((int) velY);
|
||||
return true;
|
||||
}
|
||||
|
@ -59,22 +67,40 @@ public class NestedRecyclerScrollView extends CustomScrollView{
|
|||
return !canScrollVertically(1);
|
||||
}
|
||||
|
||||
private boolean isScrolledToTop(RecyclerView rv){
|
||||
final LinearLayoutManager lm=(LinearLayoutManager) rv.getLayoutManager();
|
||||
return lm.findFirstVisibleItemPosition()==0
|
||||
&& lm.findViewByPosition(0).getTop()==rv.getPaddingTop();
|
||||
private boolean isScrolledToTop(View view){
|
||||
if(view instanceof RecyclerView rv){
|
||||
final LinearLayoutManager lm=(LinearLayoutManager) rv.getLayoutManager();
|
||||
return lm.findFirstVisibleItemPosition()==0
|
||||
&& lm.findViewByPosition(0).getTop()==rv.getPaddingTop();
|
||||
}
|
||||
return !view.canScrollVertically(-1);
|
||||
}
|
||||
|
||||
public void setScrollableChildSupplier(Supplier<RecyclerView> scrollableChildSupplier){
|
||||
public void setScrollableChildSupplier(Supplier<View> scrollableChildSupplier){
|
||||
this.scrollableChildSupplier=scrollableChildSupplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onScrollingHitEdge(float velocity){
|
||||
if(velocity>0 || takePriorityOverChildViews){
|
||||
RecyclerView view=scrollableChildSupplier.get();
|
||||
if(view!=null){
|
||||
return view.fling(0, (int) velocity);
|
||||
View view=scrollableChildSupplier==null ? null : scrollableChildSupplier.get();
|
||||
if(view instanceof RecyclerView rv){
|
||||
return rv.fling(0, (int) velocity);
|
||||
}else if(view instanceof ScrollView sv){
|
||||
if(sv.canScrollVertically((int)velocity)){
|
||||
sv.fling((int)velocity);
|
||||
return true;
|
||||
}
|
||||
}else if(view instanceof CustomScrollView sv){
|
||||
if(sv.canScrollVertically((int)velocity)){
|
||||
sv.fling((int)velocity);
|
||||
return true;
|
||||
}
|
||||
}else if(view instanceof WebView wv){
|
||||
if(wv.canScrollVertically((int)velocity)){
|
||||
wv.flingScroll(0, (int)velocity);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
|
|
@ -7,6 +7,7 @@ import org.joinmastodon.android.BuildConfig;
|
|||
|
||||
public abstract class GithubSelfUpdater{
|
||||
private static GithubSelfUpdater instance;
|
||||
public static boolean forceUpdate;
|
||||
|
||||
public static GithubSelfUpdater getInstance(){
|
||||
if(instance==null){
|
||||
|
@ -20,7 +21,7 @@ public abstract class GithubSelfUpdater{
|
|||
}
|
||||
|
||||
public static boolean needSelfUpdating(){
|
||||
return BuildConfig.BUILD_TYPE.equals("githubRelease");
|
||||
return BuildConfig.BUILD_TYPE.equals("githubRelease") || BuildConfig.BUILD_TYPE.equals("githubDebug");
|
||||
}
|
||||
|
||||
public abstract void maybeCheckForUpdates();
|
||||
|
@ -39,6 +40,8 @@ public abstract class GithubSelfUpdater{
|
|||
|
||||
public abstract void handleIntentFromInstaller(Intent intent, Activity activity);
|
||||
|
||||
public abstract void reset();
|
||||
|
||||
public enum UpdateState{
|
||||
NO_UPDATE,
|
||||
CHECKING,
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.animation.AnimatorListenerAdapter;
|
|||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.view.View;
|
||||
|
@ -27,6 +28,7 @@ public class ElevationOnScrollListener extends RecyclerView.OnScrollListener imp
|
|||
private Animator currentPanelsAnim;
|
||||
private List<View> views;
|
||||
private FragmentRootLinearLayout fragmentRootLayout;
|
||||
private Rect tmpRect=new Rect();
|
||||
|
||||
public ElevationOnScrollListener(FragmentRootLinearLayout fragmentRootLayout, View... views){
|
||||
this(fragmentRootLayout, Arrays.asList(views));
|
||||
|
@ -70,9 +72,14 @@ public class ElevationOnScrollListener extends RecyclerView.OnScrollListener imp
|
|||
}
|
||||
}
|
||||
|
||||
private int getRecyclerChildDecoratedTop(RecyclerView rv, View child){
|
||||
rv.getDecoratedBoundsWithMargins(child, tmpRect);
|
||||
return tmpRect.top;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
|
||||
boolean newAtTop=recyclerView.getChildCount()==0 || (recyclerView.getChildAdapterPosition(recyclerView.getChildAt(0))==0 && recyclerView.getChildAt(0).getTop()==recyclerView.getPaddingTop());
|
||||
boolean newAtTop=recyclerView.getChildCount()==0 || (recyclerView.getChildAdapterPosition(recyclerView.getChildAt(0))==0 && getRecyclerChildDecoratedTop(recyclerView, recyclerView.getChildAt(0))==recyclerView.getPaddingTop());
|
||||
handleScroll(recyclerView.getContext(), newAtTop);
|
||||
}
|
||||
|
||||
|
@ -120,4 +127,8 @@ public class ElevationOnScrollListener extends RecyclerView.OnScrollListener imp
|
|||
currentPanelsAnim=set;
|
||||
}
|
||||
}
|
||||
|
||||
public int getCurrentStatusBarColor(){
|
||||
return fragmentRootLayout.getStatusBarColor();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
package org.joinmastodon.android.utils;
|
||||
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Filter;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import java.util.List;
|
||||
|
@ -9,19 +10,19 @@ import java.util.function.Predicate;
|
|||
import java.util.stream.Collectors;
|
||||
|
||||
public class StatusFilterPredicate implements Predicate<Status>{
|
||||
private final List<Filter> filters;
|
||||
private final List<LegacyFilter> filters;
|
||||
|
||||
public StatusFilterPredicate(List<Filter> filters){
|
||||
public StatusFilterPredicate(List<LegacyFilter> filters){
|
||||
this.filters=filters;
|
||||
}
|
||||
|
||||
public StatusFilterPredicate(String accountID, Filter.FilterContext context){
|
||||
public StatusFilterPredicate(String accountID, FilterContext context){
|
||||
filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(context)).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean test(Status status){
|
||||
for(Filter filter:filters){
|
||||
for(LegacyFilter filter:filters){
|
||||
if(filter.matches(status))
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
package org.joinmastodon.android.utils;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
|
||||
public class ViewImageLoaderHolderTarget implements ViewImageLoader.Target{
|
||||
private final ImageLoaderViewHolder holder;
|
||||
private final int imageIndex;
|
||||
|
||||
public ViewImageLoaderHolderTarget(ImageLoaderViewHolder holder, int imageIndex){
|
||||
this.holder=holder;
|
||||
this.imageIndex=imageIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImageDrawable(Drawable d){
|
||||
if(d==null)
|
||||
holder.clearImage(imageIndex);
|
||||
else
|
||||
holder.setImage(imageIndex, d);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(){
|
||||
return ((RecyclerView.ViewHolder)holder).itemView;
|
||||
}
|
||||
}
|
|
@ -1,6 +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:interpolator="@interpolator/m3_sys_motion_easing_standard_decelerate"
|
||||
android:fromAlpha="1.0"
|
||||
android:toAlpha="0.0"/>
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="?colorM3Primary" android:state_selected="true"/>
|
||||
<item android:color="?colorM3OnSurfaceVariant"/>
|
||||
</selector>
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<stroke android:width="1dp" android:color="?colorM3OutlineVariant"/>
|
||||
<corners android:radius="12dp"/>
|
||||
</shape>
|
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp" android:tint="?colorM3OnPrimary"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||
</vector>
|
|
@ -0,0 +1,16 @@
|
|||
<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="M3,16V10.167C3,9.836 3.11,9.564 3.33,9.35C3.57,9.117 3.86,9 4.2,9H7.8C8.14,9 8.42,9.117 8.64,9.35C8.88,9.564 9,9.836 9,10.167V16H7.2V14.25H4.8V16H3ZM4.8,12.5H7.2V10.75H4.8V12.5Z"
|
||||
android:fillColor="#49454F"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M17.1,10.75V16H18.9V10.75H21V9H15V10.75H17.1Z"
|
||||
android:fillColor="#49454F"/>
|
||||
<path
|
||||
android:pathData="M10.2,9V16H15V14.25H12V9H10.2Z"
|
||||
android:fillColor="#49454F"/>
|
||||
</vector>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue