Merge branch 'donations'

This commit is contained in:
Grishka 2024-07-10 00:39:43 +03:00
commit ff7b6e4564
49 changed files with 2162 additions and 25 deletions

View File

@ -8,11 +8,11 @@ android {
generateLocaleConfig = true
}
compileSdk 33
compileSdk 34
defaultConfig {
applicationId "org.joinmastodon.android"
minSdk 23
targetSdk 33
targetSdk 34
versionCode 108
versionName "2.5.6"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@ -101,7 +101,7 @@ dependencies {
annotationProcessor 'org.parceler:parceler:1.1.12'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'
def appCenterSdkVersion = "4.4.2"
def appCenterSdkVersion = "5.0.4"
appcenterPrivateBetaImplementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"
appcenterPrivateBetaImplementation "com.microsoft.appcenter:appcenter-distribute:${appCenterSdkVersion}"
appcenterPublicBetaImplementation "com.microsoft.appcenter:appcenter-crashes:${appCenterSdkVersion}"

View File

@ -4,6 +4,7 @@
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE"/>
@ -79,6 +80,7 @@
<data android:mimeType="*/*"/>
</intent-filter>
</activity>
<activity android:name=".DonationFragmentActivity" android:exported="false" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize"/>
<service android:name=".AudioPlayerService" android:foregroundServiceType="mediaPlayback"/>
<service android:name=".NotificationActionHandlerService" android:exported="false"/>

View File

@ -88,8 +88,13 @@ public class AudioPlayerService extends Service{
nm=getSystemService(NotificationManager.class);
// registerReceiver(receiver, new IntentFilter(Intent.ACTION_MEDIA_BUTTON));
registerReceiver(receiver, new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY));
registerReceiver(receiver, new IntentFilter(ACTION_PLAY_PAUSE));
registerReceiver(receiver, new IntentFilter(ACTION_STOP));
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU){
registerReceiver(receiver, new IntentFilter(ACTION_PLAY_PAUSE), RECEIVER_EXPORTED);
registerReceiver(receiver, new IntentFilter(ACTION_STOP), RECEIVER_EXPORTED);
}else{
registerReceiver(receiver, new IntentFilter(ACTION_PLAY_PAUSE));
registerReceiver(receiver, new IntentFilter(ACTION_STOP));
}
instance=this;
}

View File

@ -0,0 +1,29 @@
package org.joinmastodon.android;
import android.os.Bundle;
import org.joinmastodon.android.fragments.DonationWebViewFragment;
import androidx.annotation.Nullable;
import me.grishka.appkit.FragmentStackActivity;
// This exists because our designer wanted to avoid extra sheet showing/hiding animations.
// This is the only way to show a fragment on top of a sheet without having to rewrite way too many things.
public class DonationFragmentActivity extends FragmentStackActivity{
@Override
protected void onCreate(@Nullable Bundle savedInstanceState){
super.onCreate(savedInstanceState);
if(savedInstanceState==null){
DonationWebViewFragment fragment=new DonationWebViewFragment();
fragment.setArguments(getIntent().getBundleExtra("fragmentArgs"));
showFragment(fragment);
overridePendingTransition(R.anim.fragment_enter, R.anim.no_op_300ms);
}
}
@Override
public void finish(){
super.finish();
overridePendingTransition(0, R.anim.fragment_exit);
}
}

View File

@ -24,7 +24,6 @@ import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.SearchResult;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.File;
import java.io.FileInputStream;
@ -45,8 +44,8 @@ import me.grishka.appkit.utils.WorkerThread;
public class CacheController{
private static final String TAG="CacheController";
private static final int DB_VERSION=3;
private static final WorkerThread databaseThread=new WorkerThread("databaseThread");
private static final Handler uiHandler=new Handler(Looper.getMainLooper());
public static final WorkerThread databaseThread=new WorkerThread("databaseThread");
public static final Handler uiHandler=new Handler(Looper.getMainLooper());
private final String accountID;
private DatabaseHelper db;
@ -467,9 +466,4 @@ public class CacheController{
db.execSQL("ALTER TABLE `notifications_mentions` ADD `time` INTEGER NOT NULL DEFAULT 0");
}
}
@FunctionalInterface
private interface DatabaseRunnable{
void run(SQLiteDatabase db) throws IOException;
}
}

View File

@ -0,0 +1,10 @@
package org.joinmastodon.android.api;
import android.database.sqlite.SQLiteDatabase;
import java.io.IOException;
@FunctionalInterface
public interface DatabaseRunnable{
void run(SQLiteDatabase db) throws IOException;
}

View File

@ -12,10 +12,12 @@ import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.api.gson.IsoInstantTypeAdapter;
import org.joinmastodon.android.api.gson.IsoLocalDateTypeAdapter;
import org.joinmastodon.android.api.session.AccountSession;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.time.Instant;
@ -29,6 +31,8 @@ import java.util.concurrent.TimeUnit;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import me.grishka.appkit.utils.WorkerThread;
import okhttp3.Cache;
import okhttp3.CacheControl;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
@ -49,8 +53,11 @@ public class MastodonAPIController{
.connectTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.cache(new Cache(new File(MastodonApp.context.getCacheDir(), "http"), 10*1024*1024))
.build();
private static final CacheControl NO_CACHE_WHATSOEVER=new CacheControl.Builder().noCache().noStore().build();
private AccountSession session;
static{
@ -80,6 +87,9 @@ public class MastodonAPIController{
if(token!=null)
builder.header("Authorization", "Bearer "+token);
if(!req.cacheable)
builder.cacheControl(NO_CACHE_WHATSOEVER);
if(req.headers!=null){
for(Map.Entry<String, String> header:req.headers.entrySet()){
builder.header(header.getKey(), header.getValue());

View File

@ -46,6 +46,7 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
boolean canceled;
Map<String, String> headers;
long timeout;
boolean cacheable;
private ProgressDialog progressDialog;
protected boolean removeUnsupportedItems;
@ -132,6 +133,10 @@ public abstract class MastodonAPIRequest<T> extends APIRequest<T>{
this.timeout=timeout;
}
protected void setCacheable(){
cacheable=true;
}
protected String getPathPrefix(){
return "/api/v1";
}

View File

@ -0,0 +1,40 @@
package org.joinmastodon.android.api.requests.catalog;
import android.net.Uri;
import android.text.TextUtils;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.donations.DonationCampaign;
public class GetDonationCampaigns extends MastodonAPIRequest<DonationCampaign>{
private final String locale, seed, source;
private boolean staging;
public GetDonationCampaigns(String locale, String seed, String source){
super(HttpMethod.GET, null, DonationCampaign.class);
this.locale=locale;
this.seed=seed;
this.source=source;
setCacheable();
}
public void setStaging(boolean staging){
this.staging=staging;
}
@Override
public Uri getURL(){
Uri.Builder builder=new Uri.Builder()
.scheme("https")
.authority("api.joinmastodon.org")
.path("/v1/donations/campaigns/active")
.appendQueryParameter("platform", "android")
.appendQueryParameter("locale", locale)
.appendQueryParameter("seed", seed);
if(staging)
builder.appendQueryParameter("environment", "staging");
if(!TextUtils.isEmpty(source))
builder.appendQueryParameter("source", source);
return builder.build();
}
}

View File

@ -33,7 +33,8 @@ import org.joinmastodon.android.model.TimelineMarkers;
import org.joinmastodon.android.model.Token;
import org.joinmastodon.android.utils.ObjectIdComparator;
import java.io.File;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
@ -44,6 +45,7 @@ import me.grishka.appkit.api.ErrorResponse;
public class AccountSession{
private static final String TAG="AccountSession";
private static final int MIN_DAYS_ACCOUNT_AGE_FOR_DONATIONS=28;
public Token token;
public Account self;
@ -276,4 +278,12 @@ public class AccountSession{
public void setNotificationsMentionsOnly(boolean mentionsOnly){
getRawLocalPreferences().edit().putBoolean("notificationsMentionsOnly", mentionsOnly).apply();
}
public boolean isEligibleForDonations(){
return ("mastodon.social".equalsIgnoreCase(domain) || "mastodon.online".equalsIgnoreCase(domain)) && self.createdAt.isBefore(Instant.now().minus(MIN_DAYS_ACCOUNT_AGE_FOR_DONATIONS, ChronoUnit.DAYS));
}
public int getDonationSeed(){
return Math.abs(getFullUsername().hashCode())%100;
}
}

View File

@ -3,11 +3,16 @@ package org.joinmastodon.android.api.session;
import android.app.Activity;
import android.app.NotificationManager;
import android.content.ComponentName;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Build;
@ -18,11 +23,13 @@ import org.joinmastodon.android.E;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.CacheController;
import org.joinmastodon.android.api.DatabaseRunnable;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.PushSubscriptionManager;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
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;
import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp;
import org.joinmastodon.android.events.EmojiUpdatedEvent;
@ -30,9 +37,10 @@ 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.LegacyFilter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Token;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.File;
import java.io.FileInputStream;
@ -60,6 +68,7 @@ public class AccountSessionManager{
private static final String TAG="AccountSessionManager";
public static final String SCOPE="read write follow push";
public static final String REDIRECT_URI="mastodon-android-auth://callback";
private static final int DB_VERSION=1;
private static final AccountSessionManager instance=new AccountSessionManager();
@ -73,6 +82,8 @@ public class AccountSessionManager{
private String lastActiveAccountID;
private SharedPreferences prefs;
private boolean loadedInstances;
private DatabaseHelper db;
private final Runnable databaseCloseRunnable=this::closeDatabase;
public static AccountSessionManager getInstance(){
return instance;
@ -450,6 +461,68 @@ public class AccountSessionManager{
}
}
private void closeDelayed(){
CacheController.databaseThread.postRunnable(databaseCloseRunnable, 10_000);
}
public void closeDatabase(){
if(db!=null){
if(BuildConfig.DEBUG)
Log.d(TAG, "closeDatabase");
db.close();
db=null;
}
}
private void cancelDelayedClose(){
if(db!=null){
CacheController.databaseThread.handler.removeCallbacks(databaseCloseRunnable);
}
}
private SQLiteDatabase getOrOpenDatabase(){
if(db==null)
db=new DatabaseHelper();
return db.getWritableDatabase();
}
private void runOnDbThread(DatabaseRunnable r){
cancelDelayedClose();
CacheController.databaseThread.postRunnable(()->{
try{
SQLiteDatabase db=getOrOpenDatabase();
r.run(db);
}catch(SQLiteException|IOException x){
Log.w(TAG, x);
}finally{
closeDelayed();
}
}, 0);
}
public void runIfDonationCampaignNotDismissed(String id, Runnable action){
runOnDbThread(db->{
try(Cursor cursor=db.query("dismissed_donation_campaigns", null, "id=?", new String[]{id}, null, null, null)){
if(!cursor.moveToFirst()){
UiUtils.runOnUiThread(action);
}
}
});
}
public void markDonationCampaignAsDismissed(String id){
runOnDbThread(db->{
ContentValues values=new ContentValues();
values.put("id", id);
values.put("dismissed_at", System.currentTimeMillis());
db.insert("dismissed_donation_campaigns", null, values);
});
}
public void clearDismissedDonationCampaigns(){
runOnDbThread(db->db.delete("dismissed_donation_campaigns", null, null));
}
private static class SessionsStorageWrapper{
public List<AccountSession> accounts;
}
@ -459,4 +532,24 @@ public class AccountSessionManager{
public List<Emoji> emojis;
public long lastUpdated;
}
private static class DatabaseHelper extends SQLiteOpenHelper{
public DatabaseHelper(){
super(MastodonApp.context, "accounts.db", null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db){
db.execSQL("""
CREATE TABLE `dismissed_donation_campaigns` (
`id` text PRIMARY KEY,
`dismissed_at` bigint
)""");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
}
}
}

View File

@ -0,0 +1,9 @@
package org.joinmastodon.android.events;
public class DismissDonationCampaignBannerEvent{
public final String campaignID;
public DismissDonationCampaignBannerEvent(String campaignID){
this.campaignID=campaignID;
}
}

View File

@ -0,0 +1,96 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.webkit.WebResourceRequest;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.DismissDonationCampaignBannerEvent;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import java.util.Objects;
import me.grishka.appkit.Nav;
public class DonationWebViewFragment extends WebViewFragment{
public static final String SUCCESS_URL="https://sponsor.joinmastodon.org/donate/success";
public static final String FAILURE_URL="https://sponsor.joinmastodon.org/donate/failure";
public static final String CANCEL_URL="https://sponsor.joinmastodon.org/donate/cancel";
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
if(BuildConfig.DEBUG){
setHasOptionsMenu(true);
}
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
webView.loadUrl(Objects.requireNonNull(getArguments().getString("url")));
}
@Override
protected boolean shouldOverrideUrlLoading(WebResourceRequest req){
String url=req.getUrl().buildUpon().clearQuery().fragment(null).build().toString();
if(url.equalsIgnoreCase(SUCCESS_URL)){
onSuccess();
return true;
}else if(url.equalsIgnoreCase(FAILURE_URL)){
onFailure();
return true;
}else if(url.equalsIgnoreCase(CANCEL_URL)){
onCancel();
return true;
}
return false;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
super.onCreateOptionsMenu(menu, inflater);
if(BuildConfig.DEBUG){
menu.add(0, 0, 0, "Simulate success");
menu.add(0, 1, 0, "Simulate failure");
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
if(item.getItemId()==0)
onSuccess();
else if(item.getItemId()==1)
onFailure();
return super.onOptionsItemSelected(item);
}
private void onFailure(){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(R.string.donation_server_error)
.setPositiveButton(R.string.ok, null)
.setOnDismissListener(dlg->Nav.finish(this))
.show();
}
private void onSuccess(){
String campaignID=getArguments().getString("campaignID");
AccountSessionManager.getInstance().markDonationCampaignAsDismissed(campaignID);
E.post(new DismissDonationCampaignBannerEvent(campaignID));
getActivity().setResult(Activity.RESULT_OK, new Intent().putExtra("postText", getArguments().getString("successPostText")));
getActivity().finish();
}
private void onCancel(){
getActivity().finish();
}
}

View File

@ -5,15 +5,23 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.text.style.TypefaceSpan;
import android.text.style.UnderlineSpan;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.animation.AnimationUtils;
import android.widget.Button;
@ -26,14 +34,17 @@ import android.widget.Toolbar;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.catalog.GetDonationCampaigns;
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
import org.joinmastodon.android.api.requests.timelines.GetListTimeline;
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.DismissDonationCampaignBannerEvent;
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
import org.joinmastodon.android.model.CacheablePaginatedResponse;
@ -41,8 +52,11 @@ import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.FollowList;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.TimelineMarkers;
import org.joinmastodon.android.model.donations.DonationCampaign;
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.sheets.DonationSheet;
import org.joinmastodon.android.ui.sheets.DonationSuccessfulSheet;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.viewcontrollers.HomeTimelineMenuController;
import org.joinmastodon.android.ui.viewcontrollers.ToolbarDropdownMenuController;
@ -53,6 +67,7 @@ import org.parceler.Parcels;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import androidx.annotation.NonNull;
@ -64,8 +79,11 @@ import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.BottomSheet;
public class HomeTimelineFragment extends StatusListFragment implements ToolbarDropdownMenuController.HostFragment{
private static final int DONATION_RESULT=211;
private ImageButton fab;
private LinearLayout listsDropdown;
private FixedAspectRatioImageView listsDropdownArrow;
@ -81,9 +99,13 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
private FollowList currentList;
private MergeRecyclerAdapter mergeAdapter;
private DiscoverInfoBannerHelper localTimelineBannerHelper;
private View donationBanner;
private boolean donationBannerDismissing;
private String maxID;
private String lastSavedMarkerID;
private DonationCampaign currentDonationCampaign;
private BottomSheet donationSheet;
public HomeTimelineFragment(){
setListLayoutId(R.layout.fragment_timeline);
@ -93,6 +115,32 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
localTimelineBannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE, accountID);
if(AccountSessionManager.get(accountID).isEligibleForDonations()){
GetDonationCampaigns req=new GetDonationCampaigns(Locale.getDefault().toLanguageTag().replace('-', '_'), String.valueOf(AccountSessionManager.get(accountID).getDonationSeed()), null);
if(getActivity().getSharedPreferences("debug", Context.MODE_PRIVATE).getBoolean("donationsStaging", false)){
req.setStaging(true);
}
req.setCallback(new Callback<>(){
@Override
public void onSuccess(DonationCampaign result){
if(result==null)
return;
AccountSessionManager.getInstance().runIfDonationCampaignNotDismissed(result.id, ()->showDonationBanner(result));
}
@Override
public void onError(ErrorResponse error){}
})
.execNoAuth("");
}
E.register(this);
}
@Override
public void onDestroy(){
super.onDestroy();
E.unregister(this);
}
@Override
@ -233,6 +281,8 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
E.register(this);
updateUpdateState(GithubSelfUpdater.getInstance().getState());
}
if(currentDonationCampaign!=null)
showDonationBanner(currentDonationCampaign);
}
@Override
@ -587,6 +637,8 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
if(GithubSelfUpdater.needSelfUpdating()){
E.unregister(this);
}
donationBanner=null;
donationBannerDismissing=false;
}
private void updateUpdateState(GithubSelfUpdater.UpdateState state){
@ -599,6 +651,13 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
updateUpdateState(ev.state);
}
@Subscribe
public void onDismissDonationCampaignBanner(DismissDonationCampaignBannerEvent ev){
if(currentDonationCampaign!=null && ev.campaignID.equals(currentDonationCampaign.id)){
dismissDonationBanner();
}
}
@Override
protected boolean shouldRemoveAccountPostsWhenUnfollowing(){
return true;
@ -653,6 +712,17 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
super.onDataLoaded(d, more);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data){
if(requestCode==DONATION_RESULT){
if(donationSheet!=null)
donationSheet.dismissWithoutAnimation();
if(resultCode==Activity.RESULT_OK){
new DonationSuccessfulSheet(getActivity(), accountID, data.getStringExtra("postText")).showWithoutAnimation();
}
}
}
private String getCurrentListTitle(){
return switch(listMode){
case FOLLOWING -> getString(R.string.timeline_following);
@ -661,6 +731,77 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
};
}
private void showDonationBanner(DonationCampaign campaign){
if(getActivity()==null)
return;
currentDonationCampaign=campaign;
if(donationBanner==null){
ViewStub stub=contentView.findViewById(R.id.donation_banner);
donationBanner=stub.inflate();
donationBanner.findViewById(R.id.banner_dismiss).setOnClickListener(v->{
AccountSessionManager.getInstance().markDonationCampaignAsDismissed(currentDonationCampaign.id);
dismissDonationBanner();
});
donationBanner.setOnClickListener(v->openDonationSheet());
}else{
donationBanner.setVisibility(View.VISIBLE);
}
TextView text=donationBanner.findViewById(R.id.banner_text);
SpannableStringBuilder ssb=new SpannableStringBuilder(campaign.bannerMessage);
ssb.append(' ');
int start=ssb.length();
ssb.append(campaign.bannerButtonText);
ssb.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.masterialDark_colorGoldenrodContainer, getActivity().getTheme())), start, ssb.length(), 0);
ssb.setSpan(new UnderlineSpan(), start, ssb.length(), 0);
ssb.setSpan(new TypefaceSpan("sans-serif-medium"), start, ssb.length(), 0);
text.setText(ssb);
donationBanner.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
donationBanner.getViewTreeObserver().removeOnPreDrawListener(this);
AnimatorSet set=new AnimatorSet();
set.playTogether(
ObjectAnimator.ofFloat(donationBanner, View.TRANSLATION_Y, donationBanner.getHeight(), 0),
ObjectAnimator.ofFloat(fab, View.TRANSLATION_Y, -donationBanner.getHeight())
);
set.setDuration(250);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.start();
return true;
}
});
}
private void dismissDonationBanner(){
if(donationBanner==null || donationBannerDismissing)
return;
AnimatorSet set=new AnimatorSet();
set.playTogether(
ObjectAnimator.ofFloat(donationBanner, View.TRANSLATION_Y, donationBanner.getHeight()),
ObjectAnimator.ofFloat(fab, View.TRANSLATION_Y, 0)
);
set.setDuration(250);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
donationBanner.setVisibility(View.GONE);
donationBannerDismissing=false;
}
});
donationBannerDismissing=true;
set.start();
currentDonationCampaign=null;
}
private void openDonationSheet(){
donationSheet=new DonationSheet(getActivity(), currentDonationCampaign, accountID, intent->startActivityForResult(intent, DONATION_RESULT));
donationSheet.setOnDismissListener(dialog->donationSheet=null);
donationSheet.show();
}
private enum ListMode{
FOLLOWING,
LOCAL,

View File

@ -0,0 +1,100 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceError;
import android.webkit.WebResourceRequest;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.api.MastodonErrorResponse;
import me.grishka.appkit.Nav;
import me.grishka.appkit.fragments.LoaderFragment;
public abstract class WebViewFragment extends LoaderFragment{
private static final String TAG="WebViewFragment";
protected WebView webView;
private Runnable backCallback=this::onGoBack;
private boolean backCallbackSet;
@Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
webView=new WebView(getActivity());
webView.setWebChromeClient(new WebChromeClient(){
@Override
public void onReceivedTitle(WebView view, String title){
setTitle(title);
}
});
webView.setWebViewClient(new WebViewClient(){
@Override
public void onPageFinished(WebView view, String url){
if(BuildConfig.DEBUG){
Log.d(TAG, "onPageFinished: "+url);
}
dataLoaded();
updateBackCallback();
}
@Override
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error){
onError(new MastodonErrorResponse(error.getDescription().toString(), -1, null));
updateBackCallback();
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request){
return WebViewFragment.this.shouldOverrideUrlLoading(request);
}
@Override
public void doUpdateVisitedHistory(WebView view, String url, boolean isReload){
updateBackCallback();
}
});
webView.getSettings().setJavaScriptEnabled(true);
return webView;
}
@Override
protected void doLoadData(){
}
@Override
public void onRefresh(){
webView.reload();
}
@Override
public void onToolbarNavigationClick(){
Nav.finish(this);
}
private void updateBackCallback(){
boolean canGoBack=webView.canGoBack();
if(canGoBack!=backCallbackSet){
if(canGoBack){
addBackCallback(backCallback);
backCallbackSet=true;
}else{
removeBackCallback(backCallback);
backCallbackSet=false;
}
}
}
private void onGoBack(){
if(webView.canGoBack())
webView.goBack();
}
protected abstract boolean shouldOverrideUrlLoading(WebResourceRequest req);
}

View File

@ -1,5 +1,7 @@
package org.joinmastodon.android.fragments.settings;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.widget.Toast;
@ -9,6 +11,7 @@ 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.CheckableListItem;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.updater.GithubSelfUpdater;
@ -18,6 +21,8 @@ import java.util.List;
import me.grishka.appkit.Nav;
public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
private CheckableListItem<Void> donationsStagingItem;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@ -28,7 +33,9 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
selfUpdateItem=new ListItem<>("Force self-update", null, this::onForceSelfUpdateClick),
resetUpdateItem=new ListItem<>("Reset self-updater", null, this::onResetUpdaterClick),
new ListItem<>("Reset search info banners", null, this::onResetDiscoverBannersClick),
new ListItem<>("Reset pre-reply sheets", null, this::onResetPreReplySheetsClick)
new ListItem<>("Reset pre-reply sheets", null, this::onResetPreReplySheetsClick),
new ListItem<>("Clear dismissed donation campaigns", null, this::onClearDismissedCampaignsClick),
donationsStagingItem=new CheckableListItem<>("Use staging environment for donations", null, CheckableListItem.Style.SWITCH, getPrefs().getBoolean("donationsStaging", false), this::toggleCheckableItem)
));
if(!GithubSelfUpdater.needSelfUpdating()){
resetUpdateItem.isEnabled=selfUpdateItem.isEnabled=false;
@ -39,6 +46,12 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
@Override
protected void doLoadData(int offset, int count){}
@Override
public void onStop(){
super.onStop();
getPrefs().edit().putBoolean("donationsStaging", donationsStagingItem.checked).apply();
}
private void onTestEmailConfirmClick(ListItem<?> item){
AccountSession sess=AccountSessionManager.getInstance().getAccount(accountID);
sess.activated=false;
@ -70,9 +83,18 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
Toast.makeText(getActivity(), "Pre-reply sheets were reset", Toast.LENGTH_SHORT).show();
}
private void onClearDismissedCampaignsClick(ListItem<?> item){
AccountSessionManager.getInstance().clearDismissedDonationCampaigns();
Toast.makeText(getActivity(), "Dismissed campaigns cleared. Restart app to see your current campaign, if any", Toast.LENGTH_LONG).show();
}
private void restartUI(){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.goClearingStack(getActivity(), HomeFragment.class, args);
}
private SharedPreferences getPrefs(){
return getActivity().getSharedPreferences("debug", Context.MODE_PRIVATE);
}
}

View File

@ -1,10 +1,14 @@
package org.joinmastodon.android.fragments.settings;
import android.app.Activity;
import android.content.Context;
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 android.widget.Toast;
import com.squareup.otto.Subscribe;
@ -12,27 +16,38 @@ 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.requests.catalog.GetDonationCampaigns;
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.donations.DonationCampaign;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
import org.joinmastodon.android.ui.sheets.DonationSheet;
import org.joinmastodon.android.ui.sheets.DonationSuccessfulSheet;
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
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.utils.MergeRecyclerAdapter;
public class SettingsMainFragment extends BaseSettingsFragment<Void>{
private static final int DONATION_RESULT=433;
private boolean loggedOut;
private HideableSingleViewRecyclerAdapter bannerAdapter;
private Button updateButton1, updateButton2;
private TextView updateText;
private DonationSheet donationSheet;
private Runnable updateDownloadProgressUpdater=new Runnable(){
@Override
public void run(){
@ -49,21 +64,26 @@ public class SettingsMainFragment extends BaseSettingsFragment<Void>{
super.onCreate(savedInstanceState);
setTitle(R.string.settings);
setSubtitle(AccountSessionManager.get(accountID).getFullUsername());
onDataLoaded(List.of(
ArrayList<ListItem<Void>> items=new ArrayList<>();
if(BuildConfig.DEBUG || BuildConfig.BUILD_TYPE.equals("appcenterPrivateBeta")){
items.add(new ListItem<>("Debug settings", null, R.drawable.ic_settings_24px, i->Nav.go(getActivity(), SettingsDebugFragment.class, makeFragmentArgs()), null, 0, true));
}
items.addAll(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_privacy, 0, R.drawable.ic_privacy_tip_24px, this::onPrivacyClick),
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<>(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.manage_accounts, 0, R.drawable.ic_switch_account_24px, this::onManageAccountsClick),
new ListItem<>(R.string.log_out, 0, R.drawable.ic_logout_24px, this::onLogOutClick, R.attr.colorM3Error, false)
new ListItem<>(getString(R.string.about_app, getString(R.string.app_name)), null, R.drawable.ic_info_24px, this::onAboutClick, null, 0, true)
));
if(BuildConfig.DEBUG || BuildConfig.BUILD_TYPE.equals("appcenterPrivateBeta")){
data.add(0, new ListItem<>("Debug settings", null, R.drawable.ic_settings_24px, i->Nav.go(getActivity(), SettingsDebugFragment.class, makeFragmentArgs()), null, 0, true));
if(AccountSessionManager.get(accountID).isEligibleForDonations()){
items.add(new ListItem<>(R.string.settings_donate, 0, R.drawable.ic_volunteer_activism_24px, this::onDonateClick));
items.add(new ListItem<>(R.string.settings_manage_donations, 0, R.drawable.ic_settings_heart_24px, this::onManageDonationClick, 0, true));
}
items.add(new ListItem<>(R.string.manage_accounts, 0, R.drawable.ic_switch_account_24px, this::onManageAccountsClick));
items.add(new ListItem<>(R.string.log_out, 0, R.drawable.ic_logout_24px, this::onLogOutClick, R.attr.colorM3Error, false));
onDataLoaded(items);
AccountSession session=AccountSessionManager.get(accountID);
session.reloadPreferences(null);
@ -117,6 +137,17 @@ public class SettingsMainFragment extends BaseSettingsFragment<Void>{
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data){
if(requestCode==DONATION_RESULT){
if(donationSheet!=null)
donationSheet.dismissWithoutAnimation();
if(resultCode==Activity.RESULT_OK){
new DonationSuccessfulSheet(getActivity(), accountID, data.getStringExtra("postText")).showWithoutAnimation();
}
}
}
private Bundle makeFragmentArgs(){
Bundle args=new Bundle();
args.putString("account", accountID);
@ -167,6 +198,39 @@ public class SettingsMainFragment extends BaseSettingsFragment<Void>{
.show();
}
private void onDonateClick(ListItem<?> item){
GetDonationCampaigns req=new GetDonationCampaigns(Locale.getDefault().toLanguageTag().replace('-', '_'), String.valueOf(AccountSessionManager.get(accountID).getDonationSeed()), null);
if(getActivity().getSharedPreferences("debug", Context.MODE_PRIVATE).getBoolean("donationsStaging", false)){
req.setStaging(true);
}
req.setCallback(new Callback<>(){
@Override
public void onSuccess(DonationCampaign result){
Activity activity=getActivity();
if(activity==null)
return;
if(result==null){
Toast.makeText(activity, "No campaign available (server misconfiguration?)", Toast.LENGTH_SHORT).show();
return;
}
donationSheet=new DonationSheet(getActivity(), result, accountID, intent->startActivityForResult(intent, DONATION_RESULT));
donationSheet.setOnDismissListener(dialog->donationSheet=null);
donationSheet.show();
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, true)
.execNoAuth("");
}
private void onManageDonationClick(ListItem<?> item){
UiUtils.launchWebBrowser(getActivity(), "https://sponsor.staging.joinmastodon.org/donate/manage");
}
@Subscribe
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
updateUpdateBanner();

View File

@ -0,0 +1,34 @@
package org.joinmastodon.android.model.donations;
import org.joinmastodon.android.api.AllFieldsAreRequired;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.model.BaseModel;
import java.util.Map;
@AllFieldsAreRequired
public class DonationCampaign extends BaseModel{
public String id;
public String bannerMessage;
public String bannerButtonText;
public String donationMessage;
public String donationButtonText;
public Amounts amounts;
public String defaultCurrency;
public String donationUrl;
public String donationSuccessPost;
@Override
public void postprocess() throws ObjectValidationException{
super.postprocess();
amounts.postprocess();
}
public static class Amounts extends BaseModel{
public Map<String, long[]> oneTime;
@RequiredField
public Map<String, long[]> monthly;
public Map<String, long[]> yearly;
}
}

View File

@ -0,0 +1,353 @@
package org.joinmastodon.android.ui.sheets;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ToggleButton;
import org.joinmastodon.android.DonationFragmentActivity;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.DonationWebViewFragment;
import org.joinmastodon.android.model.donations.DonationCampaign;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.CurrencyAmountInput;
import java.text.NumberFormat;
import java.util.Arrays;
import java.util.Currency;
import java.util.List;
import java.util.Locale;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import me.grishka.appkit.utils.CustomViewHelper;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.BottomSheet;
public class DonationSheet extends BottomSheet{
private final DonationCampaign campaign;
private final String accountID;
private final Consumer<Intent> startCallback;
private DonationFrequency frequency=DonationFrequency.MONTHLY;
private View onceTab, monthlyTab, yearlyTab;
private int currentTab;
private CurrencyAmountInput amountField;
private ToggleButton[] suggestedAmountButtons=new ToggleButton[6];
private View button;
private TextView buttonText;
private Activity activity;
public DonationSheet(@NonNull Activity activity, DonationCampaign campaign, String accountID, Consumer<Intent> startCallback){
super(activity);
this.campaign=campaign;
this.accountID=accountID;
this.activity=activity;
this.startCallback=startCallback;
Context context=activity;
View content=context.getSystemService(LayoutInflater.class).inflate(R.layout.sheet_donation, null);
setContentView(content);
setNavigationBarBackground(new ColorDrawable(UiUtils.alphaBlendColors(UiUtils.getThemeColor(context, R.attr.colorM3Surface),
UiUtils.getThemeColor(context, R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
TextView text=findViewById(R.id.text);
text.setText(campaign.donationMessage);
onceTab=findViewById(R.id.once);
monthlyTab=findViewById(R.id.monthly);
yearlyTab=findViewById(R.id.yearly);
onceTab.setOnClickListener(this::onTabClick);
monthlyTab.setOnClickListener(this::onTabClick);
yearlyTab.setOnClickListener(this::onTabClick);
if(campaign.amounts.yearly==null)
yearlyTab.setVisibility(View.GONE);
if(campaign.amounts.oneTime==null)
onceTab.setVisibility(View.GONE);
if(campaign.amounts.monthly==null){
monthlyTab.setVisibility(View.GONE);
if(campaign.amounts.oneTime!=null){
onceTab.setSelected(true);
currentTab=R.id.once;
frequency=DonationFrequency.ONCE;
}else if(campaign.amounts.yearly!=null){
yearlyTab.setSelected(true);
currentTab=R.id.yearly;
frequency=DonationFrequency.YEARLY;
}else{
Toast.makeText(context, "Amounts object is empty", Toast.LENGTH_SHORT).show();
dismiss();
return;
}
}else{
monthlyTab.setSelected(true);
currentTab=R.id.monthly;
}
View tabBarItself=findViewById(R.id.tabbar_inner);
tabBarItself.setOutlineProvider(OutlineProviders.roundedRect(20));
tabBarItself.setClipToOutline(true);
amountField=findViewById(R.id.amount);
List<String> availableCurrencies=campaign.amounts.monthly.keySet().stream().sorted().collect(Collectors.toList());
amountField.setCurrencies(availableCurrencies);
try{
amountField.setSelectedCurrency(campaign.defaultCurrency);
}catch(IllegalArgumentException x){
new M3AlertDialogBuilder(context)
.setTitle(R.string.error)
.setMessage("Default currency "+campaign.defaultCurrency+" not in list of available currencies "+availableCurrencies)
.show();
dismiss();
return;
}
amountField.setChangeListener(new CurrencyAmountInput.ChangeListener(){
@Override
public void onCurrencyChanged(String code){
updateSuggestedAmounts(code);
button.setEnabled(amountField.getAmount()>=getMinimumChargeAmount(code));
updateSuggestedButtonsState();
}
@Override
public void onAmountChanged(long amount){
button.setEnabled(amount>=getMinimumChargeAmount(amountField.getCurrency()));
updateSuggestedButtonsState();
}
});
button=findViewById(R.id.button);
buttonText=findViewById(R.id.button_text);
ViewGroup suggestedAmounts=findViewById(R.id.suggested_amounts);
for(int i=0;i<suggestedAmountButtons.length;i++){
ToggleButton btn=new ToggleButton(context);
btn.setBackgroundResource(R.drawable.bg_filter_chip);
btn.setTextAppearance(R.style.m3_label_large);
btn.setTextColor(context.getResources().getColorStateList(R.color.filter_chip_text, context.getTheme()));
btn.setMinWidth(V.dp(64));
btn.setMinimumWidth(0);
int pad=V.dp(16);
btn.setPadding(pad, 0, pad, 0);
btn.setStateListAnimator(null);
btn.setTextOff(null);
btn.setTextOn(null);
btn.setOnClickListener(this::onSuggestedAmountClick);
btn.setTag(i);
suggestedAmountButtons[i]=btn;
suggestedAmounts.addView(btn);
}
updateSuggestedAmounts(campaign.defaultCurrency);
button.setEnabled(false);
buttonText.setText(campaign.donationButtonText);
button.setOnClickListener(v->openWebView());
Arrays.stream(getCurrentSuggestedAmounts(campaign.defaultCurrency)).min().ifPresent(amountField::setAmount);
}
@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
Window window=getWindow();
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
}
private void onTabClick(View v){
if(v.getId()==currentTab)
return;
findViewById(currentTab).setSelected(false);
v.setSelected(true);
currentTab=v.getId();
if(currentTab==R.id.once)
frequency=DonationFrequency.ONCE;
else if(currentTab==R.id.monthly)
frequency=DonationFrequency.MONTHLY;
else if(currentTab==R.id.yearly)
frequency=DonationFrequency.YEARLY;
updateSuggestedAmounts(amountField.getCurrency());
}
private long[] getCurrentSuggestedAmounts(String currency){
long[] amounts=(switch(frequency){
case ONCE -> campaign.amounts.oneTime;
case MONTHLY -> campaign.amounts.monthly;
case YEARLY -> campaign.amounts.yearly;
}).get(currency);
if(amounts==null){
amounts=new long[0];
}
return amounts;
}
private void updateSuggestedAmounts(String currency){
NumberFormat format=NumberFormat.getCurrencyInstance();
try{
format.setCurrency(Currency.getInstance(currency));
}catch(IllegalArgumentException ignore){}
int defaultFractionDigits=format.getMinimumFractionDigits();
long[] amounts=getCurrentSuggestedAmounts(currency);
for(int i=0;i<suggestedAmountButtons.length;i++){
ToggleButton btn=suggestedAmountButtons[i];
if(i>=amounts.length){
btn.setVisibility(View.GONE);
continue;
}
btn.setVisibility(View.VISIBLE);
long amount=amounts[i];
format.setMinimumFractionDigits(amount%100==0 ? 0 : defaultFractionDigits);
btn.setText(format.format(amount/100.0));
}
updateSuggestedButtonsState();
}
private void onSuggestedAmountClick(View v){
int index=(int) v.getTag();
long[] amounts=getCurrentSuggestedAmounts(amountField.getCurrency());
amountField.setAmount(amounts[index]);
}
private void updateSuggestedButtonsState(){
long amount=amountField.getAmount();
long[] amounts=getCurrentSuggestedAmounts(amountField.getCurrency());
for(int i=0;i<Math.min(amounts.length, suggestedAmountButtons.length);i++){
ToggleButton btn=suggestedAmountButtons[i];
btn.setChecked(amounts[i]==amount);
}
}
private void openWebView(){
Uri.Builder builder=Uri.parse(campaign.donationUrl).buildUpon();
builder.appendQueryParameter("locale", Locale.getDefault().toLanguageTag().replace('-', '_'))
.appendQueryParameter("platform", "android")
.appendQueryParameter("currency", amountField.getCurrency())
.appendQueryParameter("amount", String.valueOf(amountField.getAmount()))
.appendQueryParameter("source", "campaign")
.appendQueryParameter("campaign_id", campaign.id)
.appendQueryParameter("frequency", switch(frequency){
case ONCE -> "one_time";
case MONTHLY -> "monthly";
case YEARLY -> "yearly";
})
.appendQueryParameter("success_callback_url", DonationWebViewFragment.SUCCESS_URL)
.appendQueryParameter("cancel_callback_url", DonationWebViewFragment.CANCEL_URL)
.appendQueryParameter("failure_callback_url", DonationWebViewFragment.FAILURE_URL);
Bundle args=new Bundle();
args.putString("url", builder.build().toString());
args.putString("account", accountID);
args.putString("campaignID", campaign.id);
args.putString("successPostText", campaign.donationSuccessPost);
args.putBoolean("_can_go_back", true);
startCallback.accept(new Intent(activity, DonationFragmentActivity.class).putExtra("fragmentArgs", args));
}
private static long getMinimumChargeAmount(String currency){
// https://docs.stripe.com/currencies#minimum-and-maximum-charge-amounts
// values are in cents
return switch(currency){
case "USD" -> 50;
case "AED" -> 2_00;
case "AUD" -> 50;
case "BGN" -> 1_00;
case "BRL" -> 50;
case "CAD" -> 50;
case "CHF" -> 50;
case "CZK" -> 15_00;
case "DKK" -> 2_50;
case "EUR" -> 50;
case "GBP" -> 30;
case "HKD" -> 4_00;
case "HUF" -> 175_00;
case "INR" -> 50;
case "JPY" -> 50_00;
case "MXN" -> 10_00;
case "MYR" -> 2_00;
case "NOK" -> 3_00;
case "NZD" -> 50;
case "PLN" -> 2_00;
case "RON" -> 2_00;
case "SEK" -> 3_00;
case "SGD" -> 50;
case "THB" -> 10_00;
default -> 50;
};
}
private enum DonationFrequency{
ONCE,
MONTHLY,
YEARLY
}
public static class SuggestedAmountsLayout extends ViewGroup implements CustomViewHelper{
private int visibleChildCount;
private static final int H_GAP=24;
private static final int V_GAP=8;
private static final int ROW_HEIGHT=32;
public SuggestedAmountsLayout(Context context){
this(context, null);
}
public SuggestedAmountsLayout(Context context, AttributeSet attrs){
this(context, attrs, 0);
}
public SuggestedAmountsLayout(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
visibleChildCount=0;
for(int i=0;i<getChildCount();i++){
View child=getChildAt(i);
if(child.getVisibility()==GONE)
continue;
visibleChildCount++;
}
int width=MeasureSpec.getSize(widthMeasureSpec);
setMeasuredDimension(width, visibleChildCount>4 ? dp(ROW_HEIGHT*2+V_GAP) : dp(ROW_HEIGHT));
int buttonsPerRow=visibleChildCount>4 ? 3 : visibleChildCount;
int buttonWidth=(width-dp(H_GAP)*(buttonsPerRow-1))/buttonsPerRow;
for(int i=0;i<getChildCount();i++){
View child=getChildAt(i);
if(child.getVisibility()==GONE)
continue;
child.measure(buttonWidth | MeasureSpec.EXACTLY, dp(ROW_HEIGHT) | MeasureSpec.EXACTLY);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b){
int width=r-l;
int buttonsPerRow=visibleChildCount>4 ? 3 : visibleChildCount;
int buttonWidth=(width-dp(H_GAP)*(buttonsPerRow-1))/buttonsPerRow;
for(int i=0;i<getChildCount();i++){
View child=getChildAt(i);
if(child.getVisibility()==GONE)
continue;
int column=i%buttonsPerRow;
int row=i/buttonsPerRow;
int left=(buttonWidth+dp(H_GAP))*column;
int top=dp(ROW_HEIGHT+V_GAP)*row;
child.layout(left, top, left+buttonWidth, top+dp(ROW_HEIGHT));
}
}
}
}

View File

@ -0,0 +1,40 @@
package org.joinmastodon.android.ui.sheets;
import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.ui.utils.UiUtils;
import androidx.annotation.NonNull;
import me.grishka.appkit.Nav;
import me.grishka.appkit.views.BottomSheet;
public class DonationSuccessfulSheet extends BottomSheet{
public DonationSuccessfulSheet(@NonNull Context context, @NonNull String accountID, String postText){
super(context);
View content=context.getSystemService(LayoutInflater.class).inflate(R.layout.sheet_donation_success, null);
setContentView(content);
setNavigationBarBackground(new ColorDrawable(UiUtils.alphaBlendColors(UiUtils.getThemeColor(context, R.attr.colorM3Surface),
UiUtils.getThemeColor(context, R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
content.findViewById(R.id.btn_done).setOnClickListener(v->dismiss());
View shareButton=content.findViewById(R.id.btn_share);
if(postText==null){
shareButton.setEnabled(false);
}
shareButton.setOnClickListener(v->{
Bundle args=new Bundle();
args.putString("account", accountID);
args.putString("prefilledText", postText);
Nav.go((Activity) context, ComposeFragment.class, args);
dismiss();
});
}
}

View File

@ -0,0 +1,340 @@
package org.joinmastodon.android.ui.views;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.Editable;
import android.text.InputFilter;
import android.text.InputType;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextWatcher;
import android.text.method.DigitsKeyListener;
import android.text.style.ReplacementSpan;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Currency;
import java.util.List;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import me.grishka.appkit.utils.CustomViewHelper;
public class CurrencyAmountInput extends LinearLayout implements CustomViewHelper{
private ActualEditText edit;
private Button currencyBtn;
private List<CurrencyInfo> currencies;
private CurrencyInfo currentCurrency;
private boolean spanAdded;
private CurrencySymbolSpan symbolSpan;
private boolean symbolBeforeAmount;
private ChangeListener changeListener;
private long lastAmount=0;
private NumberFormat numberFormat=NumberFormat.getNumberInstance();
private boolean allowSymbolToBeDeleted;
public CurrencyAmountInput(Context context){
this(context, null);
}
public CurrencyAmountInput(Context context, AttributeSet attrs){
this(context, attrs, 0);
}
public CurrencyAmountInput(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
setForeground(getResources().getDrawable(R.drawable.fg_currency_input, context.getTheme()));
setAddStatesFromChildren(true);
if(!isInEditMode())
setOutlineProvider(OutlineProviders.roundedRect(8));
setClipToOutline(true);
currencyBtn=new Button(context);
currencyBtn.setTextAppearance(R.style.m3_label_large);
currencyBtn.setSingleLine();
currencyBtn.setTextColor(UiUtils.getThemeColor(context, R.attr.colorM3OnSurfaceVariant));
int pad=dp(12);
currencyBtn.setPadding(pad, 0, pad, 0);
currencyBtn.setBackgroundColor(UiUtils.getThemeColor(context, R.attr.colorM3SurfaceVariant));
currencyBtn.setMinimumWidth(0);
currencyBtn.setMinWidth(0);
currencyBtn.setOnClickListener(v->showCurrencySelector());
currencyBtn.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_unfold_more_wght600_15pt_8x20px, 0, 0, 0);
currencyBtn.setCompoundDrawableTintList(currencyBtn.getTextColors());
currencyBtn.setCompoundDrawablePadding(dp(4));
addView(currencyBtn, new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT));
edit=new ActualEditText(context);
edit.setBackgroundColor(UiUtils.getThemeColor(context, R.attr.colorM3Surface));
pad=dp(16);
edit.setPadding(pad, 0, pad, 0);
edit.setSingleLine();
edit.setTextAppearance(R.style.m3_title_large);
edit.setTextColor(UiUtils.getThemeColor(context, R.attr.colorM3OnSurface));
edit.setGravity(Gravity.END |Gravity.CENTER_VERTICAL);
edit.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
InputFilter[] filters=edit.getText().getFilters();
for(int i=0;i<filters.length;i++){
if(filters[i] instanceof DigitsKeyListener){
filters[i]=new FormattingFriendlyDigitsKeyListener();
edit.getText().setFilters(filters);
break;
}
}
addView(edit, new LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
symbolSpan=new CurrencySymbolSpan(edit.getPaint());
NumberFormat format=NumberFormat.getInstance();
String one=format.format(1);
format=NumberFormat.getCurrencyInstance();
format.setCurrency(Currency.getInstance("USD"));
symbolBeforeAmount=format.format(1).indexOf(one)>0;
edit.addTextChangedListener(new TextWatcher(){
private boolean ignore;
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after){
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count){
}
@Override
public void afterTextChanged(Editable e){
if(ignore)
return;
ignore=true;
if(e.length()>0 && !spanAdded){
SpannableString ss=new SpannableString(" ");
ss.setSpan(symbolSpan, 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
if(symbolBeforeAmount)
e.insert(0, ss);
else
e.append(ss);
spanAdded=true;
}else if(spanAdded && e.length()<=1){
spanAdded=false;
if(e.length()>0){
allowSymbolToBeDeleted=true;
e.clear();
allowSymbolToBeDeleted=false;
}
}
ignore=false;
updateAmount();
}
});
}
public void setCurrencies(List<String> currencies){
this.currencies=currencies.stream().map(CurrencyInfo::new).collect(Collectors.toList());
}
public void setSelectedCurrency(String code){
CurrencyInfo info=null;
for(CurrencyInfo c:currencies){
if(c.code.equals(code)){
info=c;
break;
}
}
if(info==null)
throw new IllegalArgumentException();
setCurrency(info);
}
private void setCurrency(CurrencyInfo info){
currencyBtn.setText(info.code);
currentCurrency=info;
edit.invalidate();
if(changeListener!=null)
changeListener.onCurrencyChanged(info.code);
}
private void showCurrencySelector(){
ArrayAdapter<CurrencyInfo> adapter=new ArrayAdapter<>(getContext(), R.layout.item_alert_single_choice_2lines_but_different, R.id.text, currencies){
@Override
public boolean hasStableIds(){
return true;
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent){
View view=super.getView(position, convertView, parent);
TextView subtitle=view.findViewById(R.id.subtitle);
CurrencyInfo item=getItem(position);
if(item.jCurrency==null || item.jCurrency.getDisplayName().equals(item.code)){
subtitle.setVisibility(View.GONE);
}else{
subtitle.setVisibility(View.VISIBLE);
subtitle.setText(item.jCurrency.getDisplayName());
}
return view;
}
};
new M3AlertDialogBuilder(getContext())
.setTitle(R.string.currency)
.setSingleChoiceItems(adapter, currencies.indexOf(currentCurrency), (dlg, item)->{
setCurrency(currencies.get(item));
dlg.dismiss();
})
.show();
}
public void setChangeListener(ChangeListener changeListener){
this.changeListener=changeListener;
}
private void updateAmount(){
long newAmount;
try{
Number n=numberFormat.parse(edit.getText().toString().trim());
if(n instanceof Long l){
newAmount=l*100L;
}else if(n instanceof Double d){
newAmount=(long)(d*100);
}else{
newAmount=0;
}
}catch(ParseException x){
newAmount=0;
}
if(newAmount!=lastAmount){
lastAmount=newAmount;
if(changeListener!=null)
changeListener.onAmountChanged(lastAmount);
}
}
public long getAmount(){
return lastAmount;
}
public String getCurrency(){
return currentCurrency.code;
}
@SuppressLint("DefaultLocale")
public void setAmount(long amount){
String value;
if(amount%100==0)
value=String.valueOf(amount/100);
else
value=String.format("%.2f", amount/100.0);
int start=spanAdded ? 1 : 0;
edit.getText().replace(start, edit.length(), value);
}
private class ActualEditText extends EditText{
public ActualEditText(Context context){
super(context);
setClipToPadding(false);
}
@Override
protected void onSelectionChanged(int selStart, int selEnd){
super.onSelectionChanged(selStart, selEnd);
// Adjust the selection to prevent the symbol span being selected
if(spanAdded){
int newSelStart=symbolBeforeAmount ? Math.max(selStart, 1) : Math.min(selStart, length()-1);
int newSelEnd=symbolBeforeAmount ? Math.max(selEnd, 1) : Math.min(selEnd, length()-1);
if(newSelStart!=selStart || newSelEnd!=selEnd){
setSelection(newSelStart, newSelEnd);
}
}
}
}
private static class CurrencyInfo{
public String code;
public String symbol;
public Currency jCurrency;
public CurrencyInfo(String code){
this.code=code;
try{
jCurrency=Currency.getInstance(code);
symbol=jCurrency.getSymbol();
}catch(IllegalArgumentException x){
symbol=code;
}
}
@NonNull
@Override
public String toString(){
return code;
}
}
private class CurrencySymbolSpan extends ReplacementSpan{
private Paint paint;
public CurrencySymbolSpan(Paint paint){
this.paint=new Paint(paint);
this.paint.setTextSize(paint.getTextSize()*0.66f);
}
@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm){
return Math.round(this.paint.measureText(currentCurrency.symbol))+dp(2);
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint){
this.paint.setColor(paint.getColor());
this.paint.setAlpha(77);
if(!symbolBeforeAmount)
x+=dp(2);
canvas.drawText(currentCurrency.symbol, x, top+dp(1.5f)-this.paint.ascent(), this.paint);
}
}
private class FormattingFriendlyDigitsKeyListener extends DigitsKeyListener{
public FormattingFriendlyDigitsKeyListener(){
super(false, true);
}
@Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend){
// Allow the currency symbol to be inserted (always done as a separate insertion operation)
if(source instanceof Spannable s && s.getSpans(start, end, CurrencySymbolSpan.class).length>0){
return source;
}
// Don't allow the currency symbol to be deleted
if(!allowSymbolToBeDeleted && end-start<dend-dstart && dest.getSpans(dstart, dend, CurrencySymbolSpan.class).length>0){
return dest.subSequence(dstart, dend);
}
return super.filter(source, start, end, dest, dstart, dend);
}
}
public interface ChangeListener{
void onCurrencyChanged(String code);
void onAmountChanged(long amount);
}
}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300"
android:interpolator="@interpolator/cubic_bezier_default"
android:shareInterpolator="true"
>
<alpha
android:fromAlpha="0"
android:toAlpha="1" />
<translate
android:fromXDelta="@integer/hundred_dp"
android:toXDelta="0"/>
</set>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="200"
android:interpolator="@interpolator/cubic_bezier_default"
android:shareInterpolator="true"
>
<alpha
android:fromAlpha="1"
android:toAlpha="0" />
<translate
android:fromXDelta="0"
android:toXDelta="@integer/hundred_dp"/>
</set>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" android:duration="300">
</set>

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="@color/masterialDark_secondaryContainer"/>
</item>
<item android:height="0dp">
<layer-list>
<item android:gravity="center_vertical|end">
<bitmap android:src="@drawable/scribble" android:alpha="0.08" android:autoMirrored="true"/>
</item>
</layer-list>
</item>
</layer-list>

View File

@ -1,5 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true">
<ripple android:color="@color/m3_on_secondary_container_overlay">
<item>
<shape>
<corners android:radius="8dp"/>
<solid android:color="?colorM3SecondaryContainer"/>
</shape>
</item>
</ripple>
</item>
<item android:state_selected="true">
<ripple android:color="@color/m3_on_secondary_container_overlay">
<item>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true">
<shape>
<corners android:radius="8dp"/>
<stroke android:color="?colorM3Primary" android:width="1dp"/>
</shape>
</item>
<item>
<shape>
<corners android:radius="8dp"/>
<stroke android:color="?colorM3OutlineVariant" android:width="1dp"/>
</shape>
</item>
</selector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:fillColor="@android:color/white"
android:pathData="M15,10.875V9.125H18.333V10.875ZM16,16.667 L13.333,14.667 14.375,13.25 17.042,15.25ZM14.396,6.729 L13.333,5.333 16,3.333 17.062,4.729ZM4.167,15.833V12.5H3.417Q2.729,12.5 2.198,12.052Q1.667,11.604 1.667,10.917V9.083Q1.667,8.396 2.198,7.948Q2.729,7.5 3.417,7.5H6.667L10.833,5V15L6.667,12.5H5.917V15.833ZM11.708,12.792V7.208Q12.271,7.708 12.615,8.427Q12.958,9.146 12.958,10Q12.958,10.854 12.615,11.573Q12.271,12.292 11.708,12.792ZM3.417,9.25Q3.417,9.25 3.417,9.25Q3.417,9.25 3.417,9.25V10.75Q3.417,10.75 3.417,10.75Q3.417,10.75 3.417,10.75H7.208L9.083,11.917V8.083L7.208,9.25ZM6.25,10Q6.25,10 6.25,10Q6.25,10 6.25,10Q6.25,10 6.25,10Q6.25,10 6.25,10Z"/>
</vector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true" android:drawable="@drawable/ic_baseline_check_18"/>
<item android:drawable="@drawable/ic_favorite_18px"/>
</selector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="18dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:fillColor="@android:color/white"
android:pathData="M10,17 L8.958,16.062Q6.875,14.208 5.521,12.885Q4.167,11.562 3.385,10.531Q2.604,9.5 2.302,8.646Q2,7.792 2,6.896Q2,5.042 3.271,3.771Q4.542,2.5 6.396,2.5Q7.417,2.5 8.375,2.938Q9.333,3.375 10,4.167Q10.667,3.375 11.625,2.938Q12.583,2.5 13.604,2.5Q15.458,2.5 16.729,3.771Q18,5.042 18,6.896Q18,7.792 17.708,8.625Q17.417,9.458 16.635,10.479Q15.854,11.5 14.49,12.844Q13.125,14.188 11,16.104ZM10,14.979Q11.938,13.25 13.188,12.031Q14.438,10.812 15.177,9.906Q15.917,9 16.208,8.292Q16.5,7.583 16.5,6.896Q16.5,5.667 15.667,4.833Q14.833,4 13.604,4Q12.875,4 12.24,4.302Q11.604,4.604 11.146,5.146L10.417,6H9.583L8.854,5.146Q8.396,4.604 7.74,4.302Q7.083,4 6.396,4Q5.167,4 4.333,4.833Q3.5,5.667 3.5,6.896Q3.5,7.583 3.771,8.26Q4.042,8.938 4.76,9.833Q5.479,10.729 6.74,11.958Q8,13.188 10,14.979ZM10,9.479Q10,9.479 10,9.479Q10,9.479 10,9.479Q10,9.479 10,9.479Q10,9.479 10,9.479Q10,9.479 10,9.479Q10,9.479 10,9.479Q10,9.479 10,9.479Q10,9.479 10,9.479Q10,9.479 10,9.479Q10,9.479 10,9.479Q10,9.479 10,9.479Q10,9.479 10,9.479Q10,9.479 10,9.479Q10,9.479 10,9.479Q10,9.479 10,9.479Q10,9.479 10,9.479Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M482,640L622,500Q639,483 644,458.5Q649,434 639,411Q629,388 609,374Q589,360 564,360Q539,360 519,375.5Q499,391 482,408Q464,391 444.5,375.5Q425,360 400,360Q375,360 354.5,373.5Q334,387 324,410Q314,433 319.5,457.5Q325,482 342,500L482,640ZM370,880L354,752Q341,747 329.5,740Q318,733 307,725L188,775L78,585L181,507Q180,500 180,493.5Q180,487 180,480Q180,473 180,466.5Q180,460 181,453L78,375L188,185L307,235Q318,227 330,220Q342,213 354,208L370,80L590,80L606,208Q619,213 630.5,220Q642,227 653,235L772,185L882,375L779,453Q780,460 780,466.5Q780,473 780,480Q780,487 780,493.5Q780,500 778,507L881,585L771,775L653,725Q642,733 630,740Q618,747 606,752L590,880L370,880ZM440,800L519,800L533,694Q564,686 590.5,670.5Q617,655 639,633L738,674L777,606L691,541Q696,527 698,511.5Q700,496 700,480Q700,464 698,448.5Q696,433 691,419L777,354L738,286L639,328Q617,305 590.5,289.5Q564,274 533,266L520,160L441,160L427,266Q396,274 369.5,289.5Q343,305 321,327L222,286L183,354L269,418Q264,433 262,448Q260,463 260,480Q260,496 262,511Q264,526 269,541L183,606L222,674L321,632Q343,655 369.5,670.5Q396,686 427,694L440,800ZM480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480Z"/>
</vector>

View File

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="8dp"
android:height="20dp"
android:viewportWidth="8"
android:viewportHeight="20">
<group android:translateX="-6" android:scaleX="0.75" android:scaleY="0.75" android:pivotX="10" android:pivotY="10">
<path
android:fillColor="@android:color/white"
android:pathData="M10,17.375 L6.208,13.583 7.646,12.167 10,14.5 12.354,12.167 13.792,13.583ZM7.646,7.812 L6.208,6.375 10,2.583 13.792,6.375 12.354,7.812 10,5.458Z" />
</group>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:fillColor="@android:color/white"
android:pathData="M13.292,10.917Q11.354,9.146 9.812,7.521Q8.271,5.896 8.271,4.458Q8.271,3.292 9.083,2.479Q9.896,1.667 11.062,1.667Q11.729,1.667 12.312,1.938Q12.896,2.208 13.292,2.667Q13.688,2.208 14.271,1.938Q14.854,1.667 15.521,1.667Q16.688,1.667 17.5,2.479Q18.312,3.292 18.312,4.458Q18.312,5.896 16.771,7.521Q15.229,9.146 13.292,10.917ZM13.292,8.542Q14.479,7.396 15.521,6.26Q16.562,5.125 16.562,4.458Q16.562,4.021 16.26,3.719Q15.958,3.417 15.521,3.417Q15.229,3.417 15,3.531Q14.771,3.646 14.625,3.812L13.312,5.375L12,3.812Q11.833,3.646 11.594,3.531Q11.354,3.417 11.062,3.417Q10.625,3.417 10.323,3.719Q10.021,4.021 10.021,4.458Q10.021,5.125 11.062,6.26Q12.104,7.396 13.292,8.542ZM13.292,5.979Q13.292,5.979 13.292,5.979Q13.292,5.979 13.292,5.979Q13.292,5.979 13.292,5.979Q13.292,5.979 13.292,5.979Q13.292,5.979 13.292,5.979Q13.292,5.979 13.292,5.979Q13.292,5.979 13.292,5.979Q13.292,5.979 13.292,5.979Q13.292,5.979 13.292,5.979Q13.292,5.979 13.292,5.979Q13.292,5.979 13.292,5.979Q13.292,5.979 13.292,5.979ZM11.667,18.75 L5.979,17.167V18.396H0.833V9.042H7.542L12.646,10.938Q13.354,11.208 13.781,11.854Q14.208,12.5 14.208,13.229H15.729Q16.812,13.229 17.573,13.958Q18.333,14.688 18.333,15.771V16.667ZM2.583,16.646H4.229V10.792H2.583ZM11.625,16.938 L16.5,15.396Q16.396,15.208 16.188,15.094Q15.979,14.979 15.729,14.979H11.75Q11.125,14.979 10.646,14.906Q10.167,14.833 9.812,14.708L8.104,14.125L8.646,12.438L10.438,13.042Q10.771,13.146 11.167,13.188Q11.562,13.229 12.479,13.229Q12.479,13 12.344,12.802Q12.208,12.604 12.021,12.542L7.229,10.792H5.979V15.396ZM4.229,13.708ZM12.479,13.229Q12.479,13.229 12.479,13.229Q12.479,13.229 12.479,13.229Q12.479,13.229 12.479,13.229Q12.479,13.229 12.479,13.229Q12.479,13.229 12.479,13.229Q12.479,13.229 12.479,13.229Q12.479,13.229 12.479,13.229Q12.479,13.229 12.479,13.229ZM4.229,13.708ZM5.979,13.708Q5.979,13.708 5.979,13.708Q5.979,13.708 5.979,13.708Q5.979,13.708 5.979,13.708Q5.979,13.708 5.979,13.708Q5.979,13.708 5.979,13.708Q5.979,13.708 5.979,13.708Q5.979,13.708 5.979,13.708Q5.979,13.708 5.979,13.708Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M640,520L474,358Q443,328 421.5,291.5Q400,255 400,212Q400,157 438.5,118.5Q477,80 532,80Q564,80 592,93.5Q620,107 640,130Q660,107 688,93.5Q716,80 748,80Q803,80 841.5,118.5Q880,157 880,212Q880,255 859,291.5Q838,328 807,358L640,520ZM640,408L749,301Q768,282 784,260.5Q800,239 800,212Q800,190 785,175Q770,160 748,160Q734,160 721.5,165.5Q709,171 700,182L640,254L580,182Q571,171 558.5,165.5Q546,160 532,160Q510,160 495,175Q480,190 480,212Q480,239 496,260.5Q512,282 531,301L640,408ZM280,740L558,816L796,742Q791,733 781.5,726.5Q772,720 760,720L558,720Q531,720 515,718Q499,716 482,710L389,679L411,601L492,628Q509,633 532,636Q555,639 600,640L600,640Q600,640 600,640Q600,640 600,640Q600,629 593.5,619Q587,609 578,606L344,520Q344,520 344,520Q344,520 344,520L280,520L280,740ZM40,880L40,440L344,440Q351,440 358,441.5Q365,443 371,445L606,532Q639,544 659.5,574Q680,604 680,640L760,640Q810,640 845,673Q880,706 880,760L880,800L560,900L280,822L280,822L280,880L40,880ZM120,800L200,800L200,520L120,520L120,800ZM640,254L640,254Q640,254 640,254Q640,254 640,254Q640,254 640,254Q640,254 640,254Q640,254 640,254Q640,254 640,254L640,254L640,254Q640,254 640,254Q640,254 640,254Q640,254 640,254Q640,254 640,254Q640,254 640,254Q640,254 640,254L640,254Z"/>
</vector>

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android"
android:controlX1="0.25" android:controlY1="0.1" android:controlX2="0.25" android:controlY2="1"/>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="12dp"
android:paddingStart="16dp"
android:paddingEnd="8dp"
android:gravity="center_vertical"
android:background="@drawable/bg_donation_banner">
<TextView
android:id="@+id/banner_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textAppearance="@style/m3_body_medium"
android:textColor="@color/masterialDark_onSecondaryContainer"
tools:text="Donation banner text goes here"/>
<ImageButton
android:id="@+id/banner_dismiss"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?android:actionBarItemBackground"
android:tint="@color/masterialDark_onSecondaryContainer"
android:backgroundTint="@color/masterialDark_onSecondaryContainer"
android:src="@drawable/ic_baseline_close_24"
android:contentDescription="@string/dismiss"/>
</LinearLayout>

View File

@ -58,5 +58,12 @@
android:text="@string/see_new_posts"/>
</FrameLayout>
<ViewStub
android:id="@+id/donation_banner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout="@layout/donation_banner"/>
</FrameLayout>
</me.grishka.appkit.views.RecursiveSwipeRefreshLayout>

View File

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="utf-8"?>
<org.joinmastodon.android.ui.views.CustomScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_bottom_sheet"
android:outlineProvider="background"
android:elevation="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="16dp"
android:paddingHorizontal="16dp">
<View
android:id="@+id/handle"
android:layout_width="match_parent"
android:layout_height="36dp"
android:layout_marginBottom="8dp"
android:background="@drawable/bg_bottom_sheet_handle"/>
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_body_large"
android:textColor="?colorM3OnSurfaceVariant"
tools:text="By supporting Mastodon, you help sustain a global network that values people over profit. Will you join us today?"/>
<FrameLayout
android:id="@+id/tabbar"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="16dp">
<LinearLayout
android:id="@+id/tabbar_inner"
android:layout_width="match_parent"
android:layout_height="40dp"
android:orientation="horizontal"
android:layout_gravity="center"
style="@style/Widget.Mastodon.M3.SegmentedButtonContainer">
<FrameLayout
android:id="@+id/once"
style="@style/Widget.Mastodon.M3.SegmentedButton">
<org.joinmastodon.android.ui.views.CheckIconSelectableTextView
style="@style/Widget.Mastodon.M3.SegmentedButtonText"
android:text="@string/donation_once"/>
</FrameLayout>
<FrameLayout
android:id="@+id/monthly"
style="@style/Widget.Mastodon.M3.SegmentedButton">
<TextView
style="@style/Widget.Mastodon.M3.SegmentedButtonText"
android:drawableStart="@drawable/ic_donation_monthly"
android:drawableTint="?colorM3OnSurface"
android:text="@string/donation_monthly"/>
</FrameLayout>
<FrameLayout
android:id="@+id/yearly"
style="@style/Widget.Mastodon.M3.SegmentedButton">
<org.joinmastodon.android.ui.views.CheckIconSelectableTextView
style="@style/Widget.Mastodon.M3.SegmentedButtonText"
android:text="@string/donation_yearly"/>
</FrameLayout>
</LinearLayout>
</FrameLayout>
<org.joinmastodon.android.ui.views.CurrencyAmountInput
android:id="@+id/amount"
android:layout_width="match_parent"
android:layout_height="52dp"
android:layout_marginTop="16dp"/>
<view class="org.joinmastodon.android.ui.sheets.DonationSheet$SuggestedAmountsLayout"
android:id="@+id/suggested_amounts"
android:layout_width="match_parent"
android:layout_height="32dp"
android:layout_marginTop="8dp"/>
<FrameLayout
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginTop="28dp"
style="@style/Widget.Mastodon.M3.Button.Filled">
<TextView
android:id="@+id/button_text"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_gravity="center_horizontal"
android:drawableStart="@drawable/ic_volunteer_activism_20px"
style="@style/Widget.Mastodon.M3.Button.Filled"
android:background="@null"
android:padding="0dp"
android:drawablePadding="8dp"
android:drawableTint="@color/button_text_m3_filled"
android:clickable="false"
android:focusable="false"
android:duplicateParentState="true"
android:minWidth="0dp"
tools:text="Donate"/>
</FrameLayout>
</LinearLayout>
</org.joinmastodon.android.ui.views.CustomScrollView>

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<org.joinmastodon.android.ui.views.CustomScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@drawable/bg_bottom_sheet"
android:outlineProvider="background"
android:elevation="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="16dp"
android:paddingHorizontal="16dp">
<View
android:id="@+id/handle"
android:layout_width="match_parent"
android:layout_height="36dp"
android:layout_marginBottom="8dp"
android:background="@drawable/bg_bottom_sheet_handle"/>
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/m3_headline_medium"
android:textColor="?colorM3OnSurface"
android:gravity="center"
android:text="@string/donation_success_title"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAppearance="@style/m3_body_large"
android:gravity="center"
android:textColor="?colorM3OnSurfaceVariant"
android:text="@string/donation_success_subtitle"/>
<org.joinmastodon.android.ui.views.FixedAspectRatioImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:src="@drawable/donation_successful_art"
app:aspectRatio="1.777777"/>
<FrameLayout
android:id="@+id/btn_share"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginTop="16dp"
style="@style/Widget.Mastodon.M3.Button.Filled">
<TextView
android:id="@+id/button_text"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_gravity="center_horizontal"
android:drawableStart="@drawable/ic_campaign_20px"
style="@style/Widget.Mastodon.M3.Button.Filled"
android:background="@null"
android:padding="0dp"
android:drawablePadding="8dp"
android:drawableTint="@color/button_text_m3_filled"
android:clickable="false"
android:focusable="false"
android:duplicateParentState="true"
android:minWidth="0dp"
android:text="@string/donation_success_share"/>
</FrameLayout>
<Button
android:id="@+id/btn_done"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginTop="16dp"
style="@style/Widget.Mastodon.M3.Button.Outlined"
android:text="@string/done"/>
</LinearLayout>
</org.joinmastodon.android.ui.views.CustomScrollView>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="hundred_dp">150</integer>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="hundred_dp">133</integer>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="hundred_dp">200</integer>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="hundred_dp">300</integer>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="hundred_dp">400</integer>
</resources>

View File

@ -0,0 +1,336 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- light -->
<color name="masterialLight_primary">#4000DD</color>
<color name="masterialLight_onPrimary">#FFFFFF</color>
<color name="masterialLight_primaryContainer">#6648FF</color>
<color name="masterialLight_onPrimaryContainer">#FFFFFF</color>
<color name="masterialLight_secondary">#5D51AF</color>
<color name="masterialLight_onSecondary">#FFFFFF</color>
<color name="masterialLight_secondaryContainer">#B0A5FF</color>
<color name="masterialLight_onSecondaryContainer">#220C73</color>
<color name="masterialLight_tertiary">#810082</color>
<color name="masterialLight_onTertiary">#FFFFFF</color>
<color name="masterialLight_tertiaryContainer">#B722B7</color>
<color name="masterialLight_onTertiaryContainer">#FFFFFF</color>
<color name="masterialLight_error">#BA1A1A</color>
<color name="masterialLight_onError">#FFFFFF</color>
<color name="masterialLight_errorContainer">#FFDAD6</color>
<color name="masterialLight_onErrorContainer">#410002</color>
<color name="masterialLight_background">#FCF8FF</color>
<color name="masterialLight_onBackground">#1C1A25</color>
<color name="masterialLight_surface">#FCF8FF</color>
<color name="masterialLight_onSurface">#1C1A25</color>
<color name="masterialLight_surfaceVariant">#E5DFF6</color>
<color name="masterialLight_onSurfaceVariant">#474557</color>
<color name="masterialLight_outline">#787588</color>
<color name="masterialLight_outlineVariant">#C9C4DA</color>
<color name="masterialLight_scrim">#000000</color>
<color name="masterialLight_inverseSurface">#312F3B</color>
<color name="masterialLight_inverseOnSurface">#F3EEFE</color>
<color name="masterialLight_inversePrimary">#C7BFFF</color>
<color name="masterialLight_primaryFixed">#E5DEFF</color>
<color name="masterialLight_onPrimaryFixed">#180065</color>
<color name="masterialLight_primaryFixedDim">#C7BFFF</color>
<color name="masterialLight_onPrimaryFixedVariant">#4000DC</color>
<color name="masterialLight_secondaryFixed">#E5DEFF</color>
<color name="masterialLight_onSecondaryFixed">#180065</color>
<color name="masterialLight_secondaryFixedDim">#C7BFFF</color>
<color name="masterialLight_onSecondaryFixedVariant">#453895</color>
<color name="masterialLight_tertiaryFixed">#FFD7F6</color>
<color name="masterialLight_onTertiaryFixed">#380039</color>
<color name="masterialLight_tertiaryFixedDim">#FFAAF5</color>
<color name="masterialLight_onTertiaryFixedVariant">#800082</color>
<color name="masterialLight_surfaceDim">#DCD8E7</color>
<color name="masterialLight_surfaceBright">#FCF8FF</color>
<color name="masterialLight_surfaceContainerLowest">#FFFFFF</color>
<color name="masterialLight_surfaceContainerLow">#F6F1FF</color>
<color name="masterialLight_surfaceContainer">#F1EBFB</color>
<color name="masterialLight_surfaceContainerHigh">#EBE6F6</color>
<color name="masterialLight_surfaceContainerHighest">#E5E0F0</color>
<color name="masterialLight_primary_mediumContrast">#3C00D1</color>
<color name="masterialLight_onPrimary_mediumContrast">#FFFFFF</color>
<color name="masterialLight_primaryContainer_mediumContrast">#6648FF</color>
<color name="masterialLight_onPrimaryContainer_mediumContrast">#FFFFFF</color>
<color name="masterialLight_secondary_mediumContrast">#413391</color>
<color name="masterialLight_onSecondary_mediumContrast">#FFFFFF</color>
<color name="masterialLight_secondaryContainer_mediumContrast">#7368C7</color>
<color name="masterialLight_onSecondaryContainer_mediumContrast">#FFFFFF</color>
<color name="masterialLight_tertiary_mediumContrast">#7A007B</color>
<color name="masterialLight_onTertiary_mediumContrast">#FFFFFF</color>
<color name="masterialLight_tertiaryContainer_mediumContrast">#B722B7</color>
<color name="masterialLight_onTertiaryContainer_mediumContrast">#FFFFFF</color>
<color name="masterialLight_error_mediumContrast">#8C0009</color>
<color name="masterialLight_onError_mediumContrast">#FFFFFF</color>
<color name="masterialLight_errorContainer_mediumContrast">#DA342E</color>
<color name="masterialLight_onErrorContainer_mediumContrast">#FFFFFF</color>
<color name="masterialLight_background_mediumContrast">#FCF8FF</color>
<color name="masterialLight_onBackground_mediumContrast">#1C1A25</color>
<color name="masterialLight_surface_mediumContrast">#FCF8FF</color>
<color name="masterialLight_onSurface_mediumContrast">#1C1A25</color>
<color name="masterialLight_surfaceVariant_mediumContrast">#E5DFF6</color>
<color name="masterialLight_onSurfaceVariant_mediumContrast">#434153</color>
<color name="masterialLight_outline_mediumContrast">#605D70</color>
<color name="masterialLight_outlineVariant_mediumContrast">#7C788C</color>
<color name="masterialLight_scrim_mediumContrast">#000000</color>
<color name="masterialLight_inverseSurface_mediumContrast">#312F3B</color>
<color name="masterialLight_inverseOnSurface_mediumContrast">#F3EEFE</color>
<color name="masterialLight_inversePrimary_mediumContrast">#C7BFFF</color>
<color name="masterialLight_primaryFixed_mediumContrast">#7058FF</color>
<color name="masterialLight_onPrimaryFixed_mediumContrast">#FFFFFF</color>
<color name="masterialLight_primaryFixedDim_mediumContrast">#552BFB</color>
<color name="masterialLight_onPrimaryFixedVariant_mediumContrast">#FFFFFF</color>
<color name="masterialLight_secondaryFixed_mediumContrast">#7368C7</color>
<color name="masterialLight_onSecondaryFixed_mediumContrast">#FFFFFF</color>
<color name="masterialLight_secondaryFixedDim_mediumContrast">#5A4EAC</color>
<color name="masterialLight_onSecondaryFixedVariant_mediumContrast">#FFFFFF</color>
<color name="masterialLight_tertiaryFixed_mediumContrast">#C330C2</color>
<color name="masterialLight_onTertiaryFixed_mediumContrast">#FFFFFF</color>
<color name="masterialLight_tertiaryFixedDim_mediumContrast">#A400A6</color>
<color name="masterialLight_onTertiaryFixedVariant_mediumContrast">#FFFFFF</color>
<color name="masterialLight_surfaceDim_mediumContrast">#DCD8E7</color>
<color name="masterialLight_surfaceBright_mediumContrast">#FCF8FF</color>
<color name="masterialLight_surfaceContainerLowest_mediumContrast">#FFFFFF</color>
<color name="masterialLight_surfaceContainerLow_mediumContrast">#F6F1FF</color>
<color name="masterialLight_surfaceContainer_mediumContrast">#F1EBFB</color>
<color name="masterialLight_surfaceContainerHigh_mediumContrast">#EBE6F6</color>
<color name="masterialLight_surfaceContainerHighest_mediumContrast">#E5E0F0</color>
<color name="masterialLight_primary_highContrast">#1E0077</color>
<color name="masterialLight_onPrimary_highContrast">#FFFFFF</color>
<color name="masterialLight_primaryContainer_highContrast">#3C00D1</color>
<color name="masterialLight_onPrimaryContainer_highContrast">#FFFFFF</color>
<color name="masterialLight_secondary_highContrast">#1F0671</color>
<color name="masterialLight_onSecondary_highContrast">#FFFFFF</color>
<color name="masterialLight_secondaryContainer_highContrast">#413391</color>
<color name="masterialLight_onSecondaryContainer_highContrast">#FFFFFF</color>
<color name="masterialLight_tertiary_highContrast">#430044</color>
<color name="masterialLight_onTertiary_highContrast">#FFFFFF</color>
<color name="masterialLight_tertiaryContainer_highContrast">#7A007B</color>
<color name="masterialLight_onTertiaryContainer_highContrast">#FFFFFF</color>
<color name="masterialLight_error_highContrast">#4E0002</color>
<color name="masterialLight_onError_highContrast">#FFFFFF</color>
<color name="masterialLight_errorContainer_highContrast">#8C0009</color>
<color name="masterialLight_onErrorContainer_highContrast">#FFFFFF</color>
<color name="masterialLight_background_highContrast">#FCF8FF</color>
<color name="masterialLight_onBackground_highContrast">#1C1A25</color>
<color name="masterialLight_surface_highContrast">#FCF8FF</color>
<color name="masterialLight_onSurface_highContrast">#000000</color>
<color name="masterialLight_surfaceVariant_highContrast">#E5DFF6</color>
<color name="masterialLight_onSurfaceVariant_highContrast">#242232</color>
<color name="masterialLight_outline_highContrast">#434153</color>
<color name="masterialLight_outlineVariant_highContrast">#434153</color>
<color name="masterialLight_scrim_highContrast">#000000</color>
<color name="masterialLight_inverseSurface_highContrast">#312F3B</color>
<color name="masterialLight_inverseOnSurface_highContrast">#FFFFFF</color>
<color name="masterialLight_inversePrimary_highContrast">#EFE9FF</color>
<color name="masterialLight_primaryFixed_highContrast">#3C00D1</color>
<color name="masterialLight_onPrimaryFixed_highContrast">#FFFFFF</color>
<color name="masterialLight_primaryFixedDim_highContrast">#280094</color>
<color name="masterialLight_onPrimaryFixedVariant_highContrast">#FFFFFF</color>
<color name="masterialLight_secondaryFixed_highContrast">#413391</color>
<color name="masterialLight_onSecondaryFixed_highContrast">#FFFFFF</color>
<color name="masterialLight_secondaryFixedDim_highContrast">#2A197A</color>
<color name="masterialLight_onSecondaryFixedVariant_highContrast">#FFFFFF</color>
<color name="masterialLight_tertiaryFixed_highContrast">#7A007B</color>
<color name="masterialLight_onTertiaryFixed_highContrast">#FFFFFF</color>
<color name="masterialLight_tertiaryFixedDim_highContrast">#550056</color>
<color name="masterialLight_onTertiaryFixedVariant_highContrast">#FFFFFF</color>
<color name="masterialLight_surfaceDim_highContrast">#DCD8E7</color>
<color name="masterialLight_surfaceBright_highContrast">#FCF8FF</color>
<color name="masterialLight_surfaceContainerLowest_highContrast">#FFFFFF</color>
<color name="masterialLight_surfaceContainerLow_highContrast">#F6F1FF</color>
<color name="masterialLight_surfaceContainer_highContrast">#F1EBFB</color>
<color name="masterialLight_surfaceContainerHigh_highContrast">#EBE6F6</color>
<color name="masterialLight_surfaceContainerHighest_highContrast">#E5E0F0</color>
<color name="masterialLight_colorGoldenrod">#7B5800</color>
<color name="masterialLight_colorOnGoldenrod">#FFFFFF</color>
<color name="masterialLight_colorGoldenrodContainer">#FFC758</color>
<color name="masterialLight_colorOnGoldenrodContainer">#503800</color>
<color name="masterialLight_colorLime">#476800</color>
<color name="masterialLight_colorOnLime">#FFFFFF</color>
<color name="masterialLight_colorLimeContainer">#CAFF71</color>
<color name="masterialLight_colorOnLimeContainer">#3A5700</color>
<color name="masterialLight_colorGoldenrod_mediumContrast">#583E00</color>
<color name="masterialLight_colorOnGoldenrod_mediumContrast">#FFFFFF</color>
<color name="masterialLight_colorGoldenrodContainer_mediumContrast">#986D00</color>
<color name="masterialLight_colorOnGoldenrodContainer_mediumContrast">#FFFFFF</color>
<color name="masterialLight_colorLime_mediumContrast">#314A00</color>
<color name="masterialLight_colorOnLime_mediumContrast">#FFFFFF</color>
<color name="masterialLight_colorLimeContainer_mediumContrast">#588000</color>
<color name="masterialLight_colorOnLimeContainer_mediumContrast">#FFFFFF</color>
<color name="masterialLight_colorGoldenrod_highContrast">#2F1F00</color>
<color name="masterialLight_colorOnGoldenrod_highContrast">#FFFFFF</color>
<color name="masterialLight_colorGoldenrodContainer_highContrast">#583E00</color>
<color name="masterialLight_colorOnGoldenrodContainer_highContrast">#FFFFFF</color>
<color name="masterialLight_colorLime_highContrast">#182600</color>
<color name="masterialLight_colorOnLime_highContrast">#FFFFFF</color>
<color name="masterialLight_colorLimeContainer_highContrast">#314A00</color>
<color name="masterialLight_colorOnLimeContainer_highContrast">#FFFFFF</color>
<!-- dark -->
<color name="masterialDark_primary">#C7BFFF</color>
<color name="masterialDark_onPrimary">#2B009E</color>
<color name="masterialDark_primaryContainer">#4D1AF4</color>
<color name="masterialDark_onPrimaryContainer">#FAF5FF</color>
<color name="masterialDark_secondary">#C7BFFF</color>
<color name="masterialDark_onSecondary">#2E1E7E</color>
<color name="masterialDark_secondaryContainer">#3D308E</color>
<color name="masterialDark_onSecondaryContainer">#D6CFFF</color>
<color name="masterialDark_tertiary">#FFAAF5</color>
<color name="masterialDark_onTertiary">#5B005C</color>
<color name="masterialDark_tertiaryContainer">#970099</color>
<color name="masterialDark_onTertiaryContainer">#FFF5F9</color>
<color name="masterialDark_error">#FFB4AB</color>
<color name="masterialDark_onError">#690005</color>
<color name="masterialDark_errorContainer">#93000A</color>
<color name="masterialDark_onErrorContainer">#FFDAD6</color>
<color name="masterialDark_background">#13121D</color>
<color name="masterialDark_onBackground">#E5E0F0</color>
<color name="masterialDark_surface">#13121D</color>
<color name="masterialDark_onSurface">#E5E0F0</color>
<color name="masterialDark_surfaceVariant">#474557</color>
<color name="masterialDark_onSurfaceVariant">#C9C4DA</color>
<color name="masterialDark_outline">#928EA3</color>
<color name="masterialDark_outlineVariant">#474557</color>
<color name="masterialDark_scrim">#000000</color>
<color name="masterialDark_inverseSurface">#E5E0F0</color>
<color name="masterialDark_inverseOnSurface">#312F3B</color>
<color name="masterialDark_inversePrimary">#582FFE</color>
<color name="masterialDark_primaryFixed">#E5DEFF</color>
<color name="masterialDark_onPrimaryFixed">#180065</color>
<color name="masterialDark_primaryFixedDim">#C7BFFF</color>
<color name="masterialDark_onPrimaryFixedVariant">#4000DC</color>
<color name="masterialDark_secondaryFixed">#E5DEFF</color>
<color name="masterialDark_onSecondaryFixed">#180065</color>
<color name="masterialDark_secondaryFixedDim">#C7BFFF</color>
<color name="masterialDark_onSecondaryFixedVariant">#453895</color>
<color name="masterialDark_tertiaryFixed">#FFD7F6</color>
<color name="masterialDark_onTertiaryFixed">#380039</color>
<color name="masterialDark_tertiaryFixedDim">#FFAAF5</color>
<color name="masterialDark_onTertiaryFixedVariant">#800082</color>
<color name="masterialDark_surfaceDim">#13121D</color>
<color name="masterialDark_surfaceBright">#3A3844</color>
<color name="masterialDark_surfaceContainerLowest">#0E0D17</color>
<color name="masterialDark_surfaceContainerLow">#1C1A25</color>
<color name="masterialDark_surfaceContainer">#201E29</color>
<color name="masterialDark_surfaceContainerHigh">#2A2934</color>
<color name="masterialDark_surfaceContainerHighest">#35333F</color>
<color name="masterialDark_primary_mediumContrast">#CCC4FF</color>
<color name="masterialDark_onPrimary_mediumContrast">#130056</color>
<color name="masterialDark_primaryContainer_mediumContrast">#8F7FFF</color>
<color name="masterialDark_onPrimaryContainer_mediumContrast">#000000</color>
<color name="masterialDark_secondary_mediumContrast">#CCC4FF</color>
<color name="masterialDark_onSecondary_mediumContrast">#130056</color>
<color name="masterialDark_secondaryContainer_mediumContrast">#9084E6</color>
<color name="masterialDark_onSecondaryContainer_mediumContrast">#000000</color>
<color name="masterialDark_tertiary_mediumContrast">#FFB1F5</color>
<color name="masterialDark_onTertiary_mediumContrast">#2F0030</color>
<color name="masterialDark_tertiaryContainer_mediumContrast">#E552E1</color>
<color name="masterialDark_onTertiaryContainer_mediumContrast">#000000</color>
<color name="masterialDark_error_mediumContrast">#FFBAB1</color>
<color name="masterialDark_onError_mediumContrast">#370001</color>
<color name="masterialDark_errorContainer_mediumContrast">#FF5449</color>
<color name="masterialDark_onErrorContainer_mediumContrast">#000000</color>
<color name="masterialDark_background_mediumContrast">#13121D</color>
<color name="masterialDark_onBackground_mediumContrast">#E5E0F0</color>
<color name="masterialDark_surface_mediumContrast">#13121D</color>
<color name="masterialDark_onSurface_mediumContrast">#FEF9FF</color>
<color name="masterialDark_surfaceVariant_mediumContrast">#474557</color>
<color name="masterialDark_onSurfaceVariant_mediumContrast">#CDC8DE</color>
<color name="masterialDark_outline_mediumContrast">#A4A0B5</color>
<color name="masterialDark_outlineVariant_mediumContrast">#848195</color>
<color name="masterialDark_scrim_mediumContrast">#000000</color>
<color name="masterialDark_inverseSurface_mediumContrast">#E5E0F0</color>
<color name="masterialDark_inverseOnSurface_mediumContrast">#2A2934</color>
<color name="masterialDark_inversePrimary_mediumContrast">#4100DF</color>
<color name="masterialDark_primaryFixed_mediumContrast">#E5DEFF</color>
<color name="masterialDark_onPrimaryFixed_mediumContrast">#0F0048</color>
<color name="masterialDark_primaryFixedDim_mediumContrast">#C7BFFF</color>
<color name="masterialDark_onPrimaryFixedVariant_mediumContrast">#3000AE</color>
<color name="masterialDark_secondaryFixed_mediumContrast">#E5DEFF</color>
<color name="masterialDark_onSecondaryFixed_mediumContrast">#0F0048</color>
<color name="masterialDark_secondaryFixedDim_mediumContrast">#C7BFFF</color>
<color name="masterialDark_onSecondaryFixedVariant_mediumContrast">#342584</color>
<color name="masterialDark_tertiaryFixed_mediumContrast">#FFD7F6</color>
<color name="masterialDark_onTertiaryFixed_mediumContrast">#260027</color>
<color name="masterialDark_tertiaryFixedDim_mediumContrast">#FFAAF5</color>
<color name="masterialDark_onTertiaryFixedVariant_mediumContrast">#650066</color>
<color name="masterialDark_surfaceDim_mediumContrast">#13121D</color>
<color name="masterialDark_surfaceBright_mediumContrast">#3A3844</color>
<color name="masterialDark_surfaceContainerLowest_mediumContrast">#0E0D17</color>
<color name="masterialDark_surfaceContainerLow_mediumContrast">#1C1A25</color>
<color name="masterialDark_surfaceContainer_mediumContrast">#201E29</color>
<color name="masterialDark_surfaceContainerHigh_mediumContrast">#2A2934</color>
<color name="masterialDark_surfaceContainerHighest_mediumContrast">#35333F</color>
<color name="masterialDark_primary_highContrast">#FEF9FF</color>
<color name="masterialDark_onPrimary_highContrast">#000000</color>
<color name="masterialDark_primaryContainer_highContrast">#CCC4FF</color>
<color name="masterialDark_onPrimaryContainer_highContrast">#000000</color>
<color name="masterialDark_secondary_highContrast">#FEF9FF</color>
<color name="masterialDark_onSecondary_highContrast">#000000</color>
<color name="masterialDark_secondaryContainer_highContrast">#CCC4FF</color>
<color name="masterialDark_onSecondaryContainer_highContrast">#000000</color>
<color name="masterialDark_tertiary_highContrast">#FFF9FA</color>
<color name="masterialDark_onTertiary_highContrast">#000000</color>
<color name="masterialDark_tertiaryContainer_highContrast">#FFB1F5</color>
<color name="masterialDark_onTertiaryContainer_highContrast">#000000</color>
<color name="masterialDark_error_highContrast">#FFF9F9</color>
<color name="masterialDark_onError_highContrast">#000000</color>
<color name="masterialDark_errorContainer_highContrast">#FFBAB1</color>
<color name="masterialDark_onErrorContainer_highContrast">#000000</color>
<color name="masterialDark_background_highContrast">#13121D</color>
<color name="masterialDark_onBackground_highContrast">#E5E0F0</color>
<color name="masterialDark_surface_highContrast">#13121D</color>
<color name="masterialDark_onSurface_highContrast">#FFFFFF</color>
<color name="masterialDark_surfaceVariant_highContrast">#474557</color>
<color name="masterialDark_onSurfaceVariant_highContrast">#FEF9FF</color>
<color name="masterialDark_outline_highContrast">#CDC8DE</color>
<color name="masterialDark_outlineVariant_highContrast">#CDC8DE</color>
<color name="masterialDark_scrim_highContrast">#000000</color>
<color name="masterialDark_inverseSurface_highContrast">#E5E0F0</color>
<color name="masterialDark_inverseOnSurface_highContrast">#000000</color>
<color name="masterialDark_inversePrimary_highContrast">#25008C</color>
<color name="masterialDark_primaryFixed_highContrast">#E9E3FF</color>
<color name="masterialDark_onPrimaryFixed_highContrast">#000000</color>
<color name="masterialDark_primaryFixedDim_highContrast">#CCC4FF</color>
<color name="masterialDark_onPrimaryFixedVariant_highContrast">#130056</color>
<color name="masterialDark_secondaryFixed_highContrast">#E9E3FF</color>
<color name="masterialDark_onSecondaryFixed_highContrast">#000000</color>
<color name="masterialDark_secondaryFixedDim_highContrast">#CCC4FF</color>
<color name="masterialDark_onSecondaryFixedVariant_highContrast">#130056</color>
<color name="masterialDark_tertiaryFixed_highContrast">#FFDDF6</color>
<color name="masterialDark_onTertiaryFixed_highContrast">#000000</color>
<color name="masterialDark_tertiaryFixedDim_highContrast">#FFB1F5</color>
<color name="masterialDark_onTertiaryFixedVariant_highContrast">#2F0030</color>
<color name="masterialDark_surfaceDim_highContrast">#13121D</color>
<color name="masterialDark_surfaceBright_highContrast">#3A3844</color>
<color name="masterialDark_surfaceContainerLowest_highContrast">#0E0D17</color>
<color name="masterialDark_surfaceContainerLow_highContrast">#1C1A25</color>
<color name="masterialDark_surfaceContainer_highContrast">#201E29</color>
<color name="masterialDark_surfaceContainerHigh_highContrast">#2A2934</color>
<color name="masterialDark_surfaceContainerHighest_highContrast">#35333F</color>
<color name="masterialDark_colorGoldenrod">#FFEBCE</color>
<color name="masterialDark_colorOnGoldenrod">#412D00</color>
<color name="masterialDark_colorGoldenrodContainer">#F9B928</color>
<color name="masterialDark_colorOnGoldenrodContainer">#463100</color>
<color name="masterialDark_colorLime">#FFFFFF</color>
<color name="masterialDark_colorOnLime">#233600</color>
<color name="masterialDark_colorLimeContainer">#A5E820</color>
<color name="masterialDark_colorOnLimeContainer">#2E4600</color>
<color name="masterialDark_colorGoldenrod_mediumContrast">#FFEBCE</color>
<color name="masterialDark_colorOnGoldenrod_mediumContrast">#412D00</color>
<color name="masterialDark_colorGoldenrodContainer_mediumContrast">#F9B928</color>
<color name="masterialDark_colorOnGoldenrodContainer_mediumContrast">#150C00</color>
<color name="masterialDark_colorLime_mediumContrast">#FFFFFF</color>
<color name="masterialDark_colorOnLime_mediumContrast">#233600</color>
<color name="masterialDark_colorLimeContainer_mediumContrast">#A5E820</color>
<color name="masterialDark_colorOnLimeContainer_mediumContrast">#162300</color>
<color name="masterialDark_colorGoldenrod_highContrast">#FFFAF7</color>
<color name="masterialDark_colorOnGoldenrod_highContrast">#000000</color>
<color name="masterialDark_colorGoldenrodContainer_highContrast">#FFC03B</color>
<color name="masterialDark_colorOnGoldenrodContainer_highContrast">#000000</color>
<color name="masterialDark_colorLime_highContrast">#FFFFFF</color>
<color name="masterialDark_colorOnLime_highContrast">#000000</color>
<color name="masterialDark_colorLimeContainer_highContrast">#A5E820</color>
<color name="masterialDark_colorOnLimeContainer_highContrast">#000000</color>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="hundred_dp">100</integer>
</resources>

View File

@ -766,4 +766,15 @@
<item quantity="one">%,d new notification</item>
<item quantity="other">%,d new notifications</item>
</plurals>
<string name="dismiss">Dismiss</string>
<string name="donation_once">Just once</string>
<string name="donation_monthly">Monthly</string>
<string name="donation_yearly">Yearly</string>
<string name="currency">Currency</string>
<string name="donation_success_share">Spread the word</string>
<string name="donation_success_title">Thank you for your contribution!</string>
<string name="donation_success_subtitle">You should receive an email confirming your donation soon.</string>
<string name="donation_server_error">We are sorry, an error occurred and we have not been able to process your donation.\n\nPlease retry in a few minutes.</string>
<string name="settings_donate">Donate to Mastodon</string>
<string name="settings_manage_donations">Manage donations</string>
</resources>