diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..d3b8a94b0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Screenshots and screen recordings** +If applicable, add screenshots (and screen recordings, if possible) to help explain your problem. + +**Version** +Megalodon version: [e.g. v1.1.4+fork.#] + +**Additional context** +- Does this issue also occur with the respective upstream release? (Please test using the respective `upstream-xxxxxx.apk` provided in [Releases](https://github.com/sk22/megalodon/releases)) No / Yes (`mastodon#…`) + + > In this case, please consider filing an [upstream bug report](https://github.com/mastodon/mastodon-android/issues) instead. If this bug is seriously impacting your usage or you think I might want to try to fix it for Megalodon, feel free to still create this issue! + +**Crash log** +If you know your way around Android development tools, please consider attaching a crash log, if possible. diff --git a/.github/ISSUE_TEMPLATE/feature-ui-request.md b/.github/ISSUE_TEMPLATE/feature-ui-request.md new file mode 100644 index 000000000..7a56c359a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-ui-request.md @@ -0,0 +1,20 @@ +--- +name: Feature/UI request +about: Suggest an idea for this project +title: '' +labels: feature +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +If applicable: a clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/something-else.md b/.github/ISSUE_TEMPLATE/something-else.md new file mode 100644 index 000000000..995600f1e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/something-else.md @@ -0,0 +1,10 @@ +--- +name: It's something else… +about: Issues that can't be categorized as feature requests or bug reports +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/fastlane/metadata/android/cs-CZ/full_description.txt b/fastlane/metadata/android/cs-CZ/full_description.txt new file mode 100644 index 000000000..7596fd3ad --- /dev/null +++ b/fastlane/metadata/android/cs-CZ/full_description.txt @@ -0,0 +1,16 @@ +Mastodon je největší decentralizovanou sociální sítí na internetu. Místo jediné webové stránky je to síť pro miliony uživatelů v nezávislých komunitách, ve kterých mohou všichni vzájemně a bezproblémově komunikovat. Bez ohledu na to, co vás baví, můžete se setkat s vášnivými lidmi, kteří o tom přispívají na Mastodon! + +Připojte se ke komunitě a vytvořte svůj profil. Najděte a sledujte fascinující lidi a přečtěte si jejich příspěvky v chronologické časové ose bez reklam. Vyjádřete se pomocí vlastních emoji, obrázků, GIFů, videí a zvuku v 500-znakových příspěvcích. Odpovězte na vlákna a boostujte příspěvky od kohokoliv, abyste mohli sdílet skvělé věci. Najděte nové účty pro sledování a populární hashtagy pro rozšíření vaší sítě. + +Mastodon je postaven se zaměřením na soukromí a bezpečnost. Rozhodněte, zda jsou vaše příspěvky sdíleny se vašimi sledujícími, jen s lidmi, které zmíníte, nebo s celým světem. Upozornění na obsah vám umožní skrýt příspěvky obsahující citlivý nebo spouštěcí materiál, dokud se s nimi nezačnete zabývat. Každá komunita má vlastní pokyny a moderátory, aby udržela své členy v bezpečí, a robustní blokování a nahlašovací nástroje pomáhácí předcházení zneužití. + +Více funkcí: + +• Tmavý režim: Čtěte příspěvky ve světlém, tmavém nebo pravém černém režimu +• Ankety: Požádejte sledující o jejich názor a sečtěte jejich hlasy +• Objevit: Populární hashtagy a účty jsou pryč na jedno klepnutí +• Oznámení: Dostávejte oznámení o nových sledujících, odpovědích a boostech +• Sdílení: Odesílání přímo do Mastodonu z libovolného seznamu sdílení v jakékoliv aplikaci +• Roztomilost: Naším maskotem je roztomilý slon, kterého čas od času uvidíte + +Mastodon je registrovaný neziskový projekt a vývojový program je podporován přímo vašimi dary. Neexistuje žádná reklama, žádná monetizace a žádný rizikový kapitál a máme v plánu to udržet. \ No newline at end of file diff --git a/fastlane/metadata/android/de-DE/full_description.txt b/fastlane/metadata/android/de-DE/full_description.txt new file mode 100644 index 000000000..9d2abfa3b --- /dev/null +++ b/fastlane/metadata/android/de-DE/full_description.txt @@ -0,0 +1,16 @@ +Mastodon ist das größte dezentralisierte soziale Netzwerk im Internet. Statt einer einzigen Webseite ist es ein Netzwerk von Millionen von Benutzer*innen in unabhängigen Gemeinschaften, die alle miteinander interagieren können. Egal, was du magst, auf Mastodon kannst du begeisterte Menschen treffen, die darüber schreiben! + +Tritt einer Gemeinschaft bei und erstelle dein Profil. Finde und folge faszinierenden Leuten und lies ihre Beiträge in einer werbefreien, chronologischen Zeitachse. Drücke dich mit eigenen Emojis, Bildern, GIFs, Videos und Klängen in 500-Zeichen-Beiträgen aus. Antworte auf Themen und teile Beiträge von anderen, um tolle Dinge zu verbreiten. Finde neue Konten zum Folgen und angesagte Hashtags, um dein Netzwerk zu erweitern. + +Mastodon wurde mit einem Schwerpunkt auf Privatsphäre und Sicherheit gebaut. Entscheide, ob du deine Beiträge mit deinen Followern, nur mit den Menschen, die du erwähnst, oder mit der ganzen Welt teilen möchtest. Mit Inhaltswarnungen kannst du Beiträge mit sensiblem oder bedenklichen Inhalten ausblenden, bis du bereit bist, dich damit auseinanderzusetzen. Jede Gemeinschaft hat ihre eigenen Regeln und Moderator*innen, um die Sicherheit ihrer Mitglieder zu gewährleisten, sowie robuste Sperr- und Meldewerkzeuge, um Missbrauch vorzubeugen. + +Weitere Funktionen: + +• Dunkler Modus: Beiträge im hellen, dunklen oder schwarzen Modus lesen +• Umfragen: frage deine Follower nach ihrer Meinung und zähle die Stimmen +• Entdecken: trendende Hashtags und Profile sind nur einen Fingertipp entfernt +• Benachrichtigungen: erhalte Benachrichtigungen über neue Follower, Antworten und geteilte Beiträge +• Teilen: veröffentliche auf Mastodon aus jeder beliebigen anderen App +• Niedlichkeit: unser Maskottchen ist ein entzückender Elefant und du wirst ihn von Zeit zu Zeit auftauchen sehen + +Mastodon ist eine eingetragene gemeinnützige Organisation und die Entwicklung wird direkt durch deine Spenden unterstützt. Es gibt keine Werbung, keine Monetarisierung und kein Risikokapital und so soll es auch bleiben. \ No newline at end of file diff --git a/fastlane/metadata/android/fil-PH/full_description.txt b/fastlane/metadata/android/fil-PH/full_description.txt new file mode 100644 index 000000000..69aa29ff9 --- /dev/null +++ b/fastlane/metadata/android/fil-PH/full_description.txt @@ -0,0 +1,16 @@ +Mastodon is the largest decentralized social network on the internet. Instead of a single website, it’s a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what you’re into, you can meet passionate people posting about it on Mastodon! + +Join a community and create your profile. Find and and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network. + +Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse. + +More features: + +• Dark Mode: Read posts in light, dark, or true black mode +• Polls: Ask followers for their opinion and tally the votes +• Explore: Trending hashtags and accounts are a tap away +• Notifications: Get notified about new follows, replies, and reblogs +• Sharing: Post directly to Mastodon from any share sheet in any app +• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time + +Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way. \ No newline at end of file diff --git a/fastlane/metadata/android/fil-PH/short_description.txt b/fastlane/metadata/android/fil-PH/short_description.txt new file mode 100644 index 000000000..8f5a9b847 --- /dev/null +++ b/fastlane/metadata/android/fil-PH/short_description.txt @@ -0,0 +1 @@ +Decentralized social network \ No newline at end of file diff --git a/fastlane/metadata/android/fil-PH/title.txt b/fastlane/metadata/android/fil-PH/title.txt new file mode 100644 index 000000000..8123241a0 --- /dev/null +++ b/fastlane/metadata/android/fil-PH/title.txt @@ -0,0 +1 @@ +Mastodon \ No newline at end of file diff --git a/fastlane/metadata/android/hu-HU/full_description.txt b/fastlane/metadata/android/hu-HU/full_description.txt new file mode 100644 index 000000000..8e76178ab --- /dev/null +++ b/fastlane/metadata/android/hu-HU/full_description.txt @@ -0,0 +1,16 @@ +A Mastodon a legnagyobb decentralizált közösségi hálózat az interneten. Egyetlen weboldal helyett, ez több millió felhasználóból álló, független közösségek hálózata, amelyek egymással kapcsolatba tudnak lépni, zökkenőmentesen. Nem számít, mi a hobbid, a Mastodonon találkozhatsz róla posztoló lelkes emberekkel! + +Csatlakozz egy közösséghez és készítsd el a profilodat. Keress és kövess lenyűgöző embereket, és olvasd egy reklámmentes, kronologikus idővonalon a bejegyzéseiket. Fejezd ki magad egyedi hangulatjelekkel, képekkel, GIFekkel, videókkal és hanggal, 500 karakter hosszúságú posztokban. Reply to threads and reblog posts from anyone to share great stuff. Fedezz fel új fiókokat amiket követhetsz és felkapott hashtageket, hogy bővíthesd a kapcsolataidat. + +A Mastodon az adatvédelemre és a biztonságra összpontosítva épült. Döntsd el, hogy a posztjaidat csak a követőiddel, csak azokkal akiket megemlítesz, vagy az egész világgal osztod meg. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse. + +More features: + +• Dark Mode: Read posts in light, dark, or true black mode +• Polls: Ask followers for their opinion and tally the votes +• Explore: Trending hashtags and accounts are a tap away +• Notifications: Get notified about new follows, replies, and reblogs +• Sharing: Post directly to Mastodon from any share sheet in any app +• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time + +Mastodon is a registered nonprofit and development is supported directly by your donations. There’s no advertising, no monetization, and no venture capital, and we plan to keep it that way. \ No newline at end of file diff --git a/fastlane/metadata/android/hu-HU/short_description.txt b/fastlane/metadata/android/hu-HU/short_description.txt new file mode 100644 index 000000000..bf21b3e31 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/short_description.txt @@ -0,0 +1 @@ +Decentralizált szociális hálózat \ No newline at end of file diff --git a/fastlane/metadata/android/hu-HU/title.txt b/fastlane/metadata/android/hu-HU/title.txt new file mode 100644 index 000000000..8123241a0 --- /dev/null +++ b/fastlane/metadata/android/hu-HU/title.txt @@ -0,0 +1 @@ +Mastodon \ No newline at end of file diff --git a/img/f-droid-badge.png b/img/f-droid-badge.png new file mode 100644 index 000000000..ca83f5a39 Binary files /dev/null and b/img/f-droid-badge.png differ diff --git a/mastodon/build.gradle b/mastodon/build.gradle index 14f0f0347..20b2a582f 100644 --- a/mastodon/build.gradle +++ b/mastodon/build.gradle @@ -32,9 +32,11 @@ android { githubRelease{ initWith release } - noFederatedRelease{ + playRelease{ initWith release - versionNameSuffix '-nofederated' + minifyEnabled true + shrinkResources true + versionNameSuffix '-play' } } compileOptions { diff --git a/mastodon/src/main/AndroidManifest.xml b/mastodon/src/main/AndroidManifest.xml index 1b6cffb53..b7118fc32 100644 --- a/mastodon/src/main/AndroidManifest.xml +++ b/mastodon/src/main/AndroidManifest.xml @@ -14,7 +14,7 @@ mediaUris; if(Intent.ACTION_SEND.equals(intent.getAction())){ Uri singleUri=intent.getParcelableExtra(Intent.EXTRA_STREAM); diff --git a/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java b/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java index 67d0434c8..80c8bb6ad 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java +++ b/mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java @@ -10,9 +10,11 @@ public class GlobalUserPreferences{ public static boolean showReplies; public static boolean showBoosts; public static boolean loadNewPosts; + public static boolean showFederatedTimeline; public static boolean showInteractionCounts; public static boolean alwaysExpandContentWarnings; public static boolean disableMarquee; + public static boolean voteButtonForSingleChoice; public static ThemePreference theme; public static ColorPreference color; @@ -28,9 +30,11 @@ public class GlobalUserPreferences{ showReplies=prefs.getBoolean("showReplies", true); showBoosts=prefs.getBoolean("showBoosts", true); loadNewPosts=prefs.getBoolean("loadNewPosts", true); + showFederatedTimeline=prefs.getBoolean("showFederatedTimeline", !BuildConfig.BUILD_TYPE.equals("playRelease")); showInteractionCounts=prefs.getBoolean("showInteractionCounts", false); alwaysExpandContentWarnings=prefs.getBoolean("alwaysExpandContentWarnings", false); disableMarquee=prefs.getBoolean("disableMarquee", false); + voteButtonForSingleChoice=prefs.getBoolean("voteButtonForSingleChoice", true); theme=ThemePreference.values()[prefs.getInt("theme", 0)]; color=ColorPreference.values()[prefs.getInt("color", 1)]; } @@ -42,6 +46,7 @@ public class GlobalUserPreferences{ .putBoolean("showReplies", showReplies) .putBoolean("showBoosts", showBoosts) .putBoolean("loadNewPosts", loadNewPosts) + .putBoolean("showFederatedTimeline", showFederatedTimeline) .putBoolean("trueBlackTheme", trueBlackTheme) .putBoolean("showInteractionCounts", showInteractionCounts) .putBoolean("alwaysExpandContentWarnings", alwaysExpandContentWarnings) @@ -66,4 +71,3 @@ public class GlobalUserPreferences{ DARK } } - diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java index 8e70d4cbf..71833634e 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java @@ -400,10 +400,12 @@ public abstract class BaseStatusListFragment exten public void onPollOptionClick(PollOptionStatusDisplayItem.Holder holder){ Poll poll=holder.getItem().poll; Poll.Option option=holder.getItem().option; - if(poll.multiple){ + if(poll.multiple || GlobalUserPreferences.voteButtonForSingleChoice){ if(poll.selectedOptions==null) poll.selectedOptions=new ArrayList<>(); - if(poll.selectedOptions.contains(option)){ + boolean optionContained=poll.selectedOptions.contains(option); + if(!poll.multiple) poll.selectedOptions.clear(); + if(optionContained){ poll.selectedOptions.remove(option); holder.itemView.setSelected(false); }else{ @@ -412,6 +414,9 @@ public abstract class BaseStatusListFragment exten } for(int i=0;i R.string.visibility_public; - case UNLISTED -> R.string.visibility_unlisted; + case UNLISTED -> R.string.sk_visibility_unlisted; case PRIVATE -> R.string.visibility_followers_only; case DIRECT -> R.string.visibility_private; }; diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/FollowRequestsListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/FollowRequestsListFragment.java index 2fec328d4..bd9ff0a2b 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/FollowRequestsListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/FollowRequestsListFragment.java @@ -67,7 +67,7 @@ public class FollowRequestsListFragment extends BaseRecyclerFragment=Build.VERSION_CODES.N) setRetainInstance(true); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ListTimelinesFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ListTimelinesFragment.java index a78f256c5..96c567161 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ListTimelinesFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ListTimelinesFragment.java @@ -57,7 +57,7 @@ public class ListTimelinesFragment extends BaseRecyclerFragment im if(args.containsKey("profileAccount")){ profileAccountId=args.getString("profileAccount"); profileDisplayUsername=args.getString("profileDisplayUsername"); - setTitle(getString(R.string.lists_with_user, profileDisplayUsername)); + setTitle(getString(R.string.sk_lists_with_user, profileDisplayUsername)); // setHasOptionsMenu(true); } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java index 0e1345827..39d665d0a 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java @@ -249,7 +249,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList tab.setText(switch(position){ case 0 -> R.string.posts; case 1 -> R.string.posts_and_replies; - case 2 -> R.string.pinned_posts; + case 2 -> R.string.sk_pinned_posts; case 3 -> R.string.media; case 4 -> R.string.profile_about; default -> throw new IllegalStateException(); @@ -555,11 +555,14 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList menu.findItem(R.id.mute).setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername())); menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername())); menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getDisplayUsername())); + MenuItem manageUserLists=menu.findItem(R.id.manage_user_lists); if(relationship.following) { menu.findItem(R.id.hide_boosts).setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getDisplayUsername())); + manageUserLists.setTitle(getString(R.string.sk_lists_with_user, account.getDisplayUsername())); + manageUserLists.setVisible(true); }else { menu.findItem(R.id.hide_boosts).setVisible(false); - menu.findItem(R.id.manage_user_lists).setVisible(false); + manageUserLists.setVisible(false); } if(!account.isLocal()) menu.findItem(R.id.block_domain).setTitle(getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain())); @@ -658,7 +661,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList notifyProgress.setIndeterminateTintList(notifyButton.getTextColors()); followsYouView.setVisibility(relationship.followedBy ? View.VISIBLE : View.GONE); notifyButton.setSelected(relationship.notifying); - if (getActivity() != null) notifyButton.setContentDescription(getString(relationship.notifying ? R.string.user_post_notifications_on : R.string.user_post_notifications_off, '@'+account.username)); + if (getActivity() != null) notifyButton.setContentDescription(getString(relationship.notifying ? R.string.sk_user_post_notifications_on : R.string.sk_user_post_notifications_off, '@'+account.username)); } private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/SettingsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/SettingsFragment.java index 6b82d334d..3b7a8e5d7 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/SettingsFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/SettingsFragment.java @@ -9,14 +9,12 @@ import android.graphics.Canvas; import android.graphics.Rect; import android.os.Build; import android.os.Bundle; -import android.provider.Settings; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; import android.view.WindowManager; -import android.view.animation.AlphaAnimation; import android.view.animation.LinearInterpolator; import android.widget.Button; import android.widget.ImageButton; @@ -71,6 +69,7 @@ public class SettingsFragment extends MastodonToolbarFragment{ private NotificationPolicyItem notificationPolicyItem; private String accountID; private boolean needUpdateNotificationSettings; + private boolean needAppRestart; private PushSubscription pushSubscription; private ImageView themeTransitionWindowView; @@ -133,6 +132,11 @@ public class SettingsFragment extends MastodonToolbarFragment{ GlobalUserPreferences.loadNewPosts=i.checked; GlobalUserPreferences.save(); })); + items.add(new SwitchItem(R.string.sk_settings_show_federated_timeline, R.drawable.ic_fluent_earth_24_regular, GlobalUserPreferences.showFederatedTimeline, i->{ + GlobalUserPreferences.showFederatedTimeline=i.checked; + GlobalUserPreferences.save(); + needAppRestart=true; + })); items.add(new HeaderItem(R.string.settings_notifications)); items.add(notificationPolicyItem=new NotificationPolicyItem()); @@ -141,6 +145,7 @@ public class SettingsFragment extends MastodonToolbarFragment{ items.add(new SwitchItem(R.string.notify_follow, R.drawable.ic_fluent_person_add_24_regular, pushSubscription.alerts.follow, i->onNotificationsChanged(PushNotification.Type.FOLLOW, i.checked))); items.add(new SwitchItem(R.string.notify_reblog, R.drawable.ic_fluent_arrow_repeat_all_24_regular, pushSubscription.alerts.reblog, i->onNotificationsChanged(PushNotification.Type.REBLOG, i.checked))); items.add(new SwitchItem(R.string.notify_mention, R.drawable.ic_at_symbol, pushSubscription.alerts.mention, i->onNotificationsChanged(PushNotification.Type.MENTION, i.checked))); + items.add(new SwitchItem(R.string.sk_notify_posts, R.drawable.ic_fluent_alert_24_regular, pushSubscription.alerts.status, i->onNotificationsChanged(PushNotification.Type.STATUS, i.checked))); items.add(new HeaderItem(R.string.settings_boring)); items.add(new TextItem(R.string.settings_account, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/auth/edit"))); @@ -152,7 +157,7 @@ public class SettingsFragment extends MastodonToolbarFragment{ checkForUpdateItem = new TextItem(R.string.check_for_update, GithubSelfUpdater.getInstance()::checkForUpdates); items.add(checkForUpdateItem); } - items.add(new TextItem(R.string.settings_contribute_fork, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/LucasGGamerM/moshidon"))); + items.add(new TextItem(R.string.settings_contribute_fork, ()->UiUtils.launchWebBrowser(getActivity(), "https://github.com/sk22/megalodon"))); items.add(new TextItem(R.string.settings_clear_cache, this::clearImageCache)); items.add(new TextItem(R.string.log_out, this::confirmLogOut)); @@ -204,6 +209,11 @@ public class SettingsFragment extends MastodonToolbarFragment{ if(needUpdateNotificationSettings && PushSubscriptionManager.arePushNotificationsAvailable()){ AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().updatePushSettings(pushSubscription); } + if(needAppRestart){ + Intent intent = Intent.makeRestartActivityTask(MastodonApp.context.getPackageManager().getLaunchIntentForPackage(MastodonApp.context.getPackageName()).getComponent()); + MastodonApp.context.startActivity(intent); + Runtime.getRuntime().exit(0); + } } @Override @@ -291,6 +301,7 @@ public class SettingsFragment extends MastodonToolbarFragment{ case FOLLOW -> subscription.alerts.follow=enabled; case REBLOG -> subscription.alerts.reblog=enabled; case MENTION -> subscription.alerts.mention=subscription.alerts.poll=enabled; + case STATUS -> subscription.alerts.status=enabled; } needUpdateNotificationSettings=true; } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/SplashFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/SplashFragment.java index 9ad3532cc..8f607e629 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/SplashFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/SplashFragment.java @@ -1,6 +1,5 @@ package org.joinmastodon.android.fragments; -import android.content.res.Configuration; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -10,7 +9,8 @@ import android.view.WindowInsets; import org.joinmastodon.android.MastodonApp; import org.joinmastodon.android.R; -import org.joinmastodon.android.fragments.onboarding.InstanceCatalogFragment; +import org.joinmastodon.android.fragments.onboarding.InstanceCatalogSignupFragment; +import org.joinmastodon.android.fragments.onboarding.InstanceChooserLoginFragment; import org.joinmastodon.android.ui.InterpolatingMotionEffect; import org.joinmastodon.android.ui.views.SizeListenerFrameLayout; @@ -66,8 +66,9 @@ public class SplashFragment extends AppKitFragment{ private void onButtonClick(View v){ Bundle extras=new Bundle(); - extras.putBoolean("signup", v.getId()==R.id.btn_get_started); - Nav.go(getActivity(), InstanceCatalogFragment.class, extras); + boolean isSignup=v.getId()==R.id.btn_get_started; + extras.putBoolean("signup", isSignup); + Nav.go(getActivity(), isSignup ? InstanceCatalogSignupFragment.class : InstanceChooserLoginFragment.class, extras); } private void updateArtSize(int w, int h){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/BaseAccountListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/BaseAccountListFragment.java index cdc3ab78e..2ece3877a 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/BaseAccountListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/BaseAccountListFragment.java @@ -286,13 +286,16 @@ public abstract class BaseAccountListFragment extends BaseRecyclerFragment 0) position++; tab.setText(switch(position){ case 0 -> R.string.local_timeline; - case 1 -> R.string.federated_timeline; + case 1 -> R.string.sk_federated_timeline; case 2 -> R.string.hashtags; case 3 -> R.string.posts; case 4 -> R.string.news; case 5 -> R.string.for_you; - case 6 -> R.string.list_timelines; + case 6 -> R.string.sk_list_timelines; default -> throw new IllegalStateException("Unexpected value: "+position); }); tab.view.textView.setAllCaps(true); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogFragment.java index d23b8339e..a188711b4 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/onboarding/InstanceCatalogFragment.java @@ -2,46 +2,30 @@ package org.joinmastodon.android.fragments.onboarding; import android.app.Activity; import android.app.ProgressDialog; -import android.content.Context; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.LocaleList; -import android.text.Editable; import android.text.TextUtils; -import android.text.TextWatcher; -import android.util.Log; import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; -import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.EditText; -import android.widget.ImageView; import android.widget.RadioButton; import android.widget.TextView; import org.joinmastodon.android.R; import org.joinmastodon.android.api.MastodonAPIController; -import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.api.MastodonErrorResponse; import org.joinmastodon.android.api.requests.instance.GetInstance; -import org.joinmastodon.android.api.requests.catalog.GetCatalogCategories; -import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances; -import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.model.Instance; -import org.joinmastodon.android.model.catalog.CatalogCategory; import org.joinmastodon.android.model.catalog.CatalogInstance; -import org.joinmastodon.android.ui.BetterItemAnimator; -import org.joinmastodon.android.ui.DividerItemDecoration; import org.joinmastodon.android.ui.M3AlertDialogBuilder; -import org.joinmastodon.android.ui.tabs.TabLayout; import org.joinmastodon.android.ui.utils.UiUtils; -import org.parceler.Parcels; import org.w3c.dom.Document; import org.w3c.dom.Element; -import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; @@ -59,49 +43,42 @@ import java.util.stream.Collectors; import javax.xml.parsers.DocumentBuilderFactory; import androidx.annotation.NonNull; -import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.RecyclerView; -import me.grishka.appkit.Nav; import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.fragments.BaseRecyclerFragment; import me.grishka.appkit.utils.BindableViewHolder; import me.grishka.appkit.utils.MergeRecyclerAdapter; -import me.grishka.appkit.utils.SingleViewRecyclerAdapter; import me.grishka.appkit.utils.V; import me.grishka.appkit.views.UsableRecyclerView; import okhttp3.Call; import okhttp3.Request; import okhttp3.Response; -public class InstanceCatalogFragment extends BaseRecyclerFragment{ - private InstancesAdapter adapter; - private MergeRecyclerAdapter mergeAdapter; - private View headerView; - private CatalogInstance chosenInstance; - private List filteredData=new ArrayList<>(); - private Button nextButton; - private MastodonAPIRequest getCategoriesRequest; - private EditText searchEdit; - private TabLayout categoriesList; - private Runnable searchDebouncer=this::onSearchChangedDebounced; - private String currentSearchQuery; - private String currentCategory="all"; - private List categories=new ArrayList<>(); - private String loadingInstanceDomain; - private GetInstance loadingInstanceRequest; - private Call loadingInstanceRedirectRequest; - private HashMap instancesCache=new HashMap<>(); - private ProgressDialog instanceProgressDialog; - private View buttonBar; - private HashMap redirects=new HashMap<>(), redirectsInverse=new HashMap<>(); - - private boolean isSignup; +abstract class InstanceCatalogFragment extends BaseRecyclerFragment{ + protected RecyclerView.Adapter adapter; + protected MergeRecyclerAdapter mergeAdapter; + protected CatalogInstance chosenInstance; + protected Button nextButton; + protected EditText searchEdit; + protected Runnable searchDebouncer=this::onSearchChangedDebounced; + protected String currentSearchQuery; + protected String loadingInstanceDomain; + protected HashMap instancesCache=new HashMap<>(); + protected View buttonBar; + protected List filteredData=new ArrayList<>(); + protected GetInstance loadingInstanceRequest; + protected Call loadingInstanceRedirectRequest; + protected ProgressDialog instanceProgressDialog; + protected HashMap redirects=new HashMap<>(); + protected HashMap redirectsInverse=new HashMap<>(); + protected boolean isSignup; + protected CatalogInstance fakeInstance=new CatalogInstance(); private static final double DUNBAR=Math.log(800); - public InstanceCatalogFragment(){ - super(R.layout.fragment_onboarding_common, 10); + public InstanceCatalogFragment(int layout, int perPage){ + super(layout, perPage); } @Override @@ -110,266 +87,9 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment(){ - @Override - public void onSuccess(List result){ - if(getActivity()==null) - return; - Map> byLang=result.stream().collect(Collectors.groupingBy(ci->ci.language)); - for(List group:byLang.values()){ - Collections.sort(group, (a, b)->{ - double aa=Math.abs(DUNBAR-Math.log(a.lastWeekUsers)); - double bb=Math.abs(DUNBAR-Math.log(b.lastWeekUsers)); - return Double.compare(aa, bb); - }); - } - // get the list of user-configured system languages - List userLangs; - if(Build.VERSION.SDK_INT<24){ - userLangs=Collections.singletonList(getResources().getConfiguration().locale.getLanguage()); - }else{ - LocaleList ll=getResources().getConfiguration().getLocales(); - userLangs=new ArrayList<>(ll.size()); - for(int i=0;i sortedList=new ArrayList<>(); - for(String lang:userLangs){ - List langInstances=byLang.remove(lang); - if(langInstances!=null){ - sortedList.addAll(langInstances); - } - } - // sort the remaining language groups by aggregate lastWeekUsers - class InstanceGroup{ - public int activeUsers; - public List instances; - } - byLang.values().stream().map(il->{ - InstanceGroup group=new InstanceGroup(); - group.instances=il; - for(CatalogInstance instance:il){ - group.activeUsers+=instance.lastWeekUsers; - } - return group; - }).sorted(Comparator.comparingInt((InstanceGroup g)->g.activeUsers).reversed()).forEachOrdered(ig->sortedList.addAll(ig.instances)); - onDataLoaded(sortedList, false); - updateFilteredList(); - } - - @Override - public void onError(ErrorResponse error){ - error.showToast(getActivity()); - onDataLoaded(Collections.emptyList(), false); - } - }) - .execNoAuth(""); - getCategoriesRequest=new GetCatalogCategories(null) - .setCallback(new Callback<>(){ - @Override - public void onSuccess(List result){ - getCategoriesRequest=null; - CatalogCategory all=new CatalogCategory(); - all.category="all"; - categories.add(all); - result.stream().sorted(Comparator.comparingInt((CatalogCategory cc)->cc.serversCount).reversed()).forEach(categories::add); - updateCategories(); - } - - @Override - public void onError(ErrorResponse error){ - getCategoriesRequest=null; - error.showToast(getActivity()); - CatalogCategory all=new CatalogCategory(); - all.category="all"; - categories.add(all); - updateCategories(); - } - }) - .execNoAuth(""); - } - - private void updateCategories(){ - categoriesList.removeAllTabs(); - for(CatalogCategory cat:categories){ - int titleRes=getTitleForCategory(cat.category); - TabLayout.Tab tab=categoriesList.newTab().setText(titleRes!=0 ? getString(titleRes) : cat.category).setCustomView(R.layout.item_instance_category); - ImageView emoji=tab.getCustomView().findViewById(R.id.emoji); - emoji.setImageResource(getEmojiForCategory(cat.category)); - categoriesList.addTab(tab); - } - } - - @Override - public void onDestroy(){ - super.onDestroy(); - if(getCategoriesRequest!=null) - getCategoriesRequest.cancel(); - } - - @Override - protected RecyclerView.Adapter getAdapter(){ - headerView=getActivity().getLayoutInflater().inflate(R.layout.header_onboarding_instance_catalog, list, false); - searchEdit=headerView.findViewById(R.id.search_edit); - categoriesList=headerView.findViewById(R.id.categories_list); - categoriesList.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener(){ - @Override - public void onTabSelected(TabLayout.Tab tab){ - CatalogCategory category=categories.get(tab.getPosition()); - currentCategory=category.category; - updateFilteredList(); - } - - @Override - public void onTabUnselected(TabLayout.Tab tab){ - - } - - @Override - public void onTabReselected(TabLayout.Tab tab){ - - } - }); - searchEdit.setOnEditorActionListener(this::onSearchEnterPressed); - searchEdit.addTextChangedListener(new TextWatcher(){ - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after){ - - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count){ - searchEdit.removeCallbacks(searchDebouncer); - searchEdit.postDelayed(searchDebouncer, 300); - } - - @Override - public void afterTextChanged(Editable s){ - } - }); - - mergeAdapter=new MergeRecyclerAdapter(); - mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(headerView)); - mergeAdapter.addAdapter(adapter=new InstancesAdapter()); - return mergeAdapter; - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState){ - super.onViewCreated(view, savedInstanceState); - nextButton=view.findViewById(R.id.btn_next); - nextButton.setOnClickListener(this::onNextClick); - nextButton.setEnabled(chosenInstance!=null); - view.findViewById(R.id.btn_back).setOnClickListener(v->Nav.finish(this)); - list.setItemAnimator(new BetterItemAnimator()); - list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 16, 16, DividerItemDecoration.NOT_FIRST)); - view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight)); - buttonBar=view.findViewById(R.id.button_bar); - setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight)); - } - - private void onNextClick(View v){ - String domain=chosenInstance.domain; - Instance instance=instancesCache.get(domain); - if(instance!=null){ - proceedWithAuthOrSignup(instance); - }else{ - showProgressDialog(); - if(!domain.equals(loadingInstanceDomain)){ - loadInstanceInfo(domain, false); - } - } - } - - private void proceedWithAuthOrSignup(Instance instance){ - getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0); - if(isSignup){ - if(!instance.registrations){ - new M3AlertDialogBuilder(getActivity()) - .setTitle(R.string.error) - .setMessage(R.string.instance_signup_closed) - .setPositiveButton(R.string.ok, null) - .show(); - return; - } - Bundle args=new Bundle(); - args.putParcelable("instance", Parcels.wrap(instance)); - Nav.go(getActivity(), InstanceRulesFragment.class, args); - }else{ - AccountSessionManager.getInstance().authenticate(getActivity(), instance); - } - } - -// private String getEmojiForCategory(String category){ -// return switch(category){ -// case "all" -> "💬"; -// case "academia" -> "📚"; -// case "activism" -> "✊"; -// case "food" -> "🍕"; -// case "furry" -> "🦁"; -// case "games" -> "🕹"; -// case "general" -> "🐘"; -// case "journalism" -> "📰"; -// case "lgbt" -> "🏳️‍🌈"; -// case "regional" -> "📍"; -// case "art" -> "🎨"; -// case "music" -> "🎼"; -// case "tech" -> "📱"; -// default -> "❓"; -// }; -// } - - private int getEmojiForCategory(String category){ - return switch(category){ - case "all" -> R.drawable.ic_category_all; - case "academia" -> R.drawable.ic_category_academia; - case "activism" -> R.drawable.ic_category_activism; - case "food" -> R.drawable.ic_category_food; - case "furry" -> R.drawable.ic_category_furry; - case "games" -> R.drawable.ic_category_games; - case "general" -> R.drawable.ic_category_general; - case "journalism" -> R.drawable.ic_category_journalism; - case "lgbt" -> R.drawable.ic_category_lgbt; - case "regional" -> R.drawable.ic_category_regional; - case "art" -> R.drawable.ic_category_art; - case "music" -> R.drawable.ic_category_music; - case "tech" -> R.drawable.ic_category_tech; - default -> R.drawable.ic_category_unknown; - }; - } - - private int getTitleForCategory(String category){ - return switch(category){ - case "all" -> R.string.category_all; - case "academia" -> R.string.category_academia; - case "activism" -> R.string.category_activism; - case "food" -> R.string.category_food; - case "furry" -> R.string.category_furry; - case "games" -> R.string.category_games; - case "general" -> R.string.category_general; - case "journalism" -> R.string.category_journalism; - case "lgbt" -> R.string.category_lgbt; - case "regional" -> R.string.category_regional; - case "art" -> R.string.category_art; - case "music" -> R.string.category_music; - case "tech" -> R.string.category_tech; - default -> 0; - }; - } - - private boolean onSearchEnterPressed(TextView v, int actionId, KeyEvent event){ + protected boolean onSearchEnterPressed(TextView v, int actionId, KeyEvent event){ if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN) return true; currentSearchQuery=searchEdit.getText().toString().toLowerCase(); @@ -385,60 +105,73 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment prevData=new ArrayList<>(filteredData); - filteredData.clear(); - for(CatalogInstance instance:data){ - if(currentCategory.equals("all") || instance.categories.contains(currentCategory)){ - if(TextUtils.isEmpty(currentSearchQuery) || instance.domain.contains(currentSearchQuery)){ - if(instance.domain.equals(currentSearchQuery) || !isSignup || !instance.approvalRequired) - filteredData.add(instance); - } + protected List sortInstances(List result){ + Map> byLang=result.stream().collect(Collectors.groupingBy(ci->ci.language)); + for(List group:byLang.values()){ + Collections.sort(group, (a, b)->{ + double aa=Math.abs(DUNBAR-Math.log(a.lastWeekUsers)); + double bb=Math.abs(DUNBAR-Math.log(b.lastWeekUsers)); + return Double.compare(aa, bb); + }); + } + // get the list of user-configured system languages + List userLangs; + if(Build.VERSION.SDK_INT<24){ + userLangs=Collections.singletonList(getResources().getConfiguration().locale.getLanguage()); + }else{ + LocaleList ll=getResources().getConfiguration().getLocales(); + userLangs=new ArrayList<>(ll.size()); + for(int i=0;i sortedList=new ArrayList<>(); + for(String lang:userLangs){ + List langInstances=byLang.remove(lang); + if(langInstances!=null){ + sortedList.addAll(langInstances); } - - @Override - public int getNewListSize(){ - return filteredData.size(); + } + // sort the remaining language groups by aggregate lastWeekUsers + class InstanceGroup{ + public int activeUsers; + public List instances; + } + byLang.values().stream().map(il->{ + InstanceGroup group=new InstanceGroup(); + group.instances=il; + for(CatalogInstance instance:il){ + group.activeUsers+=instance.lastWeekUsers; } - - @Override - public boolean areItemsTheSame(int oldItemPosition, int newItemPosition){ - return prevData.get(oldItemPosition)==filteredData.get(newItemPosition); - } - - @Override - public boolean areContentsTheSame(int oldItemPosition, int newItemPosition){ - return prevData.get(oldItemPosition)==filteredData.get(newItemPosition); - } - }).dispatchUpdatesTo(adapter); + return group; + }).sorted(Comparator.comparingInt((InstanceGroup g)->g.activeUsers).reversed()).forEachOrdered(ig->sortedList.addAll(ig.instances)); + return sortedList; } - private void showProgressDialog(){ + protected abstract void updateFilteredList(); + + protected void showProgressDialog(){ instanceProgressDialog=new ProgressDialog(getActivity()); instanceProgressDialog.setMessage(getString(R.string.loading_instance)); instanceProgressDialog.setOnCancelListener(dialog->cancelLoadingInstanceInfo()); instanceProgressDialog.show(); } - private String normalizeInstanceDomain(String _domain){ + protected String normalizeInstanceDomain(String _domain){ if(TextUtils.isEmpty(_domain)) return null; if(_domain.contains(":")){ try{ _domain=Uri.parse(_domain).getAuthority(); - }catch(Exception ignore){} + }catch(Exception ignore){ + } if(TextUtils.isEmpty(_domain)) return null; } @@ -453,12 +186,12 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment(){ - @Override - public void onSuccess(Instance result){ - loadingInstanceRequest=null; - loadingInstanceDomain=null; - result.uri=domain; // needed for instances that use domain redirection - instancesCache.put(domain, result); - if(instanceProgressDialog!=null){ - instanceProgressDialog.dismiss(); - instanceProgressDialog=null; - proceedWithAuthOrSignup(result); - } - if(Objects.equals(domain, currentSearchQuery) || Objects.equals(currentSearchQuery, redirects.get(domain)) || Objects.equals(currentSearchQuery, redirectsInverse.get(domain))){ - boolean found=false; - for(CatalogInstance ci:filteredData){ - if(ci.domain.equals(domain)){ - found=true; - break; - } - } - if(!found){ - CatalogInstance ci=result.toCatalogInstance(); - filteredData.add(0, ci); - adapter.notifyItemInserted(0); - } + @Override + public void onSuccess(Instance result){ + loadingInstanceRequest=null; + loadingInstanceDomain=null; + result.uri=domain; // needed for instances that use domain redirection + instancesCache.put(domain, result); + if(instanceProgressDialog!=null){ + instanceProgressDialog.dismiss(); + instanceProgressDialog=null; + proceedWithAuthOrSignup(result); + } + if(Objects.equals(domain, currentSearchQuery) || Objects.equals(currentSearchQuery, redirects.get(domain)) || Objects.equals(currentSearchQuery, redirectsInverse.get(domain))){ + boolean found=false; + for(CatalogInstance ci : filteredData){ + if(ci.domain.equals(domain) && ci!=fakeInstance){ + found=true; + break; } } + if(!found){ + CatalogInstance ci=result.toCatalogInstance(); + if(filteredData.size()==1 && filteredData.get(0)==fakeInstance){ + filteredData.set(0, ci); + adapter.notifyItemChanged(0); + }else{ + filteredData.add(0, ci); + adapter.notifyItemInserted(0); + } + } + } + } - @Override - public void onError(ErrorResponse error){ - loadingInstanceRequest=null; - if(!isFromRedirect && error instanceof MastodonErrorResponse me && me.httpStatus==404){ - fetchDomainFromHostMetaAndMaybeRetry(domain, error); - return; + @Override + public void onError(ErrorResponse error){ + loadingInstanceRequest=null; + if(!isFromRedirect && error instanceof MastodonErrorResponse me && me.httpStatus==404){ + fetchDomainFromHostMetaAndMaybeRetry(domain, error); + return; + } + loadingInstanceDomain=null; + showInstanceInfoLoadError(domain, error); + if(fakeInstance!=null){ + fakeInstance.description=getString(R.string.error); + if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){ + if(list.findViewHolderForAdapterPosition(1) instanceof BindableViewHolder ivh){ + ivh.rebind(); } - loadingInstanceDomain=null; - showInstanceInfoLoadError(domain, error); } - }).execNoAuth(domain); + } + } + }).execNoAuth(domain); } private void cancelLoadingInstanceInfo(){ @@ -584,7 +330,7 @@ public class InstanceCatalogFragment extends BaseRecyclerFragment{ - public InstancesAdapter(){ - super(imgLoader); - } - - @NonNull - @Override - public InstanceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ - return new InstanceViewHolder(); - } - - @Override - public void onBindViewHolder(InstanceViewHolder holder, int position){ - holder.bind(filteredData.get(position)); - super.onBindViewHolder(holder, position); - } - - @Override - public int getItemCount(){ - return filteredData.size(); - } - - @Override - public int getItemViewType(int position){ - return -1; - } + @Override + public void onViewCreated(View view, Bundle savedInstanceState){ + super.onViewCreated(view, savedInstanceState); + nextButton=view.findViewById(R.id.btn_next); + nextButton.setOnClickListener(this::onNextClick); + nextButton.setEnabled(chosenInstance!=null); + buttonBar=view.findViewById(R.id.button_bar); + setRefreshEnabled(false); } - private class InstanceViewHolder extends BindableViewHolder implements UsableRecyclerView.Clickable{ - private final TextView title, description, userCount, lang; - private final RadioButton radioButton; - - public InstanceViewHolder(){ - super(getActivity(), R.layout.item_instance_catalog, list); - title=findViewById(R.id.title); - description=findViewById(R.id.description); - userCount=findViewById(R.id.user_count); - lang=findViewById(R.id.lang); - radioButton=findViewById(R.id.radiobtn); - if(Build.VERSION.SDK_INT getCategoriesRequest; + private TabLayout categoriesList; + private String currentCategory="all"; + private List categories=new ArrayList<>(); + + + public InstanceCatalogSignupFragment(){ + super(R.layout.fragment_onboarding_common, 10); + } + + @Override + public void onAttach(Context context){ + super.onAttach(context); + setRefreshEnabled(false); + loadData(); + } + + @Override + protected void doLoadData(int offset, int count){ + currentRequest=new GetCatalogInstances(null, null) + .setCallback(new Callback<>(){ + @Override + public void onSuccess(List result){ + if(getActivity()==null) + return; + onDataLoaded(sortInstances(result), false); + updateFilteredList(); + } + + @Override + public void onError(ErrorResponse error){ + error.showToast(getActivity()); + onDataLoaded(Collections.emptyList(), false); + } + }) + .execNoAuth(""); + getCategoriesRequest=new GetCatalogCategories(null) + .setCallback(new Callback<>(){ + @Override + public void onSuccess(List result){ + getCategoriesRequest=null; + CatalogCategory all=new CatalogCategory(); + all.category="all"; + categories.add(all); + result.stream().sorted(Comparator.comparingInt((CatalogCategory cc)->cc.serversCount).reversed()).forEach(categories::add); + updateCategories(); + } + + @Override + public void onError(ErrorResponse error){ + getCategoriesRequest=null; + error.showToast(getActivity()); + CatalogCategory all=new CatalogCategory(); + all.category="all"; + categories.add(all); + updateCategories(); + } + }) + .execNoAuth(""); + } + + private void updateCategories(){ + categoriesList.removeAllTabs(); + for(CatalogCategory cat:categories){ + int titleRes=getTitleForCategory(cat.category); + TabLayout.Tab tab=categoriesList.newTab().setText(titleRes!=0 ? getString(titleRes) : cat.category).setCustomView(R.layout.item_instance_category); + ImageView emoji=tab.getCustomView().findViewById(R.id.emoji); + emoji.setImageResource(getEmojiForCategory(cat.category)); + categoriesList.addTab(tab); + } + } + + @Override + public void onDestroy(){ + super.onDestroy(); + if(getCategoriesRequest!=null) + getCategoriesRequest.cancel(); + } + + @Override + protected RecyclerView.Adapter getAdapter(){ + headerView=getActivity().getLayoutInflater().inflate(R.layout.header_onboarding_instance_catalog, list, false); + searchEdit=headerView.findViewById(R.id.search_edit); + categoriesList=headerView.findViewById(R.id.categories_list); + categoriesList.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener(){ + @Override + public void onTabSelected(TabLayout.Tab tab){ + CatalogCategory category=categories.get(tab.getPosition()); + currentCategory=category.category; + updateFilteredList(); + } + + @Override + public void onTabUnselected(TabLayout.Tab tab){ + + } + + @Override + public void onTabReselected(TabLayout.Tab tab){ + + } + }); + searchEdit.setOnEditorActionListener(this::onSearchEnterPressed); + searchEdit.addTextChangedListener(new TextWatcher(){ + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after){ + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count){ + searchEdit.removeCallbacks(searchDebouncer); + searchEdit.postDelayed(searchDebouncer, 300); + } + + @Override + public void afterTextChanged(Editable s){ + } + }); + + mergeAdapter=new MergeRecyclerAdapter(); + mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(headerView)); + mergeAdapter.addAdapter(adapter=new InstancesAdapter()); + return mergeAdapter; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState){ + super.onViewCreated(view, savedInstanceState); + view.findViewById(R.id.btn_back).setOnClickListener(v->Nav.finish(this)); + list.setItemAnimator(new BetterItemAnimator()); + list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 16, 16, DividerItemDecoration.NOT_FIRST)); + view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight)); + setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight)); + } + + @Override + protected void proceedWithAuthOrSignup(Instance instance){ + getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0); + if(isSignup){ + if(!instance.registrations){ + new M3AlertDialogBuilder(getActivity()) + .setTitle(R.string.error) + .setMessage(R.string.instance_signup_closed) + .setPositiveButton(R.string.ok, null) + .show(); + return; + } + Bundle args=new Bundle(); + args.putParcelable("instance", Parcels.wrap(instance)); + Nav.go(getActivity(), InstanceRulesFragment.class, args); + }else{ + } + } + +// private String getEmojiForCategory(String category){ +// return switch(category){ +// case "all" -> "💬"; +// case "academia" -> "📚"; +// case "activism" -> "✊"; +// case "food" -> "🍕"; +// case "furry" -> "🦁"; +// case "games" -> "🕹"; +// case "general" -> "🐘"; +// case "journalism" -> "📰"; +// case "lgbt" -> "🏳️‍🌈"; +// case "regional" -> "📍"; +// case "art" -> "🎨"; +// case "music" -> "🎼"; +// case "tech" -> "📱"; +// default -> "❓"; +// }; +// } + + private int getEmojiForCategory(String category){ + return switch(category){ + case "all" -> R.drawable.ic_category_all; + case "academia" -> R.drawable.ic_category_academia; + case "activism" -> R.drawable.ic_category_activism; + case "food" -> R.drawable.ic_category_food; + case "furry" -> R.drawable.ic_category_furry; + case "games" -> R.drawable.ic_category_games; + case "general" -> R.drawable.ic_category_general; + case "journalism" -> R.drawable.ic_category_journalism; + case "lgbt" -> R.drawable.ic_category_lgbt; + case "regional" -> R.drawable.ic_category_regional; + case "art" -> R.drawable.ic_category_art; + case "music" -> R.drawable.ic_category_music; + case "tech" -> R.drawable.ic_category_tech; + default -> R.drawable.ic_category_unknown; + }; + } + + private int getTitleForCategory(String category){ + return switch(category){ + case "all" -> R.string.category_all; + case "academia" -> R.string.category_academia; + case "activism" -> R.string.category_activism; + case "food" -> R.string.category_food; + case "furry" -> R.string.category_furry; + case "games" -> R.string.category_games; + case "general" -> R.string.category_general; + case "journalism" -> R.string.category_journalism; + case "lgbt" -> R.string.category_lgbt; + case "regional" -> R.string.category_regional; + case "art" -> R.string.category_art; + case "music" -> R.string.category_music; + case "tech" -> R.string.category_tech; + default -> 0; + }; + } + + @Override + protected void updateFilteredList(){ + ArrayList prevData=new ArrayList<>(filteredData); + filteredData.clear(); + for(CatalogInstance instance:data){ + if(currentCategory.equals("all") || instance.categories.contains(currentCategory)){ + if(TextUtils.isEmpty(currentSearchQuery) || instance.domain.contains(currentSearchQuery)){ + if(instance.domain.equals(currentSearchQuery) || !isSignup || !instance.approvalRequired) + filteredData.add(instance); + } + } + } + DiffUtil.calculateDiff(new DiffUtil.Callback(){ + @Override + public int getOldListSize(){ + return prevData.size(); + } + + @Override + public int getNewListSize(){ + return filteredData.size(); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition){ + return prevData.get(oldItemPosition)==filteredData.get(newItemPosition); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition){ + return prevData.get(oldItemPosition)==filteredData.get(newItemPosition); + } + }).dispatchUpdatesTo(adapter); + } + + + private class InstancesAdapter extends UsableRecyclerView.Adapter{ + public InstancesAdapter(){ + super(imgLoader); + } + + @NonNull + @Override + public InstanceCatalogSignupFragment.InstanceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ + return new InstanceCatalogSignupFragment.InstanceViewHolder(); + } + + @Override + public void onBindViewHolder(InstanceCatalogSignupFragment.InstanceViewHolder holder, int position){ + holder.bind(filteredData.get(position)); + super.onBindViewHolder(holder, position); + } + + @Override + public int getItemCount(){ + return filteredData.size(); + } + + @Override + public int getItemViewType(int position){ + return -1; + } + } + + private class InstanceViewHolder extends BindableViewHolder implements UsableRecyclerView.Clickable{ + private final TextView title, description, userCount, lang; + private final RadioButton radioButton; + + public InstanceViewHolder(){ + super(getActivity(), R.layout.item_instance_catalog, list); + title=findViewById(R.id.title); + description=findViewById(R.id.description); + userCount=findViewById(R.id.user_count); + lang=findViewById(R.id.lang); + radioButton=findViewById(R.id.radiobtn); + if(Build.VERSION.SDK_INT prevData=new ArrayList<>(filteredData); + filteredData.clear(); + if(currentSearchQuery.length()>0){ + boolean foundExactMatch=false; + for(CatalogInstance inst:data){ + if(inst.normalizedDomain.contains(currentSearchQuery)){ + filteredData.add(inst); + if(inst.normalizedDomain.equals(currentSearchQuery)) + foundExactMatch=true; + } + } + if(!foundExactMatch) + filteredData.add(0, fakeInstance); + } + UiUtils.updateList(prevData, filteredData, list, adapter, Objects::equals); + for(int i=0;i(){ + @Override + public void onSuccess(List result){ + data.clear(); + data.addAll(sortInstances(result)); + } + + @Override + public void onError(ErrorResponse error){ + + } + }) + .execNoAuth(""); + } + + @Override + protected void onUpdateToolbar(){ + super.onUpdateToolbar(); + Toolbar toolbar=getToolbar(); + toolbar.setElevation(0); + toolbar.setBackground(null); + } + + @Override + protected RecyclerView.Adapter getAdapter(){ + headerView=getActivity().getLayoutInflater().inflate(R.layout.header_onboarding_login, list, false); + clearBtn=headerView.findViewById(R.id.search_clear); + searchEdit=headerView.findViewById(R.id.search_edit); + searchEdit.setOnEditorActionListener(this::onSearchEnterPressed); + searchEdit.addTextChangedListener(new TextWatcher(){ + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after){ + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count){ + searchEdit.removeCallbacks(searchDebouncer); + searchEdit.postDelayed(searchDebouncer, 300); + + if(s.length()>0){ + fakeInstance.domain=fakeInstance.normalizedDomain=s.toString(); + fakeInstance.description=getString(R.string.loading_instance); + if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){ + if(list.findViewHolderForAdapterPosition(1) instanceof InstanceViewHolder ivh){ + ivh.rebind(); + } + } + if(filteredData.isEmpty()){ + filteredData.add(fakeInstance); + adapter.notifyItemInserted(0); + } + clearBtn.setVisibility(View.VISIBLE); + }else{ + clearBtn.setVisibility(View.GONE); + } + } + + @Override + public void afterTextChanged(Editable s){ + } + }); + clearBtn.setOnClickListener(v->searchEdit.setText("")); + + mergeAdapter=new MergeRecyclerAdapter(); + mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(headerView)); + mergeAdapter.addAdapter(adapter=new InstancesAdapter()); + return mergeAdapter; + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState){ + super.onViewCreated(view, savedInstanceState); + setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background)); + + list.addItemDecoration(new RecyclerView.ItemDecoration(){ + @Override + public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){ + if(parent.getChildViewHolder(view) instanceof InstanceViewHolder){ + outRect.left=outRect.right=V.dp(16); + } + } + }); + ((UsableRecyclerView)list).setDrawSelectorOnTop(true); + } + + private class InstancesAdapter extends UsableRecyclerView.Adapter{ + public InstancesAdapter(){ + super(imgLoader); + } + + @NonNull + @Override + public InstanceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ + return new InstanceViewHolder(); + } + + @Override + public void onBindViewHolder(InstanceViewHolder holder, int position){ + holder.bind(filteredData.get(position)); + super.onBindViewHolder(holder, position); + } + + @Override + public int getItemCount(){ + return filteredData.size(); + } + + @Override + public int getItemViewType(int position){ + return -1; + } + } + + private class InstanceViewHolder extends BindableViewHolder implements UsableRecyclerView.Clickable{ + private final TextView title, description; + private final RadioButton radioButton; + + public InstanceViewHolder(){ + super(getActivity(), R.layout.item_instance_login, list); + title=findViewById(R.id.title); + description=findViewById(R.id.description); + radioButton=findViewById(R.id.radiobtn); + radioButton.setMinWidth(0); + radioButton.setMinHeight(0); + + itemView.setOutlineProvider(new ViewOutlineProvider(){ + @Override + public void getOutline(View view, Outline outline){ + outline.setRoundRect(0, getAbsoluteAdapterPosition()==1 ? 0 : V.dp(-4), view.getWidth(), view.getHeight()+(getAbsoluteAdapterPosition()==filteredData.size() ? 0 : V.dp(4)), V.dp(4)); + } + }); + itemView.setClipToOutline(true); + } + + @Override + public void onBind(CatalogInstance item){ + title.setText(item.normalizedDomain); + description.setText(item.description); + radioButton.setChecked(chosenInstance==item); + } + + @Override + public void onClick(){ + if(chosenInstance==item) + return; + if(chosenInstance!=null){ + int idx=filteredData.indexOf(chosenInstance); + if(idx!=-1){ + for(int i=0;ionStartSwipeToDismissTransition(0)); imageDescriptionButton = toolbar.getMenu() - .add(R.string.image_description) + .add(R.string.sk_image_description) .setIcon(R.drawable.ic_fluent_image_alt_text_24_regular) .setVisible(attachments.get(pager.getCurrentItem()).description != null && !attachments.get(pager.getCurrentItem()).description.isEmpty()) diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java b/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java index fdd976983..82c3eab09 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java @@ -1,12 +1,29 @@ package org.joinmastodon.android.ui.text; +import android.graphics.Typeface; +import android.graphics.fonts.FontFamily; +import android.graphics.fonts.FontStyle; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; +import android.text.style.BackgroundColorSpan; +import android.text.style.BulletSpan; +import android.text.style.ForegroundColorSpan; +import android.text.style.LeadingMarginSpan; +import android.text.style.RelativeSizeSpan; +import android.text.style.StrikethroughSpan; +import android.text.style.StyleSpan; +import android.text.style.SubscriptSpan; +import android.text.style.SuperscriptSpan; +import android.text.style.TypefaceSpan; +import android.text.style.UnderlineSpan; +import android.util.TypedValue; import android.widget.TextView; import com.twitter.twittertext.Regex; +import org.joinmastodon.android.MastodonApp; +import org.joinmastodon.android.R; import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.model.Hashtag; import org.joinmastodon.android.model.Mention; @@ -15,11 +32,11 @@ import org.jsoup.Jsoup; import org.jsoup.nodes.Element; import org.jsoup.nodes.Node; import org.jsoup.nodes.TextNode; -import org.jsoup.safety.Cleaner; import org.jsoup.safety.Safelist; import org.jsoup.select.NodeVisitor; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.function.Function; @@ -29,6 +46,8 @@ import java.util.stream.Collectors; import androidx.annotation.NonNull; +import me.grishka.appkit.utils.V; + public class HtmlParser{ private static final String TAG="HtmlParser"; private static final String VALID_URL_PATTERN_STRING = @@ -67,11 +86,17 @@ public class HtmlParser{ public Object span; public int start; public Element element; + public boolean more; public SpanInfo(Object span, int start, Element element){ + this(span, start, element, false); + } + + public SpanInfo(Object span, int start, Element element, boolean more){ this.span=span; this.start=start; this.element=element; + this.more=more; } } @@ -119,24 +144,59 @@ public class HtmlParser{ openSpans.add(new SpanInfo(new InvisibleSpan(), ssb.length(), el)); } } + case "li" -> openSpans.add(new SpanInfo(new BulletSpan(V.dp(8)), ssb.length(), el)); + case "em", "i" -> openSpans.add(new SpanInfo(new StyleSpan(Typeface.ITALIC), ssb.length(), el)); + case "h1", "h2", "h3", "h4", "h5", "h6" -> { + // increase line height above heading (multiplying the margin) + if (node.previousSibling()!=null) ssb.setSpan(new RelativeSizeSpan(2), ssb.length() - 1, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + if (!node.nodeName().equals("h1")) { + openSpans.add(new SpanInfo(new StyleSpan(Typeface.BOLD), ssb.length(), el)); + } + openSpans.add(new SpanInfo(new RelativeSizeSpan(switch(node.nodeName()) { + case "h1" -> 1.5f; + case "h2" -> 1.25f; + case "h3" -> 1.125f; + default -> 1; + }), ssb.length(), el, !node.nodeName().equals("h1"))); + } + case "strong", "b" -> openSpans.add(new SpanInfo(new StyleSpan(Typeface.BOLD), ssb.length(), el)); + case "u" -> openSpans.add(new SpanInfo(new UnderlineSpan(), ssb.length(), el)); + case "s", "del" -> openSpans.add(new SpanInfo(new StrikethroughSpan(), ssb.length(), el)); + case "sub", "sup" -> { + openSpans.add(new SpanInfo(node.nodeName().equals("sub") ? new SubscriptSpan() : new SuperscriptSpan(), ssb.length(), el)); + openSpans.add(new SpanInfo(new RelativeSizeSpan(0.8f), ssb.length(), el, true)); + } + case "code", "pre" -> openSpans.add(new SpanInfo(new TypefaceSpan("monospace"), ssb.length(), el)); + case "blockquote" -> openSpans.add(new SpanInfo(new LeadingMarginSpan.Standard(V.dp(10)), ssb.length(), el)); } } } + final static List blockElements = Arrays.asList("p", "ul", "ol", "blockquote", "h1", "h2", "h3", "h4", "h5", "h6"); + @Override public void tail(@NonNull Node node, int depth){ if(node instanceof Element el){ + processOpenSpan(el); if("span".equals(el.nodeName()) && el.hasClass("ellipsis")){ ssb.append("…", new DeleteWhenCopiedSpan(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - }else if("p".equals(el.nodeName())){ - if(node.nextSibling()!=null) - ssb.append("\n\n"); - }else if(!openSpans.isEmpty()){ - SpanInfo si=openSpans.get(openSpans.size()-1); - if(si.element==el){ - ssb.setSpan(si.span, si.start, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - openSpans.remove(openSpans.size()-1); - } + }else if(blockElements.contains(el.nodeName()) && node.nextSibling()!=null){ + ssb.append("\n"); // line end + ssb.append("\n", new RelativeSizeSpan(0.75f), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // margin after block + } + } + } + + private void processOpenSpan(Element el) { + if(!openSpans.isEmpty()){ + SpanInfo si=openSpans.get(openSpans.size()-1); + if(si.element==el){ + ssb.setSpan(si.span, si.start, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + openSpans.remove(openSpans.size()-1); + if(si.more) processOpenSpan(el); + } + if("li".equals(el.nodeName()) && el.nextSibling()!=null) { + ssb.append('\n'); } } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/DiscoverInfoBannerHelper.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/DiscoverInfoBannerHelper.java index baf87a674..a0e24d766 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/DiscoverInfoBannerHelper.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/DiscoverInfoBannerHelper.java @@ -36,7 +36,7 @@ public class DiscoverInfoBannerHelper{ case TRENDING_HASHTAGS -> R.string.trending_hashtags_info_banner; case TRENDING_LINKS -> R.string.trending_links_info_banner; case LOCAL_TIMELINE -> R.string.local_timeline_info_banner; - case FEDERATED_TIMELINE -> R.string.federated_timeline_info_banner; + case FEDERATED_TIMELINE -> R.string.sk_federated_timeline_info_banner; }); } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java new file mode 100644 index 000000000..6e59ead59 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/views/FloatingHintEditTextLayout.java @@ -0,0 +1,124 @@ +package org.joinmastodon.android.ui.views; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.content.res.TypedArray; +import android.text.Editable; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.TextView; + +import org.joinmastodon.android.R; +import org.joinmastodon.android.ui.utils.SimpleTextWatcher; + +import me.grishka.appkit.utils.CubicBezierInterpolator; +import me.grishka.appkit.utils.V; + +public class FloatingHintEditTextLayout extends FrameLayout{ + private EditText edit; + private TextView label; + private int labelTextSize; + private int offsetY; + private boolean hintVisible; + private Animator currentAnim; + + public FloatingHintEditTextLayout(Context context){ + this(context, null); + } + + public FloatingHintEditTextLayout(Context context, AttributeSet attrs){ + this(context, attrs, 0); + } + + public FloatingHintEditTextLayout(Context context, AttributeSet attrs, int defStyle){ + super(context, attrs, defStyle); + if(isInEditMode()) + V.setApplicationContext(context); + TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.FloatingHintEditTextLayout); + labelTextSize=ta.getDimensionPixelSize(R.styleable.FloatingHintEditTextLayout_android_labelTextSize, V.dp(12)); + offsetY=ta.getDimensionPixelOffset(R.styleable.FloatingHintEditTextLayout_editTextOffsetY, 0); + ta.recycle(); + } + + @Override + protected void onFinishInflate(){ + super.onFinishInflate(); + if(getChildCount()>0 && getChildAt(0) instanceof EditText et){ + edit=et; + }else{ + throw new IllegalStateException("First child must be an EditText"); + } + + label=new TextView(getContext()); + label.setTextSize(TypedValue.COMPLEX_UNIT_PX, labelTextSize); + label.setTextColor(edit.getHintTextColors()); + label.setText(edit.getHint()); + label.setSingleLine(); + label.setPivotX(0f); + label.setPivotY(0f); + LayoutParams lp=new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.START | Gravity.TOP); + lp.setMarginStart(edit.getPaddingStart()); + addView(label, lp); + + hintVisible=edit.getText().length()==0; + if(hintVisible) + label.setAlpha(0f); + + edit.addTextChangedListener(new SimpleTextWatcher(this::onTextChanged)); + } + + private void onTextChanged(Editable text){ + boolean newHintVisible=text.length()==0; + if(newHintVisible==hintVisible) + return; + if(currentAnim!=null) + currentAnim.cancel(); + hintVisible=newHintVisible; + + label.setAlpha(1); + float scale=edit.getLineHeight()/(float)label.getLineHeight(); + float transY=edit.getHeight()/2f-edit.getLineHeight()/2f+(edit.getTop()-label.getTop())-(label.getHeight()/2f-label.getLineHeight()/2f); + + AnimatorSet anim=new AnimatorSet(); + if(hintVisible){ + anim.playTogether( + ObjectAnimator.ofFloat(edit, TRANSLATION_Y, 0), + ObjectAnimator.ofFloat(label, SCALE_X, scale), + ObjectAnimator.ofFloat(label, SCALE_Y, scale), + ObjectAnimator.ofFloat(label, TRANSLATION_Y, transY) + ); + edit.setHintTextColor(0); + }else{ + label.setScaleX(scale); + label.setScaleY(scale); + label.setTranslationY(transY); + anim.playTogether( + ObjectAnimator.ofFloat(edit, TRANSLATION_Y, offsetY), + ObjectAnimator.ofFloat(label, SCALE_X, 1f), + ObjectAnimator.ofFloat(label, SCALE_Y, 1f), + ObjectAnimator.ofFloat(label, TRANSLATION_Y, 0f) + ); + } + anim.setDuration(150); + anim.setInterpolator(CubicBezierInterpolator.DEFAULT); + anim.start(); + anim.addListener(new AnimatorListenerAdapter(){ + @Override + public void onAnimationEnd(Animator animation){ + currentAnim=null; + if(hintVisible){ + label.setAlpha(0); + edit.setHintTextColor(label.getTextColors()); + } + } + }); + currentAnim=anim; + } +} diff --git a/mastodon/src/main/res/color/button_text_m3_filled.xml b/mastodon/src/main/res/color/button_text_m3_filled.xml new file mode 100644 index 000000000..84416b4f9 --- /dev/null +++ b/mastodon/src/main/res/color/button_text_m3_filled.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/color/m3_pressed_overlay.xml b/mastodon/src/main/res/color/m3_pressed_overlay.xml new file mode 100644 index 000000000..824b4b289 --- /dev/null +++ b/mastodon/src/main/res/color/m3_pressed_overlay.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/color/m3_radiobutton_tint.xml b/mastodon/src/main/res/color/m3_radiobutton_tint.xml new file mode 100644 index 000000000..029457ae2 --- /dev/null +++ b/mastodon/src/main/res/color/m3_radiobutton_tint.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/bg_button_m3_filled.xml b/mastodon/src/main/res/drawable/bg_button_m3_filled.xml new file mode 100644 index 000000000..8ba6277c9 --- /dev/null +++ b/mastodon/src/main/res/drawable/bg_button_m3_filled.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/ic_fluent_color_24_regular.xml b/mastodon/src/main/res/drawable/ic_fluent_color_24_regular.xml new file mode 100644 index 000000000..29aafe1e9 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_color_24_regular.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_m3_cancel.xml b/mastodon/src/main/res/drawable/ic_m3_cancel.xml new file mode 100644 index 000000000..258e402fb --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_m3_cancel.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_m3_search.xml b/mastodon/src/main/res/drawable/ic_m3_search.xml new file mode 100644 index 000000000..1b2a144a7 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_m3_search.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/rect_4dp.xml b/mastodon/src/main/res/drawable/rect_4dp.xml new file mode 100644 index 000000000..c44581d40 --- /dev/null +++ b/mastodon/src/main/res/drawable/rect_4dp.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/display_item_poll_option.xml b/mastodon/src/main/res/layout/display_item_poll_option.xml index c690d4f68..7d9f53ec7 100644 --- a/mastodon/src/main/res/layout/display_item_poll_option.xml +++ b/mastodon/src/main/res/layout/display_item_poll_option.xml @@ -12,7 +12,8 @@ + tools:text="scream into void. like this: aaaaaaaaaaaaaaaaaaaa"/> diff --git a/mastodon/src/main/res/layout/fragment_compose.xml b/mastodon/src/main/res/layout/fragment_compose.xml index 061d8ef68..8a93b7798 100644 --- a/mastodon/src/main/res/layout/fragment_compose.xml +++ b/mastodon/src/main/res/layout/fragment_compose.xml @@ -187,7 +187,7 @@ android:layout_weight="1" android:textSize="16sp" android:singleLine="true" - android:text="@string/mark_media_as_sensitive" /> + android:text="@string/sk_mark_media_as_sensitive" /> diff --git a/mastodon/src/main/res/layout/fragment_login.xml b/mastodon/src/main/res/layout/fragment_login.xml new file mode 100644 index 000000000..0abb69439 --- /dev/null +++ b/mastodon/src/main/res/layout/fragment_login.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + +