Initial implementation of donations

This commit is contained in:
Grishka 2024-04-15 16:36:59 +03:00
parent 1124bc48c2
commit b2d49c3143
26 changed files with 1606 additions and 13 deletions

View File

@ -13,7 +13,7 @@ android {
applicationId "org.joinmastodon.android" applicationId "org.joinmastodon.android"
minSdk 23 minSdk 23
targetSdk 33 targetSdk 33
versionCode 93 versionCode 94
versionName "2.5.0" versionName "2.5.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }

View File

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

@ -0,0 +1,28 @@
package org.joinmastodon.android.api.requests.catalog;
import android.net.Uri;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.donations.DonationCampaign;
public class GetDonationCampaigns extends MastodonAPIRequest<DonationCampaign>{
private final String locale, seed;
public GetDonationCampaigns(String locale, String seed){
super(HttpMethod.GET, null, DonationCampaign.class);
this.locale=locale;
this.seed=seed;
}
@Override
public Uri getURL(){
Uri.Builder builder=new Uri.Builder()
.scheme("https")
.authority("api.joinmastodon.org")
.path("/donations/campaigns")
.appendQueryParameter("platform", "android")
.appendQueryParameter("locale", locale)
.appendQueryParameter("seed", seed);
return builder.build();
}
}

View File

@ -33,7 +33,6 @@ import org.joinmastodon.android.model.TimelineMarkers;
import org.joinmastodon.android.model.Token; import org.joinmastodon.android.model.Token;
import org.joinmastodon.android.utils.ObjectIdComparator; import org.joinmastodon.android.utils.ObjectIdComparator;
import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -276,4 +275,12 @@ public class AccountSession{
public void setNotificationsMentionsOnly(boolean mentionsOnly){ public void setNotificationsMentionsOnly(boolean mentionsOnly){
getRawLocalPreferences().edit().putBoolean("notificationsMentionsOnly", mentionsOnly).apply(); getRawLocalPreferences().edit().putBoolean("notificationsMentionsOnly", mentionsOnly).apply();
} }
public boolean isEligibleForDonations(){
return "mastodon.social".equalsIgnoreCase(domain) || "mastodon.online".equalsIgnoreCase(domain);
}
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.Activity;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager; 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.graphics.drawable.Icon;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
@ -18,11 +23,13 @@ import org.joinmastodon.android.E;
import org.joinmastodon.android.MainActivity; import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.CacheController;
import org.joinmastodon.android.api.DatabaseRunnable;
import org.joinmastodon.android.api.MastodonAPIController; import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.PushSubscriptionManager; import org.joinmastodon.android.api.PushSubscriptionManager;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.filters.GetLegacyFilters; import org.joinmastodon.android.api.requests.filters.GetLegacyFilters;
import org.joinmastodon.android.api.requests.instance.GetCustomEmojis; 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.instance.GetInstance;
import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp; import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp;
import org.joinmastodon.android.events.EmojiUpdatedEvent; 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.Application;
import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.EmojiCategory; import org.joinmastodon.android.model.EmojiCategory;
import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.LegacyFilter;
import org.joinmastodon.android.model.Token; import org.joinmastodon.android.model.Token;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
@ -60,6 +68,7 @@ public class AccountSessionManager{
private static final String TAG="AccountSessionManager"; private static final String TAG="AccountSessionManager";
public static final String SCOPE="read write follow push"; public static final String SCOPE="read write follow push";
public static final String REDIRECT_URI="mastodon-android-auth://callback"; public static final String REDIRECT_URI="mastodon-android-auth://callback";
private static final int DB_VERSION=1;
private static final AccountSessionManager instance=new AccountSessionManager(); private static final AccountSessionManager instance=new AccountSessionManager();
@ -73,6 +82,8 @@ public class AccountSessionManager{
private String lastActiveAccountID; private String lastActiveAccountID;
private SharedPreferences prefs; private SharedPreferences prefs;
private boolean loadedInstances; private boolean loadedInstances;
private DatabaseHelper db;
private final Runnable databaseCloseRunnable=this::closeDatabase;
public static AccountSessionManager getInstance(){ public static AccountSessionManager getInstance(){
return instance; return instance;
@ -442,6 +453,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{ private static class SessionsStorageWrapper{
public List<AccountSession> accounts; public List<AccountSession> accounts;
} }
@ -451,4 +524,24 @@ public class AccountSessionManager{
public List<Emoji> emojis; public List<Emoji> emojis;
public long lastUpdated; 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,52 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.view.View;
import android.webkit.WebResourceRequest;
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/donation/success";
public static final String FAILURE_URL="https://sponsor.joinmastodon.org/donation/failure";
@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)){
new M3AlertDialogBuilder(getActivity())
.setTitle("Success")
.setMessage("Some sort of UI that would tell the user that their payment was successful")
.setPositiveButton(R.string.ok, null)
.setOnDismissListener(dlg->Nav.finish(this))
.show();
String campaignID=getArguments().getString("campaignID");
AccountSessionManager.getInstance().markDonationCampaignAsDismissed(campaignID);
E.post(new DismissDonationCampaignBannerEvent(campaignID));
return true;
}else if(url.equalsIgnoreCase(FAILURE_URL)){
new M3AlertDialogBuilder(getActivity())
.setTitle("Failure")
.setMessage("Some sort of UI that would tell the user that their payment didn't go through")
.setPositiveButton(R.string.ok, null)
.setOnDismissListener(dlg->Nav.finish(this))
.show();
return true;
}
return false;
}
}

View File

@ -7,13 +7,19 @@ import android.animation.ObjectAnimator;
import android.app.Activity; import android.app.Activity;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.TextUtils; 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.Gravity;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewStub;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo;
import android.view.animation.AnimationUtils; import android.view.animation.AnimationUtils;
import android.widget.Button; import android.widget.Button;
@ -29,11 +35,13 @@ import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E; import org.joinmastodon.android.E;
import org.joinmastodon.android.R; import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest; 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.markers.SaveMarkers;
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline; import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
import org.joinmastodon.android.api.requests.timelines.GetListTimeline; import org.joinmastodon.android.api.requests.timelines.GetListTimeline;
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline; import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.DismissDonationCampaignBannerEvent;
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent; import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
import org.joinmastodon.android.fragments.settings.SettingsMainFragment; import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
import org.joinmastodon.android.model.CacheablePaginatedResponse; import org.joinmastodon.android.model.CacheablePaginatedResponse;
@ -41,8 +49,10 @@ import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.FollowList; import org.joinmastodon.android.model.FollowList;
import org.joinmastodon.android.model.Status; import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.TimelineMarkers; 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.GapStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.sheets.DonationSheet;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.viewcontrollers.HomeTimelineMenuController; import org.joinmastodon.android.ui.viewcontrollers.HomeTimelineMenuController;
import org.joinmastodon.android.ui.viewcontrollers.ToolbarDropdownMenuController; import org.joinmastodon.android.ui.viewcontrollers.ToolbarDropdownMenuController;
@ -53,6 +63,7 @@ import org.parceler.Parcels;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Set; import java.util.Set;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -81,9 +92,12 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
private FollowList currentList; private FollowList currentList;
private MergeRecyclerAdapter mergeAdapter; private MergeRecyclerAdapter mergeAdapter;
private DiscoverInfoBannerHelper localTimelineBannerHelper; private DiscoverInfoBannerHelper localTimelineBannerHelper;
private View donationBanner;
private boolean donationBannerDismissing;
private String maxID; private String maxID;
private String lastSavedMarkerID; private String lastSavedMarkerID;
private DonationCampaign currentDonationCampaign;
public HomeTimelineFragment(){ public HomeTimelineFragment(){
setListLayoutId(R.layout.fragment_timeline); setListLayoutId(R.layout.fragment_timeline);
@ -93,6 +107,23 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
public void onCreate(Bundle savedInstanceState){ public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
localTimelineBannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE, accountID); localTimelineBannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE, accountID);
// TODO how often do we do this request? Maybe cache something somewhere?
if(AccountSessionManager.get(accountID).isEligibleForDonations()){
new GetDonationCampaigns(Locale.getDefault().toLanguageTag().replace('-', '_'), String.valueOf(AccountSessionManager.get(accountID).getDonationSeed()))
.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("");
}
} }
@Override @Override
@ -599,6 +630,12 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
updateUpdateState(ev.state); updateUpdateState(ev.state);
} }
public void onDismissDonationCampaignBanner(DismissDonationCampaignBannerEvent ev){
if(currentDonationCampaign!=null && ev.campaignID.equals(currentDonationCampaign.id)){
dismissDonationBanner();
}
}
@Override @Override
protected boolean shouldRemoveAccountPostsWhenUnfollowing(){ protected boolean shouldRemoveAccountPostsWhenUnfollowing(){
return true; return true;
@ -661,6 +698,75 @@ 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(){
new DonationSheet(getActivity(), currentDonationCampaign, accountID).show();
}
private enum ListMode{ private enum ListMode{
FOLLOWING, FOLLOWING,
LOCAL, LOCAL,

View File

@ -0,0 +1,76 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
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.api.MastodonErrorResponse;
import me.grishka.appkit.Nav;
import me.grishka.appkit.fragments.LoaderFragment;
import me.grishka.appkit.fragments.OnBackPressedListener;
public abstract class WebViewFragment extends LoaderFragment implements OnBackPressedListener{
protected WebView webView;
@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){
dataLoaded();
}
@Override
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error){
onError(new MastodonErrorResponse(error.getDescription().toString(), -1, null));
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request){
return WebViewFragment.this.shouldOverrideUrlLoading(request);
}
});
webView.getSettings().setJavaScriptEnabled(true);
return webView;
}
@Override
protected void doLoadData(){
}
@Override
public void onRefresh(){
webView.reload();
}
@Override
public boolean onBackPressed(){
if(webView.canGoBack()){
webView.goBack();
return true;
}
return false;
}
@Override
public void onToolbarNavigationClick(){
Nav.finish(this);
}
protected abstract boolean shouldOverrideUrlLoading(WebResourceRequest req);
}

View File

@ -28,7 +28,8 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
selfUpdateItem=new ListItem<>("Force self-update", null, this::onForceSelfUpdateClick), selfUpdateItem=new ListItem<>("Force self-update", null, this::onForceSelfUpdateClick),
resetUpdateItem=new ListItem<>("Reset self-updater", null, this::onResetUpdaterClick), resetUpdateItem=new ListItem<>("Reset self-updater", null, this::onResetUpdaterClick),
new ListItem<>("Reset search info banners", null, this::onResetDiscoverBannersClick), 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)
)); ));
if(!GithubSelfUpdater.needSelfUpdating()){ if(!GithubSelfUpdater.needSelfUpdating()){
resetUpdateItem.isEnabled=selfUpdateItem.isEnabled=false; resetUpdateItem.isEnabled=selfUpdateItem.isEnabled=false;
@ -70,6 +71,11 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
Toast.makeText(getActivity(), "Pre-reply sheets were reset", Toast.LENGTH_SHORT).show(); 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(){ private void restartUI(){
Bundle args=new Bundle(); Bundle args=new Bundle();
args.putString("account", accountID); args.putString("account", accountID);

View File

@ -0,0 +1,33 @@
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;
@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,288 @@
package org.joinmastodon.android.ui.sheets;
import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ToggleButton;
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.Currency;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import me.grishka.appkit.Nav;
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 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){
super(activity);
this.campaign=campaign;
this.accountID=accountID;
this.activity=activity;
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);
LinearLayout 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;
LinearLayout.LayoutParams lp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT);
lp.rightMargin=V.dp(8);
suggestedAmounts.addView(btn, lp);
}
updateSuggestedAmounts(campaign.defaultCurrency);
button.setEnabled(false);
buttonText.setText(campaign.bannerButtonText);
button.setOnClickListener(v->openWebView());
}
@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("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);
Nav.go(activity, DonationWebViewFragment.class, args);
dismiss();
}
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
}
}

View File

@ -0,0 +1,336 @@
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());
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);
this.paint.setAlpha(77);
}
@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){
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);
}
}

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"?> <?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <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"> <item android:state_selected="true">
<ripple android:color="@color/m3_on_secondary_container_overlay"> <ripple android:color="@color/m3_on_secondary_container_overlay">
<item> <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,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="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,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"/> android:text="@string/see_new_posts"/>
</FrameLayout> </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> </FrameLayout>
</me.grishka.appkit.views.RecursiveSwipeRefreshLayout> </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"/>
<LinearLayout
android:id="@+id/suggested_amounts"
android:layout_width="match_parent"
android:layout_height="32dp"
android:orientation="horizontal"
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"
tools:text="Donate"/>
</FrameLayout>
</LinearLayout>
</org.joinmastodon.android.ui.views.CustomScrollView>

View File

@ -0,0 +1,336 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- light -->
<color name="masterialLight_primary">#5E5791</color>
<color name="masterialLight_onPrimary">#FFFFFF</color>
<color name="masterialLight_primaryContainer">#E5DEFF</color>
<color name="masterialLight_onPrimaryContainer">#1A1249</color>
<color name="masterialLight_secondary">#5F5C71</color>
<color name="masterialLight_onSecondary">#FFFFFF</color>
<color name="masterialLight_secondaryContainer">#E5DFF9</color>
<color name="masterialLight_onSecondaryContainer">#1C192C</color>
<color name="masterialLight_tertiary">#7B5265</color>
<color name="masterialLight_onTertiary">#FFFFFF</color>
<color name="masterialLight_tertiaryContainer">#FFD8E7</color>
<color name="masterialLight_onTertiaryContainer">#301121</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">#1C1B20</color>
<color name="masterialLight_surface">#FCF8FF</color>
<color name="masterialLight_onSurface">#1C1B20</color>
<color name="masterialLight_surfaceVariant">#E5E0EC</color>
<color name="masterialLight_onSurfaceVariant">#47464F</color>
<color name="masterialLight_outline">#78767F</color>
<color name="masterialLight_outlineVariant">#C9C5D0</color>
<color name="masterialLight_scrim">#000000</color>
<color name="masterialLight_inverseSurface">#313036</color>
<color name="masterialLight_inverseOnSurface">#F4EFF7</color>
<color name="masterialLight_inversePrimary">#C7BFFF</color>
<color name="masterialLight_primaryFixed">#E5DEFF</color>
<color name="masterialLight_onPrimaryFixed">#1A1249</color>
<color name="masterialLight_primaryFixedDim">#C7BFFF</color>
<color name="masterialLight_onPrimaryFixedVariant">#463F77</color>
<color name="masterialLight_secondaryFixed">#E5DFF9</color>
<color name="masterialLight_onSecondaryFixed">#1C192C</color>
<color name="masterialLight_secondaryFixedDim">#C8C3DC</color>
<color name="masterialLight_onSecondaryFixedVariant">#474459</color>
<color name="masterialLight_tertiaryFixed">#FFD8E7</color>
<color name="masterialLight_onTertiaryFixed">#301121</color>
<color name="masterialLight_tertiaryFixedDim">#ECB8CE</color>
<color name="masterialLight_onTertiaryFixedVariant">#613B4D</color>
<color name="masterialLight_surfaceDim">#DDD8E0</color>
<color name="masterialLight_surfaceBright">#FCF8FF</color>
<color name="masterialLight_surfaceContainerLowest">#FFFFFF</color>
<color name="masterialLight_surfaceContainerLow">#F7F2FA</color>
<color name="masterialLight_surfaceContainer">#F1ECF4</color>
<color name="masterialLight_surfaceContainerHigh">#EBE6EF</color>
<color name="masterialLight_surfaceContainerHighest">#E5E1E9</color>
<color name="masterialLight_primary_mediumContrast">#423B73</color>
<color name="masterialLight_onPrimary_mediumContrast">#FFFFFF</color>
<color name="masterialLight_primaryContainer_mediumContrast">#746DA9</color>
<color name="masterialLight_onPrimaryContainer_mediumContrast">#FFFFFF</color>
<color name="masterialLight_secondary_mediumContrast">#434055</color>
<color name="masterialLight_onSecondary_mediumContrast">#FFFFFF</color>
<color name="masterialLight_secondaryContainer_mediumContrast">#767288</color>
<color name="masterialLight_onSecondaryContainer_mediumContrast">#FFFFFF</color>
<color name="masterialLight_tertiary_mediumContrast">#5D3749</color>
<color name="masterialLight_onTertiary_mediumContrast">#FFFFFF</color>
<color name="masterialLight_tertiaryContainer_mediumContrast">#94687B</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">#1C1B20</color>
<color name="masterialLight_surface_mediumContrast">#FCF8FF</color>
<color name="masterialLight_onSurface_mediumContrast">#1C1B20</color>
<color name="masterialLight_surfaceVariant_mediumContrast">#E5E0EC</color>
<color name="masterialLight_onSurfaceVariant_mediumContrast">#44424B</color>
<color name="masterialLight_outline_mediumContrast">#605E67</color>
<color name="masterialLight_outlineVariant_mediumContrast">#7C7983</color>
<color name="masterialLight_scrim_mediumContrast">#000000</color>
<color name="masterialLight_inverseSurface_mediumContrast">#313036</color>
<color name="masterialLight_inverseOnSurface_mediumContrast">#F4EFF7</color>
<color name="masterialLight_inversePrimary_mediumContrast">#C7BFFF</color>
<color name="masterialLight_primaryFixed_mediumContrast">#746DA9</color>
<color name="masterialLight_onPrimaryFixed_mediumContrast">#FFFFFF</color>
<color name="masterialLight_primaryFixedDim_mediumContrast">#5C558E</color>
<color name="masterialLight_onPrimaryFixedVariant_mediumContrast">#FFFFFF</color>
<color name="masterialLight_secondaryFixed_mediumContrast">#767288</color>
<color name="masterialLight_onSecondaryFixed_mediumContrast">#FFFFFF</color>
<color name="masterialLight_secondaryFixedDim_mediumContrast">#5D596F</color>
<color name="masterialLight_onSecondaryFixedVariant_mediumContrast">#FFFFFF</color>
<color name="masterialLight_tertiaryFixed_mediumContrast">#94687B</color>
<color name="masterialLight_onTertiaryFixed_mediumContrast">#FFFFFF</color>
<color name="masterialLight_tertiaryFixedDim_mediumContrast">#795063</color>
<color name="masterialLight_onTertiaryFixedVariant_mediumContrast">#FFFFFF</color>
<color name="masterialLight_surfaceDim_mediumContrast">#DDD8E0</color>
<color name="masterialLight_surfaceBright_mediumContrast">#FCF8FF</color>
<color name="masterialLight_surfaceContainerLowest_mediumContrast">#FFFFFF</color>
<color name="masterialLight_surfaceContainerLow_mediumContrast">#F7F2FA</color>
<color name="masterialLight_surfaceContainer_mediumContrast">#F1ECF4</color>
<color name="masterialLight_surfaceContainerHigh_mediumContrast">#EBE6EF</color>
<color name="masterialLight_surfaceContainerHighest_mediumContrast">#E5E1E9</color>
<color name="masterialLight_primary_highContrast">#211950</color>
<color name="masterialLight_onPrimary_highContrast">#FFFFFF</color>
<color name="masterialLight_primaryContainer_highContrast">#423B73</color>
<color name="masterialLight_onPrimaryContainer_highContrast">#FFFFFF</color>
<color name="masterialLight_secondary_highContrast">#222033</color>
<color name="masterialLight_onSecondary_highContrast">#FFFFFF</color>
<color name="masterialLight_secondaryContainer_highContrast">#434055</color>
<color name="masterialLight_onSecondaryContainer_highContrast">#FFFFFF</color>
<color name="masterialLight_tertiary_highContrast">#381728</color>
<color name="masterialLight_onTertiary_highContrast">#FFFFFF</color>
<color name="masterialLight_tertiaryContainer_highContrast">#5D3749</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">#1C1B20</color>
<color name="masterialLight_surface_highContrast">#FCF8FF</color>
<color name="masterialLight_onSurface_highContrast">#000000</color>
<color name="masterialLight_surfaceVariant_highContrast">#E5E0EC</color>
<color name="masterialLight_onSurfaceVariant_highContrast">#24232B</color>
<color name="masterialLight_outline_highContrast">#44424B</color>
<color name="masterialLight_outlineVariant_highContrast">#44424B</color>
<color name="masterialLight_scrim_highContrast">#000000</color>
<color name="masterialLight_inverseSurface_highContrast">#313036</color>
<color name="masterialLight_inverseOnSurface_highContrast">#FFFFFF</color>
<color name="masterialLight_inversePrimary_highContrast">#EFE9FF</color>
<color name="masterialLight_primaryFixed_highContrast">#423B73</color>
<color name="masterialLight_onPrimaryFixed_highContrast">#FFFFFF</color>
<color name="masterialLight_primaryFixedDim_highContrast">#2C255B</color>
<color name="masterialLight_onPrimaryFixedVariant_highContrast">#FFFFFF</color>
<color name="masterialLight_secondaryFixed_highContrast">#434055</color>
<color name="masterialLight_onSecondaryFixed_highContrast">#FFFFFF</color>
<color name="masterialLight_secondaryFixedDim_highContrast">#2D2A3E</color>
<color name="masterialLight_onSecondaryFixedVariant_highContrast">#FFFFFF</color>
<color name="masterialLight_tertiaryFixed_highContrast">#5D3749</color>
<color name="masterialLight_onTertiaryFixed_highContrast">#FFFFFF</color>
<color name="masterialLight_tertiaryFixedDim_highContrast">#442233</color>
<color name="masterialLight_onTertiaryFixedVariant_highContrast">#FFFFFF</color>
<color name="masterialLight_surfaceDim_highContrast">#DDD8E0</color>
<color name="masterialLight_surfaceBright_highContrast">#FCF8FF</color>
<color name="masterialLight_surfaceContainerLowest_highContrast">#FFFFFF</color>
<color name="masterialLight_surfaceContainerLow_highContrast">#F7F2FA</color>
<color name="masterialLight_surfaceContainer_highContrast">#F1ECF4</color>
<color name="masterialLight_surfaceContainerHigh_highContrast">#EBE6EF</color>
<color name="masterialLight_surfaceContainerHighest_highContrast">#E5E1E9</color>
<color name="masterialLight_colorGoldenrod">#7A590C</color>
<color name="masterialLight_colorOnGoldenrod">#FFFFFF</color>
<color name="masterialLight_colorGoldenrodContainer">#FFDEA6</color>
<color name="masterialLight_colorOnGoldenrodContainer">#271900</color>
<color name="masterialLight_colorLime">#4F6628</color>
<color name="masterialLight_colorOnLime">#FFFFFF</color>
<color name="masterialLight_colorLimeContainer">#D1ECA0</color>
<color name="masterialLight_colorOnLimeContainer">#121F00</color>
<color name="masterialLight_colorGoldenrod_mediumContrast">#583E00</color>
<color name="masterialLight_colorOnGoldenrod_mediumContrast">#FFFFFF</color>
<color name="masterialLight_colorGoldenrodContainer_mediumContrast">#936F23</color>
<color name="masterialLight_colorOnGoldenrodContainer_mediumContrast">#FFFFFF</color>
<color name="masterialLight_colorLime_mediumContrast">#34490F</color>
<color name="masterialLight_colorOnLime_mediumContrast">#FFFFFF</color>
<color name="masterialLight_colorLimeContainer_mediumContrast">#657C3C</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">#34490F</color>
<color name="masterialLight_colorOnLimeContainer_highContrast">#FFFFFF</color>
<!-- dark -->
<color name="masterialDark_primary">#C7BFFF</color>
<color name="masterialDark_onPrimary">#2F285F</color>
<color name="masterialDark_primaryContainer">#463F77</color>
<color name="masterialDark_onPrimaryContainer">#E5DEFF</color>
<color name="masterialDark_secondary">#C8C3DC</color>
<color name="masterialDark_onSecondary">#312E41</color>
<color name="masterialDark_secondaryContainer">#474459</color>
<color name="masterialDark_onSecondaryContainer">#E5DFF9</color>
<color name="masterialDark_tertiary">#ECB8CE</color>
<color name="masterialDark_onTertiary">#482536</color>
<color name="masterialDark_tertiaryContainer">#613B4D</color>
<color name="masterialDark_onTertiaryContainer">#FFD8E7</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">#141318</color>
<color name="masterialDark_onBackground">#E5E1E9</color>
<color name="masterialDark_surface">#141318</color>
<color name="masterialDark_onSurface">#E5E1E9</color>
<color name="masterialDark_surfaceVariant">#47464F</color>
<color name="masterialDark_onSurfaceVariant">#C9C5D0</color>
<color name="masterialDark_outline">#928F99</color>
<color name="masterialDark_outlineVariant">#47464F</color>
<color name="masterialDark_scrim">#000000</color>
<color name="masterialDark_inverseSurface">#E5E1E9</color>
<color name="masterialDark_inverseOnSurface">#313036</color>
<color name="masterialDark_inversePrimary">#5E5791</color>
<color name="masterialDark_primaryFixed">#E5DEFF</color>
<color name="masterialDark_onPrimaryFixed">#1A1249</color>
<color name="masterialDark_primaryFixedDim">#C7BFFF</color>
<color name="masterialDark_onPrimaryFixedVariant">#463F77</color>
<color name="masterialDark_secondaryFixed">#E5DFF9</color>
<color name="masterialDark_onSecondaryFixed">#1C192C</color>
<color name="masterialDark_secondaryFixedDim">#C8C3DC</color>
<color name="masterialDark_onSecondaryFixedVariant">#474459</color>
<color name="masterialDark_tertiaryFixed">#FFD8E7</color>
<color name="masterialDark_onTertiaryFixed">#301121</color>
<color name="masterialDark_tertiaryFixedDim">#ECB8CE</color>
<color name="masterialDark_onTertiaryFixedVariant">#613B4D</color>
<color name="masterialDark_surfaceDim">#141318</color>
<color name="masterialDark_surfaceBright">#3A383E</color>
<color name="masterialDark_surfaceContainerLowest">#0E0E13</color>
<color name="masterialDark_surfaceContainerLow">#1C1B20</color>
<color name="masterialDark_surfaceContainer">#201F25</color>
<color name="masterialDark_surfaceContainerHigh">#2A292F</color>
<color name="masterialDark_surfaceContainerHighest">#35343A</color>
<color name="masterialDark_primary_mediumContrast">#CCC4FF</color>
<color name="masterialDark_onPrimary_mediumContrast">#150B44</color>
<color name="masterialDark_primaryContainer_mediumContrast">#9189C7</color>
<color name="masterialDark_onPrimaryContainer_mediumContrast">#000000</color>
<color name="masterialDark_secondary_mediumContrast">#CDC8E1</color>
<color name="masterialDark_onSecondary_mediumContrast">#161426</color>
<color name="masterialDark_secondaryContainer_mediumContrast">#928EA5</color>
<color name="masterialDark_onSecondaryContainer_mediumContrast">#000000</color>
<color name="masterialDark_tertiary_mediumContrast">#F1BCD2</color>
<color name="masterialDark_onTertiary_mediumContrast">#2A0B1C</color>
<color name="masterialDark_tertiaryContainer_mediumContrast">#B28498</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">#141318</color>
<color name="masterialDark_onBackground_mediumContrast">#E5E1E9</color>
<color name="masterialDark_surface_mediumContrast">#141318</color>
<color name="masterialDark_onSurface_mediumContrast">#FEF9FF</color>
<color name="masterialDark_surfaceVariant_mediumContrast">#47464F</color>
<color name="masterialDark_onSurfaceVariant_mediumContrast">#CDC9D4</color>
<color name="masterialDark_outline_mediumContrast">#A5A1AC</color>
<color name="masterialDark_outlineVariant_mediumContrast">#85828C</color>
<color name="masterialDark_scrim_mediumContrast">#000000</color>
<color name="masterialDark_inverseSurface_mediumContrast">#E5E1E9</color>
<color name="masterialDark_inverseOnSurface_mediumContrast">#2A292F</color>
<color name="masterialDark_inversePrimary_mediumContrast">#474179</color>
<color name="masterialDark_primaryFixed_mediumContrast">#E5DEFF</color>
<color name="masterialDark_onPrimaryFixed_mediumContrast">#0F053F</color>
<color name="masterialDark_primaryFixedDim_mediumContrast">#C7BFFF</color>
<color name="masterialDark_onPrimaryFixedVariant_mediumContrast">#352E65</color>
<color name="masterialDark_secondaryFixed_mediumContrast">#E5DFF9</color>
<color name="masterialDark_onSecondaryFixed_mediumContrast">#110F21</color>
<color name="masterialDark_secondaryFixedDim_mediumContrast">#C8C3DC</color>
<color name="masterialDark_onSecondaryFixedVariant_mediumContrast">#363447</color>
<color name="masterialDark_tertiaryFixed_mediumContrast">#FFD8E7</color>
<color name="masterialDark_onTertiaryFixed_mediumContrast">#230716</color>
<color name="masterialDark_tertiaryFixedDim_mediumContrast">#ECB8CE</color>
<color name="masterialDark_onTertiaryFixedVariant_mediumContrast">#4F2B3C</color>
<color name="masterialDark_surfaceDim_mediumContrast">#141318</color>
<color name="masterialDark_surfaceBright_mediumContrast">#3A383E</color>
<color name="masterialDark_surfaceContainerLowest_mediumContrast">#0E0E13</color>
<color name="masterialDark_surfaceContainerLow_mediumContrast">#1C1B20</color>
<color name="masterialDark_surfaceContainer_mediumContrast">#201F25</color>
<color name="masterialDark_surfaceContainerHigh_mediumContrast">#2A292F</color>
<color name="masterialDark_surfaceContainerHighest_mediumContrast">#35343A</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">#CDC8E1</color>
<color name="masterialDark_onSecondaryContainer_highContrast">#000000</color>
<color name="masterialDark_tertiary_highContrast">#FFF9F9</color>
<color name="masterialDark_onTertiary_highContrast">#000000</color>
<color name="masterialDark_tertiaryContainer_highContrast">#F1BCD2</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">#141318</color>
<color name="masterialDark_onBackground_highContrast">#E5E1E9</color>
<color name="masterialDark_surface_highContrast">#141318</color>
<color name="masterialDark_onSurface_highContrast">#FFFFFF</color>
<color name="masterialDark_surfaceVariant_highContrast">#47464F</color>
<color name="masterialDark_onSurfaceVariant_highContrast">#FEF9FF</color>
<color name="masterialDark_outline_highContrast">#CDC9D4</color>
<color name="masterialDark_outlineVariant_highContrast">#CDC9D4</color>
<color name="masterialDark_scrim_highContrast">#000000</color>
<color name="masterialDark_inverseSurface_highContrast">#E5E1E9</color>
<color name="masterialDark_inverseOnSurface_highContrast">#000000</color>
<color name="masterialDark_inversePrimary_highContrast">#292258</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">#150B44</color>
<color name="masterialDark_secondaryFixed_highContrast">#E9E3FD</color>
<color name="masterialDark_onSecondaryFixed_highContrast">#000000</color>
<color name="masterialDark_secondaryFixedDim_highContrast">#CDC8E1</color>
<color name="masterialDark_onSecondaryFixedVariant_highContrast">#161426</color>
<color name="masterialDark_tertiaryFixed_highContrast">#FFDEEA</color>
<color name="masterialDark_onTertiaryFixed_highContrast">#000000</color>
<color name="masterialDark_tertiaryFixedDim_highContrast">#F1BCD2</color>
<color name="masterialDark_onTertiaryFixedVariant_highContrast">#2A0B1C</color>
<color name="masterialDark_surfaceDim_highContrast">#141318</color>
<color name="masterialDark_surfaceBright_highContrast">#3A383E</color>
<color name="masterialDark_surfaceContainerLowest_highContrast">#0E0E13</color>
<color name="masterialDark_surfaceContainerLow_highContrast">#1C1B20</color>
<color name="masterialDark_surfaceContainer_highContrast">#201F25</color>
<color name="masterialDark_surfaceContainerHigh_highContrast">#2A292F</color>
<color name="masterialDark_surfaceContainerHighest_highContrast">#35343A</color>
<color name="masterialDark_colorGoldenrod">#EDC06C</color>
<color name="masterialDark_colorOnGoldenrod">#412D00</color>
<color name="masterialDark_colorGoldenrodContainer">#5D4200</color>
<color name="masterialDark_colorOnGoldenrodContainer">#FFDEA6</color>
<color name="masterialDark_colorLime">#B5D087</color>
<color name="masterialDark_colorOnLime">#233600</color>
<color name="masterialDark_colorLimeContainer">#384D12</color>
<color name="masterialDark_colorOnLimeContainer">#D1ECA0</color>
<color name="masterialDark_colorGoldenrod_mediumContrast">#F1C470</color>
<color name="masterialDark_colorOnGoldenrod_mediumContrast">#201400</color>
<color name="masterialDark_colorGoldenrodContainer_mediumContrast">#B28B3D</color>
<color name="masterialDark_colorOnGoldenrodContainer_mediumContrast">#000000</color>
<color name="masterialDark_colorLime_mediumContrast">#B9D48A</color>
<color name="masterialDark_colorOnLime_mediumContrast">#0E1900</color>
<color name="masterialDark_colorLimeContainer_mediumContrast">#809956</color>
<color name="masterialDark_colorOnLimeContainer_mediumContrast">#000000</color>
<color name="masterialDark_colorGoldenrod_highContrast">#FFFAF7</color>
<color name="masterialDark_colorOnGoldenrod_highContrast">#000000</color>
<color name="masterialDark_colorGoldenrodContainer_highContrast">#F1C470</color>
<color name="masterialDark_colorOnGoldenrodContainer_highContrast">#000000</color>
<color name="masterialDark_colorLime_highContrast">#F5FFDC</color>
<color name="masterialDark_colorOnLime_highContrast">#000000</color>
<color name="masterialDark_colorLimeContainer_highContrast">#B9D48A</color>
<color name="masterialDark_colorOnLimeContainer_highContrast">#000000</color>
</resources>

View File

@ -747,4 +747,9 @@
<string name="notifications_from_user">Notifications from %s</string> <string name="notifications_from_user">Notifications from %s</string>
<string name="notifications_muted">Notifications from %s have been dismissed.</string> <string name="notifications_muted">Notifications from %s have been dismissed.</string>
<string name="notifications_allowed">%s will now appear in your notification list.</string> <string name="notifications_allowed">%s will now appear in your notification list.</string>
<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>
</resources> </resources>