Compare commits

...

128 Commits

Author SHA1 Message Date
Grishka
162bc86ebe Merge branch 'l10n_master' 2024-11-02 11:20:36 +03:00
Grishka
af8f042f10 Add changelog 2024-11-02 11:20:18 +03:00
Eugen Rochko
7aae8b03af New translations strings.xml (Vietnamese) 2024-11-01 15:38:16 +01:00
Grishka
b28c095226 Fix dynamic colors setting 2024-11-01 10:45:41 +03:00
Eugen Rochko
9432fc9b8c New translations strings.xml (Chinese Traditional) 2024-11-01 03:09:45 +01:00
Eugen Rochko
37df47c7cd New translations strings.xml (Russian) 2024-10-31 13:22:11 +01:00
Grishka
62602839db Merge branch 'l10n_master' 2024-10-31 15:13:13 +03:00
Eugen Rochko
52c56db1ce New translations strings.xml (Russian) 2024-10-31 10:26:24 +01:00
Eugen Rochko
cdc3b37ee4 New translations strings.xml (Swedish) 2024-10-31 09:15:57 +01:00
Grishka
46bd36b65d And more colors 2024-10-31 10:22:55 +03:00
Grishka
70eb5bf68c Fix more colors 2024-10-31 10:18:34 +03:00
Grishka
c2f6b16aff Support grouping follow notifications (AND-230) 2024-10-31 10:09:42 +03:00
Grishka
603c058ec9 Allow disabling dynamic colors on Android 12+ (AND-143) 2024-10-31 10:05:25 +03:00
Grishka
3582d7bdad Fix colors in high-contrast modes 2024-10-31 09:24:26 +03:00
Grishka
d988e1aecf Assorted minor fixes 2024-10-31 08:55:28 +03:00
Grishka
7bf322d48a Crash fix 2024-10-31 08:28:14 +03:00
Eugen Rochko
5c4a450ef0 New translations strings.xml (Belarusian) 2024-10-30 20:51:38 +01:00
Eugen Rochko
c370fab1b4 New translations strings.xml (Belarusian) 2024-10-30 19:55:52 +01:00
Eugen Rochko
c79cba96ec New translations strings.xml (Chinese Traditional) 2024-10-30 17:05:32 +01:00
Eugen Rochko
ca4aed3dc2 New translations full_description.txt (Scottish Gaelic) 2024-10-30 13:33:29 +01:00
Eugen Rochko
c20237d32c New translations strings.xml (Scottish Gaelic) 2024-10-30 13:33:28 +01:00
Grishka
6082a0bcd8 Provide URLs for the link button thing in system app switcher
closes #632
2024-10-30 14:27:44 +03:00
Grishka
3de494f9e9 Avatar cropping (AND-203)
closes #76
2024-10-30 13:39:06 +03:00
Grishka
1b6c299251 Fix custom emoji keyboard 2024-10-30 09:20:35 +03:00
Grishka
01ae5b915d oops I forgot the copy link thing 2024-10-30 05:55:04 +03:00
Eugen Rochko
d0ca465194 New translations strings.xml (Spanish) 2024-10-29 18:25:05 +01:00
Eugen Rochko
07564f2964 New translations strings.xml (Italian) 2024-10-29 11:13:11 +01:00
Grishka
eb45b59cac Fix gap loading for real this time 2024-10-29 05:48:48 +03:00
Eugen Rochko
6e4c4c86f6 New translations strings.xml (Chinese Traditional) 2024-10-28 19:07:33 +01:00
Eugen Rochko
b52dd603a1 New translations full_description.txt (Icelandic) 2024-10-28 14:51:47 +01:00
Eugen Rochko
573e13f39f New translations strings.xml (Icelandic) 2024-10-28 14:51:45 +01:00
Grishka
5848dc0e67 oops 2024-10-28 11:53:42 +03:00
Eugen Rochko
7ae5546113 New translations strings.xml (Interlingua) 2024-10-28 09:41:58 +01:00
Eugen Rochko
a126a078b4 New translations strings.xml (Kabyle) 2024-10-28 09:41:56 +01:00
Eugen Rochko
808dab6f50 New translations strings.xml (Occitan) 2024-10-28 09:41:54 +01:00
Eugen Rochko
70dc5aece0 New translations strings.xml (Scottish Gaelic) 2024-10-28 09:41:53 +01:00
Eugen Rochko
80f76d0f05 New translations strings.xml (Sinhala) 2024-10-28 09:41:51 +01:00
Eugen Rochko
d2a96af886 New translations strings.xml (Bosnian) 2024-10-28 09:41:50 +01:00
Eugen Rochko
6b6e720ca5 New translations strings.xml (Filipino) 2024-10-28 09:41:49 +01:00
Eugen Rochko
9eacb7b067 New translations strings.xml (Welsh) 2024-10-28 09:41:48 +01:00
Eugen Rochko
36cce87ffc New translations strings.xml (Burmese) 2024-10-28 09:41:46 +01:00
Eugen Rochko
b028c3ad38 New translations strings.xml (Hindi) 2024-10-28 09:41:45 +01:00
Eugen Rochko
eada060f57 New translations strings.xml (Croatian) 2024-10-28 09:41:44 +01:00
Eugen Rochko
53cfbcb5b0 New translations strings.xml (Thai) 2024-10-28 09:41:43 +01:00
Eugen Rochko
23c624f575 New translations strings.xml (Bengali) 2024-10-28 09:41:42 +01:00
Eugen Rochko
669f3a50c8 New translations strings.xml (Persian) 2024-10-28 09:41:41 +01:00
Eugen Rochko
fa3f4f6eda New translations strings.xml (Indonesian) 2024-10-28 09:41:40 +01:00
Eugen Rochko
d97ffc32aa New translations strings.xml (Portuguese, Brazilian) 2024-10-28 09:41:38 +01:00
Eugen Rochko
1aecf4021f New translations strings.xml (Galician) 2024-10-28 09:41:37 +01:00
Eugen Rochko
f3d76a26f7 New translations strings.xml (Vietnamese) 2024-10-28 09:41:35 +01:00
Eugen Rochko
e5dc62db6f New translations strings.xml (Chinese Traditional) 2024-10-28 09:41:34 +01:00
Eugen Rochko
6008368045 New translations strings.xml (Chinese Simplified) 2024-10-28 09:41:33 +01:00
Eugen Rochko
83af61a758 New translations strings.xml (Ukrainian) 2024-10-28 09:41:31 +01:00
Eugen Rochko
eefcc34277 New translations strings.xml (Turkish) 2024-10-28 09:41:30 +01:00
Eugen Rochko
4a65976eea New translations strings.xml (Swedish) 2024-10-28 09:41:29 +01:00
Eugen Rochko
cb8aea258a New translations strings.xml (Slovenian) 2024-10-28 09:41:28 +01:00
Eugen Rochko
e5297b023d New translations strings.xml (Slovak) 2024-10-28 09:41:26 +01:00
Eugen Rochko
21c6f41013 New translations strings.xml (Russian) 2024-10-28 09:41:25 +01:00
Eugen Rochko
19f8d908c7 New translations strings.xml (Portuguese) 2024-10-28 09:41:24 +01:00
Eugen Rochko
fae7f73f7a New translations strings.xml (Polish) 2024-10-28 09:41:22 +01:00
Eugen Rochko
41da8cc1d7 New translations strings.xml (Norwegian) 2024-10-28 09:41:21 +01:00
Eugen Rochko
39d5a32494 New translations strings.xml (Dutch) 2024-10-28 09:41:20 +01:00
Eugen Rochko
c885b5a85e New translations strings.xml (Lithuanian) 2024-10-28 09:41:19 +01:00
Eugen Rochko
76f2b63171 New translations strings.xml (Korean) 2024-10-28 09:41:17 +01:00
Eugen Rochko
58e35d8da3 New translations strings.xml (Georgian) 2024-10-28 09:41:16 +01:00
Eugen Rochko
a5e03357df New translations strings.xml (Japanese) 2024-10-28 09:41:15 +01:00
Eugen Rochko
5a7ab6be70 New translations strings.xml (Italian) 2024-10-28 09:41:14 +01:00
Eugen Rochko
e977b46392 New translations strings.xml (Armenian) 2024-10-28 09:41:13 +01:00
Eugen Rochko
66de2f4b87 New translations strings.xml (Hungarian) 2024-10-28 09:41:11 +01:00
Eugen Rochko
1a48277cf2 New translations strings.xml (Finnish) 2024-10-28 09:41:08 +01:00
Eugen Rochko
6a55bd2248 New translations strings.xml (Greek) 2024-10-28 09:41:07 +01:00
Eugen Rochko
3a82395428 New translations strings.xml (German) 2024-10-28 09:41:06 +01:00
Eugen Rochko
a98b93feec New translations strings.xml (Danish) 2024-10-28 09:41:04 +01:00
Eugen Rochko
0cff2658f3 New translations strings.xml (Czech) 2024-10-28 09:41:03 +01:00
Eugen Rochko
d3d95d49de New translations strings.xml (Catalan) 2024-10-28 09:41:02 +01:00
Eugen Rochko
71ca9b6f3d New translations strings.xml (Belarusian) 2024-10-28 09:41:01 +01:00
Eugen Rochko
bf9c3d4d8d New translations strings.xml (Arabic) 2024-10-28 09:40:59 +01:00
Eugen Rochko
4b304629b7 New translations strings.xml (Spanish) 2024-10-28 09:40:58 +01:00
Eugen Rochko
c4314f100e New translations strings.xml (French) 2024-10-28 09:40:57 +01:00
Eugen Rochko
14c250446c New translations strings.xml (Frisian) 2024-10-28 09:40:55 +01:00
Eugen Rochko
8bc1c8e79c New translations strings.xml (Icelandic) 2024-10-28 09:40:54 +01:00
Eugen Rochko
5e6781817d New translations strings.xml (Basque) 2024-10-28 09:40:53 +01:00
Grishka
5b5e4fbbd2 Remove unused resources 2024-10-28 11:30:48 +03:00
Grishka
3dcc6d0013 Media viewer redesign (AND-196) 2024-10-28 11:26:40 +03:00
Grishka
2ad50cd972 Allow returning to previous scroll position in home timeline (AND-189) 2024-10-27 05:38:15 +03:00
Grishka
ced5fe4ee0 Revert "fix"
This reverts commit db4afd4c8f.
2024-10-27 04:33:48 +03:00
Grishka
db4afd4c8f fix 2024-10-26 07:21:45 +03:00
Grishka
a0d3bd83f2 Fix possible post duplicates in home timeline 2024-10-26 07:11:31 +03:00
Grishka
2092a6b8fe Fix new posts button dismiss gesture 2024-10-26 06:47:43 +03:00
Grishka
ad04433944 Load timeline gaps depending on scroll direction (AND-190)
Closes #70, closes #154, closes #147, closes #281
2024-10-26 06:45:01 +03:00
Grishka
57da77b642 Disable overscrolling in account switcher sheet (AND-207) 2024-10-25 05:17:40 +03:00
Grishka
f80e3771d1 Make toolbar scroll away in home timeline (AND-208) 2024-10-25 05:10:07 +03:00
Grishka
6a18d8ef03 Replace characters Android doesn't like (AND-193), fix #890 2024-10-25 04:37:11 +03:00
Grishka
a312018441 Open old file picker by long-pressing "add media" button (AND-194) 2024-10-25 04:31:54 +03:00
Grishka
3026bd5c51 Support for Android 15's color contrast setting WIP 2024-10-25 02:40:33 +03:00
Eugen Rochko
5afde48052 New translations strings.xml (Icelandic) 2024-10-24 17:29:09 +02:00
Grishka
14209dc785 Assorted crash fixes 2024-10-24 02:31:05 +03:00
Grishka
f1b30f251c Improve rendering of block elements in rich text
fixes #907
2024-10-24 01:37:28 +03:00
Eugen Rochko
6a849d654f New translations strings.xml (Frisian) 2024-10-19 16:50:00 +02:00
Eugen Rochko
2f9c5fe210 New translations strings.xml (Basque) 2024-10-18 00:48:18 +02:00
Eugen Rochko
43f096420f New translations strings.xml (Icelandic) 2024-10-16 12:12:44 +02:00
Eugen Rochko
5b848ed1ca New translations strings.xml (Russian) 2024-10-13 10:05:10 +02:00
Eugen Rochko
9e1cf330d7 New translations strings.xml (Greek) 2024-10-12 09:39:28 +02:00
Grishka
1ad2d08e27 Merge branch 'l10n_master' 2024-10-10 04:09:47 +03:00
Grishka
42658add38 Fix opening links in non-browser apps 2024-10-10 04:09:20 +03:00
Grishka
b211789847 Fix colors for quotes and code blocks 2024-10-10 01:29:21 +03:00
Eugen Rochko
9c88183366 New translations strings.xml (Russian) 2024-10-09 20:16:06 +02:00
Eugen Rochko
c76dba3a8c New translations strings.xml (Basque) 2024-10-09 18:07:53 +02:00
Eugen Rochko
29bee87f2a New translations strings.xml (Basque) 2024-10-09 16:54:04 +02:00
Grishka
c139f85b99 Fix wrong unread notification count for some accounts on 4.3 2024-10-09 06:09:53 +03:00
Grishka
3247d4f2f5 Fix notifications loading on pre-4.3 servers
fixes #897
2024-10-09 05:28:01 +03:00
Grishka
77b2f98f17 Quotes in text formatting (AND-222) 2024-10-09 05:20:58 +03:00
Grishka
82c6c8076a Lists in text formatting (AND-221) 2024-10-09 03:00:37 +03:00
Grishka
4177faa553 Monospace text formatting (AND-223) 2024-10-09 01:21:50 +03:00
Eugen Rochko
92ec125661 New translations strings.xml (Indonesian) 2024-10-08 12:59:00 +02:00
Grishka
513a57663b Display bold, italic, and strikethrough formatting (AND-220, AND-224) 2024-10-07 18:48:06 +03:00
Eugen Rochko
20e7f716f1 New translations strings.xml (Chinese Simplified) 2024-10-07 08:00:50 +02:00
Grishka
71f92cb66c Bump version 2024-10-06 01:25:21 +03:00
Grishka
77b2abd0cb Merge branch 'l10n_master' 2024-10-06 01:24:29 +03:00
Grishka
15385dd924 Make fastlane create draft releases 2024-10-06 01:22:25 +03:00
Grishka
08847ec641 Always reset notifications marker on "mark as read"
fixes #897
2024-10-06 01:11:21 +03:00
Grishka
805fc5d8c7 Crash fix 2024-10-06 01:06:45 +03:00
Eugen Rochko
3d7a95d336 New translations strings.xml (German) 2024-10-05 09:01:18 +02:00
Eugen Rochko
c1869386ff New translations strings.xml (German) 2024-10-05 07:34:09 +02:00
Eugen Rochko
7a728c52cf New translations strings.xml (German) 2024-10-04 21:23:57 +02:00
Eugen Rochko
22f3aad538 New translations strings.xml (Frisian) 2024-10-04 18:40:52 +02:00
Eugen Rochko
42da6dcf48 New translations strings.xml (Dutch) 2024-10-04 18:40:50 +02:00
Eugen Rochko
c0f18b1f61 New translations strings.xml (Frisian) 2024-10-04 17:34:12 +02:00
168 changed files with 3190 additions and 1229 deletions

View File

@@ -28,7 +28,7 @@ platform :android do
build_type: "release",
)
upload_to_play_store(
changes_not_sent_for_review: true,
release_status: "draft",
skip_upload_images: true,
skip_upload_screenshots: true
)

View File

@@ -0,0 +1,7 @@
- New option to use our color theme instead of Material You
- Support for the color contrast setting in Android 15
- You can now crop your profile picture when editing your profile
- You can now long-press "Add media" in the composer to bring up the file picker
- New look for the media viewer
- New follower notifications will now appear grouped
- Directional loading for gaps in timelines: load newer posts when scrolling up, older when down

View File

@@ -1 +1 @@
116.txt
125.txt

View File

@@ -1,36 +1,36 @@
Is Mastodon an dòigh as fheàrr airson sùil a chumail air na tha a dol. Lean duine sam bith air a cho-shaoghal agus faic a h-uile càil a-rèir an ama. Chan eil sgeul air algairimean, sanasachd no clickbait.
Seo an aplacaid Android oifigeil airson Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
Seo an aplacaid Android oifigeil airson Mastodon. The e àlainn s cho luath ris a ghaoth, air a dhealbhadh ach am biodh e cumhachdach ach furasta cleachdadh. Seo na nì thu san aplacaid againn:
EXPLORE
RÙRAICH
Discover new writers, journalists, artists, photographers, scientists and more
See whats happening in the world
Lorg sgrìobhadairean, luchd-naidheachd, luchd-ealain, luchd togail dhealbhan, luchd-saidheans is eile
Faic na tha a dol air an t-saoghal
READ
LEUGH
Keep up with people you care about in a chronological feed with no interruptions
Follow hashtags to keep up with specific topics in real time
Gabh naidheachdan na feadhainn a tha cudromach dhut a-rèir an ama s gun bhuairidhean
Lean tagaichean hais airson ceum a chumail ri cuspairean àraid ann am fìor-àm
CREATE
CRUTHAICH
■ Post to your followers or the whole world, with polls, high quality images and videos
Participate in interesting conversations with other people
■ Postaich chun luchd-leantainn agad no chun t-saoghail air fad, le cunntasan-bheachd agus dealbhan s videothan le càileachd àrd
Gabh pàirt ann an còmhraidhean inntinneach le daoine eile
CURATE
CURAIDICH
■ Create lists of people to never miss a post
Filter words or phrases to control what you do and dont want to see
■ Cruthaich liostaichean de dhaoine ach nach caill thu post uapa-san
Criathraich faclan no abairtean a stiùireadh na chì s nach fhaic thu
AND MORE!
AGUS MÒRAN A BHARRACHD!
A beautiful theme that adapts to your personalized color scheme, light or dark
Share and scan QR codes to quickly exchange Mastodon profiles with others
Login and switch between multiple accounts
Get notified when a specific person posts with the bell button
No spoilers! You can put your posts behind content warnings
Ùrlar àlainn a fhreagras dhan sgeama dhathan phearsanaichte agad, soilleir no dorcha
Co-roinn is sganaich còdaichean QR airson pròifilean Mhastodon iomlaid le càch sa bhad
Clàraich a-steach s geàrr leum eadar iomadh cunntas
Faigh brath nuair a phostaich cuideigin sònraichte rud le putan a chluig
Gun spoilers! S urrainn dhut na postaichean agad a chur air cùlaibh rabhaidhean susbainte
A POWERFUL PUBLISHING PLATFORM
ÙRLAR FOILLSEACHAIDH CUMHACHDACH
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. If they follow you, theyll see it.

View File

@@ -34,13 +34,13 @@ OG FLEIRA!
Þú þarft ekki lengur að prófa þig áfram með og friðþægja eitthvert ógagnsætt algrími sem ákvarðar hvort vinir þínir fái að sjá það sem þú birtir. Ef viðkomandi fylgist með þér, mun það sjást.
Ef þú birtir það á opna vefnum, er hægt að skoða það á opna vefnum. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
Ef þú birtir það á opna vefnum, er hægt að skoða það á opna vefnum. Þú ert örugg(ur) við að deila tenglum á Mastodon, vitandi það að hver sem er mun geta lesið þá án þess að skrá sig inn.
Between threads, polls, high quality images, videos, audio, and content warnings, Mastodon offers plenty of ways to express yourself in a way that suits you.
Með samræðum, hágæða myndefni, myndskeiðum, hljóðskrám og viðvörunum vegna efnis, býður Mastodon upp á margar leiðir til að tjá þig á þann hátt sem þér hentar.
ÖFLUGT KERFI TIL LESTRAR
We dont need to show you ads, so we dont need to keep you in our app. Mastodon has the richest selection of 3rd party apps and integrations so you can choose the experience that fits you best.
Við þurfum ekkert að sýna þér auglýsingar og höfum því enga ástæðu til að halda þér inni í okkar eigin forritum. Mastodon býður upp á mikið úrval forrita frá utanaðkomandi aðilum og samþættingu við önnur kerfi, þannig að þú getir valið það sem þér líkar best.
Thanks to the chronological home feed, its easy to tell when youve caught up on all updates and can move on to something else.
@@ -58,4 +58,4 @@ Ekki ánægð/ur með valið þitt? Þú getur alltaf skipt yfir á annan Mastod
Mastodon er skráð sem samtök án hagnaðarmarkmiða í BNA og Þýskalandi. We are not motivated by extracting monetary value from the platform, but by whats best for the platform.
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.
EINS OG BIRST HEFUR Í: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com og víðar.

View File

@@ -13,8 +13,8 @@ android {
applicationId "org.joinmastodon.android"
minSdk 23
targetSdk 34
versionCode 120
versionName "2.7.1"
versionCode 125
versionName "2.8.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -90,7 +90,7 @@ dependencies {
implementation 'me.grishka.litex:viewpager:1.0.0'
implementation 'me.grishka.litex:viewpager2:1.0.0'
implementation 'me.grishka.litex:palette:1.0.0'
implementation 'me.grishka.appkit:appkit:1.4.1'
implementation 'me.grishka.appkit:appkit:1.4.4'
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'org.jsoup:jsoup:1.14.3'
implementation 'com.squareup:otto:1.3.8'

View File

@@ -23,7 +23,11 @@
</intent>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="http"/>
<data android:scheme="http" android:host="*"/>
</intent>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="https" android:host="*"/>
</intent>
</queries>

View File

@@ -11,6 +11,7 @@ public class GlobalUserPreferences{
public static boolean useCustomTabs;
public static boolean altTextReminders, confirmUnfollow, confirmBoost, confirmDeletePost;
public static ThemePreference theme=ThemePreference.AUTO;
public static boolean useDynamicColors;
private static SharedPreferences getPrefs(){
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
@@ -29,6 +30,7 @@ public class GlobalUserPreferences{
confirmBoost=prefs.getBoolean("confirmBoost", false);
confirmDeletePost=prefs.getBoolean("confirmDeletePost", true);
theme=ThemePreference.values()[prefs.getInt("theme", 0)];
useDynamicColors=prefs.getBoolean("useDynamicColors", true);
}
public static void save(){
@@ -40,6 +42,7 @@ public class GlobalUserPreferences{
.putBoolean("confirmUnfollow", confirmUnfollow)
.putBoolean("confirmBoost", confirmBoost)
.putBoolean("confirmDeletePost", confirmDeletePost)
.putBoolean("useDynamicColors", useDynamicColors)
.apply();
}

View File

@@ -3,6 +3,7 @@ package org.joinmastodon.android;
import android.Manifest;
import android.app.Application;
import android.app.Fragment;
import android.app.assist.AssistContent;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
@@ -16,6 +17,7 @@ import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.AssistContentProviderFragment;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
@@ -229,4 +231,11 @@ public class MainActivity extends FragmentStackActivity{
return null;
return getFragmentManager().findFragmentById(fragmentContainers.get(fragmentContainers.size()-1).getId());
}
@Override
public void onProvideAssistContent(AssistContent outContent){
if(getTopmostFragment() instanceof AssistContentProviderFragment provider){
provider.onProvideAssistContent(outContent);
}
}
}

View File

@@ -295,6 +295,15 @@ public class CacheController{
.collect(Collectors.toList());
PaginatedResponse<List<NotificationViewModel>> res=new PaginatedResponse<>(converted, result.isEmpty() ? null : result.get(result.size()-1).id);
callback.onSuccess(res);
if(!onlyMentions){
loadingNotifications=false;
synchronized(pendingNotificationsCallbacks){
for(Callback<PaginatedResponse<List<NotificationViewModel>>> cb:pendingNotificationsCallbacks){
cb.onSuccess(res);
}
pendingNotificationsCallbacks.clear();
}
}
databaseThread.postRunnable(()->putNotifications(converted.stream().map(nvm->nvm.notification).collect(Collectors.toList()), accounts, statuses, onlyMentions, maxID==null), 0);
}

View File

@@ -31,7 +31,7 @@ import okio.Source;
public class ResizedImageRequestBody extends CountingRequestBody{
private File tempFile;
private Uri uri;
private String contentType;
private MediaType contentType;
private int maxSize;
public ResizedImageRequestBody(Uri uri, int maxSize, ProgressListener progressListener) throws IOException{
@@ -42,15 +42,16 @@ public class ResizedImageRequestBody extends CountingRequestBody{
opts.inJustDecodeBounds=true;
if("file".equals(uri.getScheme())){
BitmapFactory.decodeFile(uri.getPath(), opts);
contentType=UiUtils.getFileMediaType(new File(uri.getPath())).type();
contentType=UiUtils.getFileMediaType(new File(uri.getPath()));
}else{
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
BitmapFactory.decodeStream(in, null, opts);
}
contentType=MastodonApp.context.getContentResolver().getType(uri);
String mime=MastodonApp.context.getContentResolver().getType(uri);
contentType=TextUtils.isEmpty(mime) ? null : MediaType.get(mime);
}
if(TextUtils.isEmpty(contentType))
contentType="image/jpeg";
if(contentType==null)
contentType=MediaType.get("image/jpeg");
if(needResize(opts.outWidth, opts.outHeight) || needCrop(opts.outWidth, opts.outHeight)){
Bitmap bitmap;
if(Build.VERSION.SDK_INT>=28){
@@ -136,7 +137,7 @@ public class ResizedImageRequestBody extends CountingRequestBody{
bitmap.compress(Bitmap.CompressFormat.PNG, 0, out);
}else{
bitmap.compress(Bitmap.CompressFormat.JPEG, 97, out);
contentType="image/jpeg";
contentType=MediaType.get("image/jpeg");
}
}
length=tempFile.length();
@@ -163,7 +164,7 @@ public class ResizedImageRequestBody extends CountingRequestBody{
@Override
public MediaType contentType(){
return MediaType.get(contentType);
return contentType;
}
@Override

View File

@@ -1,10 +1,27 @@
package org.joinmastodon.android.api.requests.notifications;
import org.joinmastodon.android.api.ApiUtils;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.NotificationType;
import java.util.EnumSet;
public class GetUnreadNotificationsCount extends MastodonAPIRequest<GetUnreadNotificationsCount.Response>{
public GetUnreadNotificationsCount(){
public GetUnreadNotificationsCount(EnumSet<NotificationType> includeTypes, EnumSet<NotificationType> groupedTypes){
super(HttpMethod.GET, "/notifications/unread_count", Response.class);
if(includeTypes!=null){
for(String type: ApiUtils.enumSetToStrings(includeTypes, NotificationType.class)){
addQueryParameter("types[]", type);
}
for(String type:ApiUtils.enumSetToStrings(EnumSet.complementOf(includeTypes), NotificationType.class)){
addQueryParameter("exclude_types[]", type);
}
}
if(groupedTypes!=null){
for(String type:ApiUtils.enumSetToStrings(groupedTypes, NotificationType.class)){
addQueryParameter("grouped_types[]", type);
}
}
}
@Override

View File

@@ -124,6 +124,7 @@ public class AccountSessionManager{
public void addAccount(Instance instance, Token token, Account self, Application app, AccountActivationInfo activationInfo){
instances.put(instance.getDomain(), instance);
runOnDbThread(db->insertInstanceIntoDatabase(db, instance.getDomain(), instance, List.of(), 0));
AccountSession session=new AccountSession(token, self, app, instance.getDomain(), activationInfo==null, activationInfo);
sessions.put(session.getID(), session);
lastActiveAccountID=session.getID();
@@ -349,6 +350,7 @@ public class AccountSessionManager{
@Override
public void onSuccess(Instance instance){
instances.put(domain, instance);
runOnDbThread(db->insertInstanceIntoDatabase(db, domain, instance, List.of(), 0));
updateInstanceEmojis(instance, domain);
}
@@ -581,6 +583,12 @@ public class AccountSessionManager{
runOnDbThread(db->db.delete("dismissed_donation_campaigns", null, null));
}
public void clearInstanceInfo(){
SQLiteDatabase db=getOrOpenDatabase();
db.delete("instances", null, null);
db.close();
}
private static void insertInstanceIntoDatabase(SQLiteDatabase db, String domain, Instance instance, List<Emoji> emojis, long lastUpdated){
ContentValues values=new ContentValues();
values.put("domain", domain);

View File

@@ -0,0 +1,7 @@
package org.joinmastodon.android.fragments;
import android.app.assist.AssistContent;
public interface AssistContentProviderFragment{
void onProvideAssistContent(AssistContent content);
}

View File

@@ -186,7 +186,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@Override
public void openPhotoViewer(String parentID, Status _status, int attachmentIndex, MediaGridStatusDisplayItem.Holder gridHolder){
final Status status=_status.getContentStatus();
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, status, accountID, new PhotoViewer.Listener(){
currentPhotoViewer=new PhotoViewer(getActivity(), this, status.mediaAttachments, attachmentIndex, status, accountID, new PhotoViewer.Listener(){
private MediaAttachmentViewController transitioningHolder;
@Override

View File

@@ -290,7 +290,13 @@ public class ComposeFragment extends MastodonToolbarFragment implements ComposeE
languageBtn=view.findViewById(R.id.btn_language);
replyText=view.findViewById(R.id.reply_text);
mediaBtn.setOnClickListener(v->openFilePicker());
mediaBtn.setOnClickListener(v->openFilePicker(false));
if(UiUtils.isPhotoPickerAvailable()){
mediaBtn.setOnLongClickListener(v->{
openFilePicker(true);
return true;
});
}
pollBtn.setOnClickListener(v->togglePoll());
emojiBtn.setOnClickListener(v->emojiKeyboard.toggleKeyboardPopup(mainEditText));
spoilerBtn.setOnClickListener(v->toggleSpoiler());
@@ -887,9 +893,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements ComposeE
*
* <p>For earlier versions use the built in docs ui via {@link Intent#ACTION_GET_CONTENT}
*/
private void openFilePicker(){
private void openFilePicker(boolean forceGetContent){
Intent intent;
boolean usePhotoPicker=UiUtils.isPhotoPickerAvailable();
boolean usePhotoPicker=!forceGetContent && UiUtils.isPhotoPickerAvailable();
if(usePhotoPicker){
intent=new Intent(MediaStore.ACTION_PICK_IMAGES);
if(mediaViewController.getMaxAttachments()-mediaViewController.getMediaAttachmentsCount()>1)

View File

@@ -166,7 +166,7 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{
fakeAttachment.meta.width=width;
fakeAttachment.meta.height=height;
photoViewer=new PhotoViewer(getActivity(), Collections.singletonList(fakeAttachment), 0, null, accountID, new PhotoViewer.Listener(){
photoViewer=new PhotoViewer(getActivity(), null, Collections.singletonList(fakeAttachment), 0, null, accountID, new PhotoViewer.Listener(){
@Override
public void setPhotoViewVisibility(int index, boolean visible){
image.setAlpha(visible ? 1f : 0f);

View File

@@ -84,7 +84,7 @@ public class CreateListAddMembersFragment extends BaseAccountListFragment implem
public void onSuccess(HeaderPaginationList<Account> result){
for(Account acc:result)
accountIDsInList.add(acc.id);
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()));
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID, getActivity())).collect(Collectors.toList()));
}
})
.exec(accountID);

View File

@@ -3,6 +3,7 @@ package org.joinmastodon.android.fragments;
import android.annotation.SuppressLint;
import android.app.Fragment;
import android.app.NotificationManager;
import android.app.assist.AssistContent;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -57,7 +58,7 @@ import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class HomeFragment extends AppKitFragment{
public class HomeFragment extends AppKitFragment implements AssistContentProviderFragment{
private FragmentRootLinearLayout content;
private HomeTimelineFragment homeTimelineFragment;
private NotificationsListFragment notificationsFragment;
@@ -296,7 +297,7 @@ public class HomeFragment extends AppKitFragment{
if(instance==null)
return;
if(instance.getApiVersion()>=2){
new GetUnreadNotificationsCount()
new GetUnreadNotificationsCount(EnumSet.allOf(NotificationType.class), NotificationType.getGroupableTypes())
.setCallback(new Callback<>(){
@Override
public void onSuccess(GetUnreadNotificationsCount.Response result){
@@ -379,4 +380,11 @@ public class HomeFragment extends AppKitFragment{
if(notificationsFragment.loaded)
notificationsFragment.rebuildAllDisplayItems();
}
@Override
public void onProvideAssistContent(AssistContent content){
if(fragmentForTab(currentTab) instanceof AssistContentProviderFragment provider){
provider.onProvideAssistContent(content);
}
}
}

View File

@@ -9,7 +9,9 @@ import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.os.VibrationEffect;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
@@ -31,10 +33,12 @@ import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.Toolbar;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
@@ -58,20 +62,25 @@ import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.sheets.DonationSheet;
import org.joinmastodon.android.ui.sheets.DonationSuccessfulSheet;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewcontrollers.HomeTimelineMenuController;
import org.joinmastodon.android.ui.viewcontrollers.ToolbarDropdownMenuController;
import org.joinmastodon.android.ui.views.FixedAspectRatioImageView;
import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
import org.joinmastodon.android.ui.views.NewPostsButtonContainer;
import org.joinmastodon.android.updater.GithubSelfUpdater;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
@@ -102,6 +111,11 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
private DiscoverInfoBannerHelper localTimelineBannerHelper;
private View donationBanner;
private boolean donationBannerDismissing;
private NestedRecyclerScrollView scrollWrapper;
private String scrollBackItemID;
private int scrollBackItemOffset, scrollBackItemIndex;
private long scrollBackTime;
private String maxID;
private String lastSavedMarkerID;
@@ -109,6 +123,7 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
private BottomSheet donationSheet;
public HomeTimelineFragment(){
setLayout(R.layout.fragment_loader_hiding_toolbar);
setListLayoutId(R.layout.fragment_timeline);
}
@@ -272,13 +287,53 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
newPostsBtnWrap.setOnHideButtonListener(this::hideNewPostsButton);
updateToolbarLogo();
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
private HashSet<GapStatusDisplayItem> gaps=new HashSet<>();
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
if(newPostsBtnShown && list.getChildAdapterPosition(list.getChildAt(0))<=getMainAdapterOffset()){
hideNewPostsButton();
}
for(StatusDisplayItem item:displayItems){
if(item instanceof GapStatusDisplayItem gap){
gaps.add(gap);
}
}
if(gaps.isEmpty())
return;
for(int i=0;i<list.getChildCount();i++){
View child=list.getChildAt(i);
if(list.getChildViewHolder(child) instanceof GapStatusDisplayItem.Holder holder){
GapStatusDisplayItem gap=holder.getItem();
if(!gap.visible){
gap.visible=true;
gap.enteredFromTop=child.getTop()<list.getHeight()/2;
gaps.remove(gap);
}
}
}
for(GapStatusDisplayItem gap:gaps){
gap.visible=false;
}
gaps.clear();
}
});
View bottomOverlays=view.findViewById(R.id.bottom_overlays);
NestedRecyclerScrollView scroller=view.findViewById(R.id.scroller);
scroller.setScrollableChildSupplier(()->list);
scroller.setTakePriorityOverChildViews(true);
scroller.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY)->{
bottomOverlays.setTranslationY(scrollY-getToolbar().getHeight());
});
scroller.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
scroller.getViewTreeObserver().removeOnPreDrawListener(this);
bottomOverlays.setTranslationY(scroller.getScrollY()-getToolbar().getHeight());
return true;
}
});
scrollWrapper=scroller;
if(GithubSelfUpdater.needSelfUpdating()){
updateUpdateState(GithubSelfUpdater.getInstance().getState());
@@ -297,6 +352,10 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
state=updater.getState();
if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING)
getToolbar().getMenu().findItem(R.id.settings).setIcon(R.drawable.ic_settings_updateready_24px);
if("debug".equals(BuildConfig.BUILD_TYPE)){
menu.add(0, 1, 0, "Make a gap");
}
}
@Override
@@ -309,6 +368,17 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
}else if(id==R.id.edit_list){
args.putParcelable("list", Parcels.wrap(currentList));
Nav.go(getActivity(), EditListFragment.class, args);
}else if(id==1){
if(data.size()<35){
Toast.makeText(getActivity(), "Too few posts. Load at least 35", Toast.LENGTH_SHORT).show();
return true;
}
Status gapStatus=data.get(1);
gapStatus.hasGapAfter=true;
onStatusUpdated(gapStatus);
for(Status s:new ArrayList<>(data.subList(2, 32))){
removeStatus(s);
}
}
return true;
}
@@ -386,6 +456,10 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
result.get(result.size()-1).hasGapAfter=true;
toAdd=result;
}
if(!(toAdd instanceof ArrayList<?>))
toAdd=new ArrayList<>(toAdd);
Set<String> existingPostIDs=data.stream().map(s->s.id).collect(Collectors.toSet());
toAdd.removeIf(s->existingPostIDs.contains(s.id));
if(needCache)
AccountSessionManager.get(accountID).filterStatuses(toAdd, FilterContext.HOME);
if(!toAdd.isEmpty()){
@@ -408,13 +482,23 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
public void onGapClick(GapStatusDisplayItem.Holder item){
if(dataLoading)
return;
item.getItem().loading=true;
GapStatusDisplayItem gap=item.getItem();
gap.loading=true;
V.setVisibilityAnimated(item.progress, View.VISIBLE);
V.setVisibilityAnimated(item.text, View.GONE);
GapStatusDisplayItem gap=item.getItem();
dataLoading=true;
boolean needCache=listMode==ListMode.FOLLOWING;
loadAdditionalPosts(item.getItemID(), null, 20, null, new Callback<>(){
boolean insertBelowGap=!gap.enteredFromTop;
String maxID, minID;
if(gap.enteredFromTop){
maxID=null;
int gapPos=displayItems.indexOf(gap);
minID=displayItems.get(gapPos+1).parentID;
}else{
maxID=item.getItemID();
minID=null;
}
loadAdditionalPosts(maxID, minID, 20, null, new Callback<>(){
@Override
public void onSuccess(List<Status> result){
@@ -432,9 +516,9 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
if(gapStatus!=null){
gapStatus.hasGapAfter=false;
if(needCache)
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(gapStatus), false);
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(List.of(gapStatus), false);
}
}else{
}else if(insertBelowGap){
Set<String> idsBelowGap=new HashSet<>();
boolean belowGap=false;
int gapPostIndex=0;
@@ -445,7 +529,7 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
belowGap=true;
s.hasGapAfter=false;
if(needCache)
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(Collections.singletonList(s), false);
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(List.of(s), false);
}else{
gapPostIndex++;
}
@@ -463,8 +547,8 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
}
if(needCache)
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1);
targetList.clear();
List<StatusDisplayItem> targetList=displayItems.subList(gapPos, gapPos+1); // Get a sub-list that contains the gap item
targetList.clear(); // remove the gap item
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
for(Status s:result){
if(idsBelowGap.contains(s.id))
@@ -481,6 +565,61 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
}
if(needCache)
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(insertedPosts, false);
}else{
Set<String> idsAboveGap=new HashSet<>();
int gapPostIndex=0;
Status gapPost=null;
for(Status s:data){
if(s.id.equals(gap.parentID)){
gapPost=s;
break;
}else{
idsAboveGap.add(s.id);
gapPostIndex++;
}
}
if(gapPost==null)
return;
boolean needAdjustScroll=false;
int scrollTop=0;
for(int i=0;i<list.getChildCount();i++){
View child=list.getChildAt(i);
if(list.getChildViewHolder(child) instanceof GapStatusDisplayItem.Holder gapHolder && gapHolder.getItem()==gap){
needAdjustScroll=true;
scrollTop=child.getBottom()+1;
break;
}
}
List<StatusDisplayItem> targetList=displayItems.subList(gapPos+1, gapPos+1);
List<Status> insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1);
for(int i=result.size()-1;i>=0;i--){
Status s=result.get(i);
if(idsAboveGap.contains(s.id))
break;
targetList.addAll(0, buildDisplayItems(s));
insertedPosts.add(0, s);
}
int addedItemCount=targetList.size();
boolean gapRemoved=false;
if(insertedPosts.size()<result.size()){ // There was an intersection, remove the gap
gapRemoved=true;
gapPost.hasGapAfter=false;
if(needCache)
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(List.of(gapPost), false);
displayItems.remove(gapPos);
adapter.notifyItemRemoved(getMainAdapterOffset()+gapPos);
}else{
gap.loading=false;
adapter.notifyItemChanged(getMainAdapterOffset()+gapPos);
}
if(!insertedPosts.isEmpty()){
if(needCache)
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(insertedPosts, false);
adapter.notifyItemRangeInserted(getMainAdapterOffset()+gapPos+(gapRemoved ? 0 : 1), addedItemCount);
if(needAdjustScroll){
((LinearLayoutManager)list.getLayoutManager()).scrollToPositionWithOffset(getMainAdapterOffset()+gapPos+(gapRemoved ? 0 : 1)+addedItemCount, scrollTop);
}
}
}
}
@@ -616,7 +755,7 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
private void onNewPostsBtnClick(View v){
if(newPostsBtnShown){
hideNewPostsButton();
scrollToTop();
smoothScrollRecyclerViewToTop(list);
}
}
@@ -723,6 +862,68 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
}
}
@Override
public void scrollToTop(){
if(list.getChildCount()==0)
return;
scrollWrapper.smoothScrollTo(0, 0);
View topChild=list.getLayoutManager().getChildAt(0);
if(list.getChildAdapterPosition(topChild)==0){
if(topChild.getTop()==list.getPaddingTop() && scrollBackItemID!=null && System.currentTimeMillis()-scrollBackTime<5*60_000){
int indexWithinPost=0;
for(int i=0;i<displayItems.size();i++){
StatusDisplayItem item=displayItems.get(i);
if(item.parentID.equals(scrollBackItemID)){
if(indexWithinPost==scrollBackItemIndex){
((LinearLayoutManager)list.getLayoutManager()).scrollToPositionWithOffset(i+getMainAdapterOffset(), scrollBackItemOffset);
list.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
list.getViewTreeObserver().removeOnPreDrawListener(this);
list.scrollBy(0, V.dp(-300));
list.smoothScrollBy(0, V.dp(300));
return true;
}
});
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.S)
UiUtils.playVibrationEffectIfSupported(getActivity(), VibrationEffect.Composition.PRIMITIVE_THUD);
return;
}
indexWithinPost++;
}
}
}else{
smoothScrollRecyclerViewToTop(list);
return;
}
}else if(list.getChildViewHolder(topChild) instanceof StatusDisplayItem.Holder<?> itemHolder){
int postIndex;
String id=itemHolder.getItemID();
for(postIndex=0;postIndex<data.size();postIndex++){
if(data.get(postIndex).id.equals(id))
break;
}
if(postIndex>1){
scrollBackItemID=id;
scrollBackItemIndex=0;
for(StatusDisplayItem item:displayItems){
if(item.parentID.equals(id)){
if(item==itemHolder.getItem())
break;
scrollBackItemIndex++;
}
}
scrollBackItemOffset=topChild.getTop();
scrollBackTime=System.currentTimeMillis();
}else{
scrollBackItemID=null;
}
}
smoothScrollRecyclerViewToTop(list);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.S)
UiUtils.playVibrationEffectIfSupported(getActivity(), VibrationEffect.Composition.PRIMITIVE_QUICK_RISE);
}
private String getCurrentListTitle(){
return switch(listMode){
case FOLLOWING -> getString(R.string.timeline_following);

View File

@@ -190,7 +190,7 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements
@Subscribe
public void onAccountAddedToList(AccountAddedToListEvent ev){
if(ev.accountID.equals(accountID) && ev.listID.equals(followList.id)){
data.add(new AccountViewModel(ev.account, accountID));
data.add(new AccountViewModel(ev.account, accountID, getActivity()));
list.getAdapter().notifyItemInserted(data.size()-1);
}
}
@@ -281,7 +281,7 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.confirm_remove_list_members)
.setTitle(selectedAccounts.size()>1 ? R.string.confirm_remove_list_members : R.string.confirm_remove_list_member)
.setPositiveButton(R.string.remove, (dlg, which)->removeAccounts(new HashSet<>(selectedAccounts), null))
.setNegativeButton(R.string.cancel, null)
.show();
@@ -337,7 +337,7 @@ public class ListMembersFragment extends PaginatedAccountListFragment implements
onDone.run();
for(Account acc:accounts){
accountIDsInList.add(acc.id);
data.add(new AccountViewModel(acc, accountID));
data.add(new AccountViewModel(acc, accountID, getActivity()));
}
list.getAdapter().notifyItemRangeInserted(data.size()-accounts.size(), accounts.size());
}

View File

@@ -80,7 +80,7 @@ public class NotificationRequestsFragment extends MastodonRecyclerFragment<Notif
accountViewModels.clear();
maxID=result.getNextPageMaxID();
for(NotificationRequest req:result){
accountViewModels.put(req.account.id, new AccountViewModel(req.account, accountID, false));
accountViewModels.put(req.account.id, new AccountViewModel(req.account, accountID, false, getActivity()));
}
onDataLoaded(result, !TextUtils.isEmpty(maxID));
endMark.setVisibility(TextUtils.isEmpty(maxID) ? View.VISIBLE : View.GONE);

View File

@@ -241,7 +241,7 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
public boolean onOptionsItemSelected(MenuItem item){
int id=item.getItemId();
if(id==R.id.mark_all_read){
markAsRead();
markAsRead(true);
resetUnreadBackground();
}else if(id==R.id.filters){
showFiltersAlert();
@@ -257,11 +257,11 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
return mergeAdapter;
}
private void markAsRead(){
private void markAsRead(boolean force){
if(data.isEmpty())
return;
String id=data.get(0).notification.pageMaxId;
if(ObjectIdComparator.INSTANCE.compare(id, realUnreadMarker)>0){
if(force || ObjectIdComparator.INSTANCE.compare(id, realUnreadMarker)>0){
new SaveMarkers(null, id).exec(accountID);
AccountSessionManager.get(accountID).setNotificationsMarker(id, true);
realUnreadMarker=id;
@@ -290,7 +290,7 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
return;
for(NotificationViewModel n:items){
if(ObjectIdComparator.INSTANCE.compare(n.notification.pageMinId, realUnreadMarker)<=0){
markAsRead();
markAsRead(false);
break;
}
}

View File

@@ -6,9 +6,11 @@ import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.app.Fragment;
import android.app.assist.AssistContent;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.graphics.Outline;
@@ -66,6 +68,7 @@ import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.SingleImagePhotoViewerListener;
import org.joinmastodon.android.ui.Snackbar;
import org.joinmastodon.android.ui.photoviewer.AvatarCropper;
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
import org.joinmastodon.android.ui.sheets.DecentralizationExplainerSheet;
import org.joinmastodon.android.ui.tabs.TabLayout;
@@ -106,7 +109,7 @@ import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
public class ProfileFragment extends LoaderFragment implements ScrollableToTop, AssistContentProviderFragment{
private static final int AVATAR_RESULT=722;
private static final int COVER_RESULT=343;
@@ -554,8 +557,8 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
}
setTitle(account.displayName);
setSubtitle(getResources().getQuantityString(R.plurals.x_posts, (int)(account.statusesCount%1000), account.statusesCount));
ViewImageLoader.load(avatar, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
ViewImageLoader.load(cover, null, new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
ViewImageLoader.loadWithoutAnimation(avatar, avatar.getDrawable(), new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(100), V.dp(100)));
ViewImageLoader.loadWithoutAnimation(cover, cover.getDrawable(), new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.header : account.headerStatic, 1000, 1000));
SpannableStringBuilder ssb=new SpannableStringBuilder(account.displayName);
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
HtmlParser.parseCustomEmoji(ssb, account.emojis);
@@ -580,7 +583,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
domain=AccountSessionManager.get(accountID).domain;
usernameDomain.setText(domain);
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account, getActivity());
if(TextUtils.isEmpty(parsedBio)){
bio.setVisibility(View.GONE);
}else{
@@ -615,7 +618,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
fields.add(joined);
for(AccountField field:account.fields){
field.parsedValue=ssb=HtmlParser.parse(field.value, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
field.parsedValue=ssb=HtmlParser.parse(field.value, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account, getActivity());
field.valueEmojis=ssb.getSpans(0, ssb.length(), CustomEmojiSpan.class);
ssb=new SpannableStringBuilder(field.name);
HtmlParser.parseCustomEmoji(ssb, account.emojis);
@@ -763,6 +766,9 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
})
.wrapProgress(getActivity(), R.string.loading, false)
.exec(accountID);
}else if(id==R.id.copy_link){
getActivity().getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, account.url));
UiUtils.maybeShowTextCopiedToast(getActivity());
}
return true;
}
@@ -1134,7 +1140,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
if(ava==null)
return;
int radius=V.dp(25);
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(account.avatar, ava), 0,
currentPhotoViewer=new PhotoViewer(getActivity(), null, createFakeAttachments(account.avatar, ava), 0,
null, accountID, new SingleImagePhotoViewerListener(avatar, avatarBorder, new int[]{radius, radius, radius, radius}, this, ()->currentPhotoViewer=null, ()->ava, null, null));
}
}
@@ -1148,7 +1154,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
Drawable drawable=cover.getDrawable();
if(drawable==null || drawable instanceof ColorDrawable || account.headerStatic.endsWith("/missing.png"))
return;
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(account.header, drawable), 0,
currentPhotoViewer=new PhotoViewer(getActivity(), null, createFakeAttachments(account.header, drawable), 0,
null, accountID, new SingleImagePhotoViewerListener(cover, cover, null, this, ()->currentPhotoViewer=null, ()->drawable, ()->avatarBorder.setTranslationZ(2), ()->avatarBorder.setTranslationZ(0)));
}
}
@@ -1171,9 +1177,16 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
public void onActivityResult(int requestCode, int resultCode, Intent data){
if(resultCode==Activity.RESULT_OK){
if(requestCode==AVATAR_RESULT){
editNewAvatar=data.getData();
ViewImageLoader.loadWithoutAnimation(avatar, null, new UrlImageLoaderRequest(editNewAvatar, V.dp(100), V.dp(100)));
editDirty=true;
if(!isTablet){
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
int radius=V.dp(25);
new AvatarCropper(getActivity(), data.getData(), new SingleImagePhotoViewerListener(avatar, avatarBorder, new int[]{radius, radius, radius, radius}, this, ()->{}, null, null, null), (thumbnail, uri)->{
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
avatar.setImageDrawable(thumbnail);
editNewAvatar=uri;
editDirty=true;
}, ()->getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)).show();
}else if(requestCode==COVER_RESULT){
editNewCover=data.getData();
ViewImageLoader.loadWithoutAnimation(cover, null, new UrlImageLoaderRequest(editNewCover, V.dp(1000), V.dp(1000)));
@@ -1206,6 +1219,13 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
return actionButton.getVisibility()==View.VISIBLE && actionButtonWrap.getTop()+actionButtonWrap.getHeight()>scrollView.getScrollY();
}
@Override
public void onProvideAssistContent(AssistContent content){
if(account!=null){
content.setWebUri(Uri.parse(account.url));
}
}
private class ProfilePagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
@NonNull
@Override

View File

@@ -243,7 +243,7 @@ public class SplashFragment extends AppKitFragment{
@Override
public boolean wantsLightNavigationBar(){
return true;
return false;
}
@Override

View File

@@ -1,8 +1,10 @@
package org.joinmastodon.android.fragments;
import android.app.assist.AssistContent;
import android.content.res.ColorStateList;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
@@ -46,7 +48,7 @@ import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
public class ThreadFragment extends StatusListFragment{
public class ThreadFragment extends StatusListFragment implements AssistContentProviderFragment{
private Status mainStatus;
private ImageView endMark;
private FrameLayout replyContainer;
@@ -236,6 +238,8 @@ public class ThreadFragment extends StatusListFragment{
protected void drawDivider(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder, RecyclerView parent, Canvas c, Paint paint){
if(holder instanceof StatusDisplayItem.Holder<?> statusHolder && siblingHolder instanceof StatusDisplayItem.Holder<?> siblingStatusHolder){
Status siblingStatus=getStatusByID(siblingStatusHolder.getItemID());
if(siblingStatus==null)
return;
if(statusHolder.getItemID().equals(siblingStatus.inReplyToId) && siblingStatus!=mainStatus && !statusHolder.getItemID().equals(mainStatus.id))
return;
}
@@ -258,6 +262,11 @@ public class ThreadFragment extends StatusListFragment{
return null;
}
@Override
public void onProvideAssistContent(AssistContent content){
content.setWebUri(Uri.parse(mainStatus.url));
}
private class ReplyLinesItemDecoration extends RecyclerView.ItemDecoration{
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
@@ -291,6 +300,7 @@ public class ThreadFragment extends StatusListFragment{
continue;
float lineX=V.dp(36);
paint.setAlpha(Math.round(255*child.getAlpha()));
c.save();
c.clipRect(child.getX(), child.getY(), child.getX()+child.getWidth(), child.getY()+child.getHeight());
if(holder instanceof HeaderStatusDisplayItem.Holder){

View File

@@ -61,7 +61,7 @@ public class AccountSearchFragment extends BaseAccountListFragment{
protected void onSuccess(List<Account> result){
setEmptyText(R.string.no_search_results);
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), false);
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID, getActivity())).collect(Collectors.toList()), false);
}
protected String getSearchViewPlaceholder(){

View File

@@ -44,7 +44,7 @@ public class AddNewListMembersFragment extends AccountSearchFragment{
@Override
public void onSuccess(HeaderPaginationList<Account> result){
setEmptyText("");
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), result.nextPageUri!=null);
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID, getActivity())).collect(Collectors.toList()), result.nextPageUri!=null);
maxID=result.getNextPageMaxID();
}
})

View File

@@ -24,7 +24,7 @@ public abstract class PaginatedAccountListFragment extends BaseAccountListFragme
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else
nextMaxID=null;
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), nextMaxID!=null);
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID, getActivity())).collect(Collectors.toList()), nextMaxID!=null);
}
})
.exec(accountID);

View File

@@ -31,7 +31,7 @@ public class DiscoverAccountsFragment extends BaseAccountListFragment implements
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<FollowSuggestion> result){
List<AccountViewModel> accounts=result.stream().map(fs->new AccountViewModel(fs.account, accountID)).collect(Collectors.toList());
List<AccountViewModel> accounts=result.stream().map(fs->new AccountViewModel(fs.account, accountID, getActivity())).collect(Collectors.toList());
onDataLoaded(accounts, false);
bannerHelper.onBannerBecameVisible();
}

View File

@@ -109,7 +109,7 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
return;
onDataLoaded(results.stream().map(sr->{
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, true);
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, true, getActivity());
if(sr.type==SearchResult.Type.HASHTAG){
vm.hashtagItem.setOnClick(i->openHashtag(sr));
}
@@ -126,7 +126,7 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
onDataLoaded(Stream.of(result.hashtags.stream().map(SearchResult::new), result.accounts.stream().map(SearchResult::new))
.flatMap(Function.identity())
.map(sr->{
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, false);
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, false, getActivity());
if(sr.type==SearchResult.Type.HASHTAG){
vm.hashtagItem.setOnClick(i->openHashtag(sr));
}

View File

@@ -70,7 +70,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<FollowSuggestion> result){
onDataLoaded(result.stream().map(fs->new AccountViewModel(fs.account, accountID).stripLinksFromBio()).collect(Collectors.toList()), false);
onDataLoaded(result.stream().map(fs->new AccountViewModel(fs.account, accountID, getActivity()).stripLinksFromBio()).collect(Collectors.toList()), false);
}
})
.exec(accountID);

View File

@@ -2,6 +2,7 @@ package org.joinmastodon.android.fragments.onboarding;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@@ -24,7 +25,9 @@ import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.SingleImagePhotoViewerListener;
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
import org.joinmastodon.android.ui.photoviewer.AvatarCropper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.ListItemViewHolder;
import org.joinmastodon.android.ui.views.ReorderableLinearLayout;
@@ -53,6 +56,7 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment{
private Uri avatarUri, coverUri;
private LinearLayout scrollContent;
private CheckableListItem<Void> discoverableItem;
private View avaBorder;
private static final int AVATAR_RESULT=348;
private static final int COVER_RESULT=183;
@@ -80,6 +84,7 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment{
bioEdit=view.findViewById(R.id.bio);
avaImage=view.findViewById(R.id.avatar);
coverImage=view.findViewById(R.id.header);
avaBorder=view.findViewById(R.id.avatar_border);
btn=view.findViewById(R.id.btn_next);
btn.setOnClickListener(v->onButtonClick());
@@ -152,20 +157,25 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment{
public void onActivityResult(int requestCode, int resultCode, Intent data){
if(resultCode!=Activity.RESULT_OK)
return;
ImageView img;
Uri uri=data.getData();
int size;
if(requestCode==AVATAR_RESULT){
img=avaImage;
avatarUri=uri;
size=V.dp(100);
if(!isTablet){
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
int radius=V.dp(25);
new AvatarCropper(getActivity(), data.getData(), new SingleImagePhotoViewerListener(avaImage, avaBorder, new int[]{radius, radius, radius, radius}, this, ()->{}, null, null, null), (thumbnail, newUri)->{
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
avaImage.setImageDrawable(thumbnail);
avaImage.setForeground(null);
avatarUri=newUri;
}, ()->getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED)).show();
}else{
img=coverImage;
coverUri=uri;
size=V.dp(1000);
ViewImageLoader.load(coverImage, null, new UrlImageLoaderRequest(uri, size, size));
coverImage.setForeground(null);
}
img.setForeground(null);
ViewImageLoader.load(img, null, new UrlImageLoaderRequest(uri, size, size));
}
private void showDiscoverabilityAlert(){

View File

@@ -2,10 +2,16 @@ package org.joinmastodon.android.fragments.settings;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.os.Bundle;
import android.util.Pair;
import android.view.ViewGroup;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.PushSubscriptionManager;
import org.joinmastodon.android.api.session.AccountActivationInfo;
import org.joinmastodon.android.api.session.AccountSession;
@@ -14,12 +20,22 @@ import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.palette.graphics.Palette;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.utils.V;
public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
private CheckableListItem<Void> donationsStagingItem;
@@ -37,7 +53,9 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
new ListItem<>("Reset search info banners", null, this::onResetDiscoverBannersClick),
new ListItem<>("Reset pre-reply sheets", null, this::onResetPreReplySheetsClick),
new ListItem<>("Clear dismissed donation campaigns", null, this::onClearDismissedCampaignsClick),
donationsStagingItem=new CheckableListItem<>("Use staging environment for donations", "Restart app to apply", CheckableListItem.Style.SWITCH, getPrefs().getBoolean("donationsStaging", false), this::toggleCheckableItem)
donationsStagingItem=new CheckableListItem<>("Use staging environment for donations", "Restart app to apply", CheckableListItem.Style.SWITCH, getPrefs().getBoolean("donationsStaging", false), this::toggleCheckableItem),
new ListItem<>("Delete cached instance info", null, this::onDeleteInstanceInfoClick),
new ListItem<>("View dynamic color values", null, this::onViewColorsClick)
));
if(!GithubSelfUpdater.needSelfUpdating()){
resetUpdateItem.isEnabled=selfUpdateItem.isEnabled=false;
@@ -95,6 +113,63 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
Toast.makeText(getActivity(), "Dismissed campaigns cleared. Restart app to see your current campaign, if any", Toast.LENGTH_LONG).show();
}
private void onDeleteInstanceInfoClick(ListItem<?> item){
AccountSessionManager.getInstance().clearInstanceInfo();
Toast.makeText(getActivity(), "Instances removed from database", Toast.LENGTH_LONG).show();
}
private void onViewColorsClick(ListItem<?> item){
ArrayList<Pair<Integer, String>> attrs=new ArrayList<>();
Field[] fields=R.attr.class.getFields();
try{
for(Field fld:fields){
if(fld.getName().startsWith("color") && fld.getType().equals(int.class)){
attrs.add(new Pair<>((Integer)fld.get(null), fld.getName()));
}
}
}catch(IllegalAccessException x){
Toast.makeText(getActivity(), x.toString(), Toast.LENGTH_SHORT).show();
return;
}
class ColorsAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
@NonNull
@Override
public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
TextView view=new TextView(getActivity());
int pad=V.dp(16);
view.setPadding(pad, pad, pad, pad);
view.setTextSize(14);
view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
return new SimpleViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position){
Pair<Integer, String> attr=attrs.get(position);
TextView view=(TextView) holder.itemView;
int color=UiUtils.getThemeColor(getActivity(), attr.first);
view.setBackgroundColor(color);
view.setText(String.format("%s\n#%06X", attr.second, (color & 0xFF000000) != 0xFF000000 ? color : (color & 0xFFFFFF)));
view.setTextColor(new Palette.Swatch(color | 0xFF000000, 1).getBodyTextColor());
}
@Override
public int getItemCount(){
return attrs.size();
}
}
RecyclerView rv=new RecyclerView(getActivity());
rv.setLayoutManager(new LinearLayoutManager(getActivity()));
rv.setAdapter(new ColorsAdapter());
new M3AlertDialogBuilder(getActivity())
.setTitle("Dynamic colors")
.setView(rv)
.setPositiveButton(R.string.ok, null)
.show();
}
private void restartUI(){
Bundle args=new Bundle();
args.putString("account", accountID);

View File

@@ -21,6 +21,7 @@ import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
@@ -29,7 +30,7 @@ import me.grishka.appkit.FragmentStackActivity;
public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
private ImageView themeTransitionWindowView;
private ListItem<Void> themeItem;
private CheckableListItem<Void> showCWsItem, hideSensitiveMediaItem, interactionCountsItem, emojiInNamesItem;
private CheckableListItem<Void> showCWsItem, hideSensitiveMediaItem, interactionCountsItem, emojiInNamesItem, dynamicColorsItem;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -37,13 +38,20 @@ public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
setTitle(R.string.settings_display);
AccountSession s=AccountSessionManager.get(accountID);
AccountLocalPreferences lp=s.getLocalPreferences();
onDataLoaded(List.of(
themeItem=new ListItem<>(R.string.settings_theme, getAppearanceValue(), R.drawable.ic_dark_mode_24px, this::onAppearanceClick),
showCWsItem=new CheckableListItem<>(R.string.settings_show_cws, 0, CheckableListItem.Style.SWITCH, lp.showCWs, R.drawable.ic_warning_24px, this::toggleCheckableItem),
hideSensitiveMediaItem=new CheckableListItem<>(R.string.settings_hide_sensitive_media, 0, CheckableListItem.Style.SWITCH, lp.hideSensitiveMedia, R.drawable.ic_no_adult_content_24px, this::toggleCheckableItem),
interactionCountsItem=new CheckableListItem<>(R.string.settings_show_interaction_counts, 0, CheckableListItem.Style.SWITCH, lp.showInteractionCounts, R.drawable.ic_social_leaderboard_24px, this::toggleCheckableItem),
emojiInNamesItem=new CheckableListItem<>(R.string.settings_show_emoji_in_names, 0, CheckableListItem.Style.SWITCH, lp.customEmojiInNames, R.drawable.ic_emoticon_24px, this::toggleCheckableItem)
));
List<ListItem<Void>> items=new ArrayList<>();
items.add(themeItem=new ListItem<>(R.string.settings_theme, getAppearanceValue(), R.drawable.ic_dark_mode_24px, this::onAppearanceClick));
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.S){
items.add(dynamicColorsItem=new CheckableListItem<>(R.string.settings_use_dynamic_colors, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.useDynamicColors, R.drawable.ic_palette_24px, item->{
toggleCheckableItem(item);
setUseDynamicColors(item.checked);
}));
dynamicColorsItem.checkedChangeListener=this::setUseDynamicColors;
}
items.add(showCWsItem=new CheckableListItem<>(R.string.settings_show_cws, 0, CheckableListItem.Style.SWITCH, lp.showCWs, R.drawable.ic_warning_24px, this::toggleCheckableItem));
items.add(hideSensitiveMediaItem=new CheckableListItem<>(R.string.settings_hide_sensitive_media, 0, CheckableListItem.Style.SWITCH, lp.hideSensitiveMedia, R.drawable.ic_no_adult_content_24px, this::toggleCheckableItem));
items.add(interactionCountsItem=new CheckableListItem<>(R.string.settings_show_interaction_counts, 0, CheckableListItem.Style.SWITCH, lp.showInteractionCounts, R.drawable.ic_social_leaderboard_24px, this::toggleCheckableItem));
items.add(emojiInNamesItem=new CheckableListItem<>(R.string.settings_show_emoji_in_names, 0, CheckableListItem.Style.SWITCH, lp.customEmojiInNames, R.drawable.ic_emoticon_24px, this::toggleCheckableItem));
onDataLoaded(items);
}
@Override
@@ -109,6 +117,13 @@ public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
.show();
}
private void setUseDynamicColors(boolean useDynamicColors){
dynamicColorsItem.checked=useDynamicColors;
GlobalUserPreferences.useDynamicColors=useDynamicColors;
GlobalUserPreferences.save();
restartActivityToApplyNewTheme();
}
private void maybeApplyNewThemeRightNow(GlobalUserPreferences.ThemePreference prev){
boolean isCurrentDark=prev==GlobalUserPreferences.ThemePreference.DARK ||
(prev==GlobalUserPreferences.ThemePreference.AUTO && Build.VERSION.SDK_INT>=30 && getResources().getConfiguration().isNightModeActive());

View File

@@ -128,7 +128,7 @@ public class SettingsServerAboutFragment extends LoaderFragment{
hlp.leftMargin=hlp.rightMargin=V.dp(16);
scrollingLayout.addView(heading, hlp);
AccountViewModel model=new AccountViewModel(instance.getContactAccount(), accountID);
AccountViewModel model=new AccountViewModel(instance.getContactAccount(), accountID, getActivity());
AccountViewHolder holder=new AccountViewHolder(this, scrollingLayout, null);
holder.setStyle(AccountViewHolder.AccessoryType.NONE, false);
holder.bind(model);

View File

@@ -27,10 +27,10 @@ public enum NotificationType{
MODERATION_WARNING;
public boolean canBeGrouped(){
return this==REBLOG || this==FAVORITE;
return this==REBLOG || this==FAVORITE || this==FOLLOW;
}
public static EnumSet<NotificationType> getGroupableTypes(){
return EnumSet.of(FAVORITE, REBLOG);
return EnumSet.of(FAVORITE, REBLOG, FOLLOW);
}
}

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.model.viewmodel;
import android.content.Context;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
@@ -24,11 +25,11 @@ public class AccountViewModel{
public final CharSequence parsedName, parsedBio;
public final String verifiedLink;
public AccountViewModel(Account account, String accountID){
this(account, accountID, true);
public AccountViewModel(Account account, String accountID, Context context){
this(account, accountID, true, context);
}
public AccountViewModel(Account account, String accountID, boolean needBio){
public AccountViewModel(Account account, String accountID, boolean needBio, Context context){
this.account=account;
avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(50), V.dp(50));
emojiHelper=new CustomEmojiHelper();
@@ -38,7 +39,7 @@ public class AccountViewModel{
parsedName=account.displayName;
SpannableStringBuilder ssb=new SpannableStringBuilder(parsedName);
if(needBio){
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account, context);
ssb.append(parsedBio);
}else{
parsedBio=null;

View File

@@ -11,10 +11,10 @@ public class SearchResultViewModel{
public AccountViewModel account;
public ListItem<Hashtag> hashtagItem;
public SearchResultViewModel(SearchResult result, String accountID, boolean isRecents){
public SearchResultViewModel(SearchResult result, String accountID, boolean isRecents, Context context){
this.result=result;
switch(result.type){
case ACCOUNT -> account=new AccountViewModel(result.account, accountID);
case ACCOUNT -> account=new AccountViewModel(result.account, accountID, context);
case HASHTAG -> {
hashtagItem=new ListItem<>((isRecents ? "#" : "")+result.hashtag.name, null, isRecents ? R.drawable.ic_history_24px : R.drawable.ic_tag_24px, null, result.hashtag);
hashtagItem.isEnabled=true;

View File

@@ -0,0 +1,15 @@
package org.joinmastodon.android.ui;
public enum ColorContrastMode{
DEFAULT,
MEDIUM,
HIGH;
public static ColorContrastMode fromContrastValue(float value){
if(value>0.75f)
return HIGH;
if(value>0.25f)
return MEDIUM;
return DEFAULT;
}
}

View File

@@ -216,7 +216,7 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{
}
}
private class EmojiViewHolder extends BindableViewHolder<Emoji> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
private class EmojiViewHolder extends BindableViewHolder<Emoji> implements ImageLoaderViewHolder{
public int positionWithinCategory;
public EmojiViewHolder(){
super(new ImageView(activity));
@@ -226,6 +226,7 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{
int pad=V.dp(12);
img.setPadding(pad, pad, pad, pad);
img.setBackgroundResource(R.drawable.bg_custom_emoji);
itemView.setOnClickListener(v->onClick());
}
@Override
@@ -247,8 +248,7 @@ public class CustomEmojiPopupKeyboard extends PopupKeyboard{
((ImageView)itemView).setImageDrawable(null);
}
@Override
public void onClick(){
private void onClick(){
listener.onEmojiSelected(item);
}
}

View File

@@ -15,7 +15,7 @@ public class AccountStatusDisplayItem extends StatusDisplayItem{
public AccountStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Account account){
super(parentID, parentFragment);
this.account=new AccountViewModel(account, parentFragment.getAccountID());
this.account=new AccountViewModel(account, parentFragment.getAccountID(), parentFragment.getActivity());
}
@Override

View File

@@ -13,6 +13,8 @@ import org.joinmastodon.android.ui.drawables.SawtoothTearDrawable;
// Mind the gap!
public class GapStatusDisplayItem extends StatusDisplayItem{
public boolean loading;
public boolean enteredFromTop; // While the user was scrolling, did the gap item pop out from behind the top edge of the list?
public boolean visible; // Is this item currently within the viewport of the RecyclerView (and has a bound view)?
public GapStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment){
super(parentID, parentFragment);

View File

@@ -39,7 +39,7 @@ public class InlineStatusStatusDisplayItem extends StatusDisplayItem{
if(AccountSessionManager.get(parentFragment.getAccountID()).getLocalPreferences().customEmojiInNames)
HtmlParser.parseCustomEmoji(parsedName, status.account.emojis);
parsedPostText=HtmlParser.parse(status.content, status.emojis, status.mentions, status.tags, parentFragment.getAccountID(), status.getContentStatus());
parsedPostText=HtmlParser.parse(status.content, status.emojis, status.mentions, status.tags, parentFragment.getAccountID(), status.getContentStatus(), parentFragment.getActivity());
for(Object span:parsedPostText.getSpans(0, parsedPostText.length(), Object.class)){
if(!(span instanceof CustomEmojiSpan))
parsedPostText.removeSpan(span);

View File

@@ -73,6 +73,7 @@ public class NotificationHeaderStatusDisplayItem extends StatusDisplayItem{
text=parentFragment.getResources().getQuantityString(switch(notification.notification.type){
case FAVORITE -> R.plurals.user_and_x_more_favorited;
case REBLOG -> R.plurals.user_and_x_more_boosted;
case FOLLOW -> R.plurals.user_and_x_more_followed;
default -> throw new IllegalStateException("Unexpected value: " + notification.notification.type);
}, notification.notification.notificationsCount-1, "{{name}}", notification.notification.notificationsCount-1);
}else if(notification.notification.type==NotificationType.POLL){

View File

@@ -141,7 +141,7 @@ public abstract class StatusDisplayItem{
}
if(!TextUtils.isEmpty(statusForContent.content)){
SpannableStringBuilder parsedText=HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID, statusForContent);
SpannableStringBuilder parsedText=HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID, statusForContent, fragment.getActivity());
if(filtered){
HtmlParser.applyFilterHighlights(fragment.getActivity(), parsedText, status.filtered);
}

View File

@@ -58,7 +58,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
public void setTranslatedText(String text){
Status statusForContent=status.getContentStatus();
translatedText=HtmlParser.parse(text, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, parentFragment.getAccountID(), statusForContent);
translatedText=HtmlParser.parse(text, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, parentFragment.getAccountID(), statusForContent, parentFragment.getActivity());
translationEmojiHelper.setText(translatedText);
}

View File

@@ -0,0 +1,68 @@
package org.joinmastodon.android.ui.drawables;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import me.grishka.appkit.utils.V;
public class VideoPlayerSeekBarThumbDrawable extends Drawable{
private Paint thumbPaint=new Paint(Paint.ANTI_ALIAS_FLAG), clearPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
private Path clearPath=new Path();
public VideoPlayerSeekBarThumbDrawable(){
thumbPaint.setColor(0xffffffff);
clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
clearPath.addRect(0, 0, V.dp(20), V.dp(32), Path.Direction.CW);
Path tmp=new Path();
float radius=V.dp(2);
tmp.addRoundRect(V.dp(-2), V.dp(12), V.dp(2), V.dp(20), radius, radius, Path.Direction.CW);
tmp.addRoundRect(V.dp(18), V.dp(12), V.dp(22), V.dp(20), radius, radius, Path.Direction.CW);
clearPath.op(tmp, Path.Op.DIFFERENCE);
}
@Override
public void draw(@NonNull Canvas canvas){
Rect bounds=getBounds();
int thumbWidth=V.dp(4), thumbHeight=V.dp(32);
int thumbX=bounds.centerX()-thumbWidth/2, thumbY=bounds.centerY()-thumbHeight/2;
canvas.save();
canvas.translate(thumbX-V.dp(8), thumbY);
canvas.drawPath(clearPath, clearPaint);
canvas.restore();
canvas.drawRoundRect(thumbX, thumbY, thumbX+thumbWidth, thumbY+thumbHeight, V.dp(2), V.dp(2), thumbPaint);
}
@Override
public void setAlpha(int alpha){
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter){
}
@Override
public int getOpacity(){
return PixelFormat.TRANSLUCENT;
}
@Override
public int getIntrinsicWidth(){
return V.dp(8);
}
@Override
public int getIntrinsicHeight(){
return V.dp(32);
}
}

View File

@@ -0,0 +1,360 @@
package org.joinmastodon.android.ui.photoviewer;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.view.ContextThemeWrapper;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.Toast;
import android.window.OnBackInvokedDispatcher;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.WindowRootFrameLayout;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class AvatarCropper implements ZoomPanView.Listener{
private Activity activity;
private Context context;
private WindowManager wm;
private WindowRootFrameLayout windowView;
private FragmentRootLinearLayout overlay;
private ZoomPanView zoomPanView;
private ImageButton closeButton;
private ImageView image;
private View confirmButton;
private Runnable onCancel;
private OnCropChosenListener cropChosenListener;
private Uri originalUri;
private PhotoViewer.Listener listener;
private Drawable background=new ColorDrawable(0xff000000);
public AvatarCropper(Activity activity, Uri imageUri, PhotoViewer.Listener photoViewerListener, OnCropChosenListener cropChosenListener, Runnable onCancel){
this.activity=activity;
this.context=new ContextThemeWrapper(activity, UiUtils.getThemeForUserPreference(activity, GlobalUserPreferences.ThemePreference.DARK));
originalUri=imageUri;
wm=context.getSystemService(WindowManager.class);
this.cropChosenListener=cropChosenListener;
this.onCancel=onCancel;
this.listener=photoViewerListener;
windowView=(WindowRootFrameLayout) LayoutInflater.from(this.context).inflate(R.layout.avatar_cropper, null);
overlay=windowView.findViewById(R.id.overlay);
closeButton=windowView.findViewById(R.id.btn_back);
zoomPanView=windowView.findViewById(R.id.zoom_pan_view);
image=windowView.findViewById(R.id.image);
confirmButton=windowView.findViewById(R.id.btn_confirm);
windowView.setBackground(background);
windowView.setDispatchApplyWindowInsetsListener((v, insets)->{
int bottomInset=0;
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
bottomInset=inset>0 ? Math.max(inset, V.dp(24)) : 0;
}
((FrameLayout.LayoutParams)confirmButton.getLayoutParams()).bottomMargin=bottomInset+V.dp(16+80);
return overlay.dispatchApplyWindowInsets(insets);
});
windowView.setDispatchKeyEventListener((v, keyCode, event)->{
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.TIRAMISU && event.getKeyCode()==KeyEvent.KEYCODE_BACK){
if(event.getAction()==KeyEvent.ACTION_DOWN){
dismiss(true, onCancel);
}
return true;
}
return false;
});
closeButton.setOnClickListener(v->dismiss(true, onCancel));
overlay.setStatusBarColor(0);
overlay.setNavigationBarColor(0);
overlay.setBackground(new OverlayDrawable());
zoomPanView.setListener(this);
zoomPanView.setFill(true);
zoomPanView.setSwipeToDismissEnabled(false);
ViewImageLoader.load(new ViewImageLoader.Target(){
@Override
public void setImageDrawable(Drawable d){
if(d!=null){
image.setImageDrawable(d);
image.setLayoutParams(new FrameLayout.LayoutParams(d.getIntrinsicWidth(), d.getIntrinsicHeight(), Gravity.CENTER));
zoomPanView.updateLayout();
}
}
@Override
public View getView(){
return image;
}
}, null, new UrlImageLoaderRequest(Bitmap.Config.ARGB_8888, 0, 0, List.of(), imageUri), false);
windowView.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom)->{
if(left==oldLeft && top==oldTop && right==oldRight && bottom==oldBottom)
return;
int width=right-left;
int height=bottom-top;
int size=V.dp(192);
int hpad=(width-size)/2;
int vpad=(height-size)/2;
zoomPanView.setPadding(hpad, vpad, hpad, vpad);
zoomPanView.updateLayout();
});
confirmButton.setOnClickListener(v->confirm());
}
public void show(){
WindowManager.LayoutParams wlp=new WindowManager.LayoutParams(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT);
wlp.type=WindowManager.LayoutParams.TYPE_APPLICATION;
wlp.flags=WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR
| WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
wlp.format=PixelFormat.TRANSLUCENT;
wlp.setTitle(context.getString(R.string.avatar_move_and_scale));
if(Build.VERSION.SDK_INT>=28)
wlp.layoutInDisplayCutoutMode=Build.VERSION.SDK_INT>=30 ? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS : WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
windowView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
wm.addView(windowView, wlp);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.TIRAMISU){
windowView.findOnBackInvokedDispatcher().registerOnBackInvokedCallback(OnBackInvokedDispatcher.PRIORITY_DEFAULT, ()->dismiss(true, onCancel));
}
}
public void dismiss(boolean animated, Runnable onDone){
if(animated){
windowView.animate()
.alpha(0)
.setDuration(250)
.setInterpolator(CubicBezierInterpolator.DEFAULT)
.withEndAction(()->{
wm.removeView(windowView);
if(onDone!=null)
onDone.run();
})
.start();
}else{
wm.removeView(windowView);
if(onDone!=null)
onDone.run();
}
}
@Override
public void onTransitionAnimationUpdate(float translateX, float translateY, float scale){
listener.setTransitioningViewTransform(translateX, translateY, scale);
}
@Override
public void onTransitionAnimationFinished(){
listener.endPhotoViewTransition();
}
@Override
public void onSetBackgroundAlpha(float alpha){
background.setAlpha(Math.round(255*alpha));
overlay.setAlpha(alpha);
confirmButton.setAlpha(alpha);
}
@Override
public void onStartSwipeToDismiss(){
}
@Override
public void onStartSwipeToDismissTransition(float velocityY){
}
@Override
public void onSwipeToDismissCanceled(){
}
@Override
public void onDismissed(){
listener.setPhotoViewVisibility(0, true);
wm.removeView(windowView);
listener.photoViewerDismissed();
}
@Override
public void onSingleTap(){
}
private void confirm(){
// stop receiving input events to allow the user to interact with the underlying UI while the animation is still running
WindowManager.LayoutParams wlp=(WindowManager.LayoutParams) windowView.getLayoutParams();
wlp.flags|=WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
windowView.setSystemUiVisibility(windowView.getSystemUiVisibility() | (activity.getWindow().getDecorView().getSystemUiVisibility() & (View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR)));
wm.updateViewLayout(windowView, wlp);
Drawable drawable=image.getDrawable();
zoomPanView.endAllAnimations();
Rect rect=new Rect();
image.getHitRect(rect);
float scale=image.getScaleX();
int x=Math.round((zoomPanView.getPaddingLeft()-rect.left)/scale);
int y=Math.round((zoomPanView.getPaddingTop()-rect.top)/scale);
int size=Math.round(V.dp(192)/scale);
if(x==0 && y==0 && drawable.getIntrinsicWidth()==drawable.getIntrinsicHeight() && size==drawable.getIntrinsicWidth()){
dismissWithTransition();
cropChosenListener.onCropChosen(drawable, originalUri);
return;
}
Bitmap croppedBitmap=Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
Canvas c=new Canvas(croppedBitmap);
c.translate(-x, -y);
drawable.draw(c);
MastodonAPIController.runInBackground(()->{
String mimetype;
if("file".equals(originalUri.getScheme())){
mimetype=UiUtils.getFileMediaType(new File(originalUri.getPath())).type();
}else{
mimetype=activity.getContentResolver().getType(originalUri);
}
if(mimetype==null)
mimetype="image/jpeg";
Bitmap.CompressFormat format=switch(mimetype){
case "image/png", "image/gif" -> Bitmap.CompressFormat.PNG;
default -> Bitmap.CompressFormat.JPEG;
};
File outputFile=new File(activity.getCacheDir(), "avatar_upload."+(format==Bitmap.CompressFormat.PNG ? "png" : "jpg"));
try(FileOutputStream out=new FileOutputStream(outputFile)){
croppedBitmap.compress(format, 97, out);
}catch(IOException e){
activity.runOnUiThread(()->{
Toast.makeText(activity, R.string.error_saving_file, Toast.LENGTH_SHORT).show();
dismiss(true, onCancel);
});
return;
}
outputFile.deleteOnExit();
activity.runOnUiThread(()->{
image.setImageBitmap(croppedBitmap);
image.getLayoutParams().width=image.getLayoutParams().height=size;
zoomPanView.updateLayout();
cropChosenListener.onCropChosen(new BitmapDrawable(croppedBitmap), Uri.fromFile(outputFile));
dismissWithTransition();
});
});
}
private void dismissWithTransition(){
zoomPanView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
zoomPanView.getViewTreeObserver().removeOnPreDrawListener(this);
listener.setPhotoViewVisibility(0, true);
int[] radius=new int[4];
Rect rect=new Rect();
if(listener.startPhotoViewTransition(0, rect, radius)){
zoomPanView.animateOut(rect, radius, 0);
}else{
windowView.animate()
.alpha(0)
.setDuration(300)
.setInterpolator(CubicBezierInterpolator.DEFAULT)
.withEndAction(AvatarCropper.this::onDismissed)
.start();
}
return true;
}
});
}
private static class OverlayDrawable extends Drawable{
private Path path=new Path(), tmpPath=new Path();
private Paint overlayPaint=new Paint(Paint.ANTI_ALIAS_FLAG), strokePaint=new Paint(Paint.ANTI_ALIAS_FLAG);
public OverlayDrawable(){
overlayPaint.setColor(0xb3000000);
strokePaint.setColor(0x4dffffff);
strokePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.ADD));
strokePaint.setStyle(Paint.Style.STROKE);
strokePaint.setStrokeWidth(V.dp(1));
}
@Override
public void draw(@NonNull Canvas canvas){
canvas.drawPath(path, overlayPaint);
Rect bounds=getBounds();
float size=V.dp(192)-strokePaint.getStrokeWidth();
float x=bounds.centerX()-size/2;
float y=bounds.centerY()-size/2;
float radius=V.dp(40)-strokePaint.getStrokeWidth()/2f;
canvas.drawRoundRect(x, y, x+size, y+size, radius, radius, strokePaint);
}
@Override
public void setAlpha(int alpha){
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter){
}
@Override
public int getOpacity(){
return PixelFormat.TRANSLUCENT;
}
@Override
protected void onBoundsChange(@NonNull Rect bounds){
path.rewind();
path.addRect(bounds.left, bounds.top, bounds.right, bounds.bottom, Path.Direction.CW);
tmpPath.rewind();
int size=V.dp(192);
int x=bounds.centerX()-size/2;
int y=bounds.centerY()-size/2;
tmpPath.addRoundRect(x, y, x+size, y+size, V.dp(40), V.dp(40), Path.Direction.CW);
path.op(tmpPath, Path.Op.DIFFERENCE);
}
}
public interface OnCropChosenListener{
void onCropChosen(Drawable thumbnail, Uri uri);
}
}

View File

@@ -17,7 +17,10 @@ import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
import android.graphics.Insets;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.graphics.drawable.BitmapDrawable;
@@ -28,17 +31,18 @@ import android.media.MediaPlayer;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.SystemClock;
import android.provider.MediaStore;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;
import android.util.Property;
import android.view.ContextThemeWrapper;
import android.view.DisplayCutout;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.MenuItem;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
@@ -53,18 +57,28 @@ import android.widget.ProgressBar;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.Toolbar;
import android.window.OnBackInvokedDispatcher;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.ComposeFragment;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.Snackbar;
import org.joinmastodon.android.ui.drawables.VideoPlayerSeekBarThumbDrawable;
import org.joinmastodon.android.ui.utils.BlurHashDecoder;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.WindowRootFrameLayout;
import org.parceler.Parcels;
import java.io.File;
import java.io.FileOutputStream;
@@ -77,14 +91,17 @@ import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.palette.graphics.ColorUtils;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
import me.grishka.appkit.Nav;
import me.grishka.appkit.imageloader.ImageCache;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.BottomSheet;
import me.grishka.appkit.views.FragmentRootLinearLayout;
import okio.BufferedSink;
import okio.Okio;
@@ -97,31 +114,38 @@ public class PhotoViewer implements ZoomPanView.Listener{
private Activity activity;
private List<Attachment> attachments;
private int[] backgroundColors;
private int currentIndex;
private WindowManager wm;
private Listener listener;
private Status status;
private String accountID;
private BaseStatusListFragment<?> parentFragment;
private FrameLayout windowView;
private WindowRootFrameLayout windowView;
private FragmentRootLinearLayout uiOverlay;
private ViewPager2 pager;
private ColorDrawable background=new ColorDrawable(0xff000000);
private ArrayList<MediaPlayer> players=new ArrayList<>();
private int screenOnRefCount=0;
private Toolbar toolbar;
private View toolbarWrap;
private SeekBar videoSeekBar;
private TextView videoTimeView;
private ImageButton videoPlayPauseButton;
private View videoControls;
private TextView altText;
private ImageButton backButton, downloadButton;
private View bottomBar;
private View postActions;
private View replyBtn, boostBtn, favoriteBtn, shareBtn, bookmarkBtn;
private TextView replyText, boostText, favoriteText;
private boolean uiVisible=true;
private AudioManager.OnAudioFocusChangeListener audioFocusListener=this::onAudioFocusChanged;
private Runnable uiAutoHider=()->{
if(uiVisible)
toggleUI();
};
private Animator currentSheetRelatedToolbarAnimation;
private Animator currentUiVisibilityAnimation;
private boolean videoPositionNeedsUpdating;
private Runnable videoPositionUpdater=this::updateVideoPosition;
@@ -157,53 +181,63 @@ public class PhotoViewer implements ZoomPanView.Listener{
}
};
public PhotoViewer(Activity activity, List<Attachment> attachments, int index, Status status, String accountID, Listener listener){
public PhotoViewer(Activity activity, BaseStatusListFragment<?> parentFragment, List<Attachment> attachments, int index, Status status, String accountID, Listener listener){
this.activity=activity;
this.attachments=attachments.stream().filter(a->a.type==Attachment.Type.IMAGE || a.type==Attachment.Type.GIFV || a.type==Attachment.Type.VIDEO).collect(Collectors.toList());
currentIndex=index;
this.listener=listener;
this.status=status;
this.accountID=accountID;
this.parentFragment=parentFragment;
backgroundColors=new int[this.attachments.size()];
int i=0;
float[] hsl=new float[3];
for(Attachment att:this.attachments){
if(TextUtils.isEmpty(att.blurhash)){
backgroundColors[i]=0xff000000;
}else{
ColorUtils.colorToHSL(BlurHashDecoder.decodeToSingleColor(att.blurhash) | 0xff000000, hsl);
hsl[2]=Math.min(hsl[2], 0.15f);
backgroundColors[i]=ColorUtils.HSLToColor(hsl);
}
i++;
}
wm=activity.getWindowManager();
windowView=new FrameLayout(activity){
@Override
public boolean dispatchKeyEvent(KeyEvent event){
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.TIRAMISU && event.getKeyCode()==KeyEvent.KEYCODE_BACK){
if(event.getAction()==KeyEvent.ACTION_DOWN){
onStartSwipeToDismissTransition(0f);
}
return true;
windowView=new WindowRootFrameLayout(activity);
windowView.setDispatchKeyEventListener((v, keyCode, event)->{
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.TIRAMISU && event.getKeyCode()==KeyEvent.KEYCODE_BACK){
if(event.getAction()==KeyEvent.ACTION_DOWN){
onStartSwipeToDismissTransition(0f);
}
return false;
return true;
}
@Override
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=29){
DisplayCutout cutout=insets.getDisplayCutout();
Insets tappable=insets.getTappableElementInsets();
if(cutout!=null){
// Make controls extend beneath the cutout, and replace insets to avoid cutout insets being filled with "navigation bar color"
int leftInset=Math.max(0, cutout.getSafeInsetLeft()-tappable.left);
int rightInset=Math.max(0, cutout.getSafeInsetRight()-tappable.right);
toolbarWrap.setPadding(leftInset, 0, rightInset, 0);
videoControls.setPadding(leftInset, 0, rightInset, 0);
}else{
toolbarWrap.setPadding(0, 0, 0, 0);
videoControls.setPadding(0, 0, 0, 0);
}
insets=insets.replaceSystemWindowInsets(tappable.left, tappable.top, tappable.right, tappable.bottom);
return false;
});
windowView.setDispatchApplyWindowInsetsListener((v, insets)->{
int bottomInset=insets.getSystemWindowInsetBottom();
bottomBar.setPadding(bottomBar.getPaddingLeft(), bottomBar.getPaddingTop(), bottomBar.getPaddingRight(), bottomInset>0 ? Math.max(bottomInset+V.dp(8), V.dp(40)) : V.dp(12));
insets=insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0);
if(Build.VERSION.SDK_INT>=29){
DisplayCutout cutout=insets.getDisplayCutout();
Insets tappable=insets.getTappableElementInsets();
if(cutout!=null){
// Make controls extend beneath the cutout, and replace insets to avoid cutout insets being filled with "navigation bar color"
int leftInset=Math.max(0, cutout.getSafeInsetLeft()-tappable.left);
int rightInset=Math.max(0, cutout.getSafeInsetRight()-tappable.right);
toolbarWrap.setPadding(leftInset, 0, rightInset, 0);
bottomBar.setPadding(leftInset, bottomBar.getPaddingTop(), rightInset, bottomBar.getPaddingBottom());
}else{
toolbarWrap.setPadding(0, 0, 0, 0);
bottomBar.setPadding(0, bottomBar.getPaddingTop(), 0, bottomBar.getPaddingBottom());
}
uiOverlay.dispatchApplyWindowInsets(insets);
int bottomInset=insets.getSystemWindowInsetBottom();
if(bottomInset>0 && bottomInset<V.dp(36)){
uiOverlay.setPadding(uiOverlay.getPaddingLeft(), uiOverlay.getPaddingTop(), uiOverlay.getPaddingRight(), V.dp(36));
}
return insets.consumeSystemWindowInsets();
insets=insets.replaceSystemWindowInsets(tappable.left, tappable.top, tappable.right, bottomBar.getVisibility()==View.VISIBLE ? 0 : tappable.bottom);
}
};
uiOverlay.dispatchApplyWindowInsets(insets);
return insets.consumeSystemWindowInsets();
});
windowView.setBackground(background);
background.setAlpha(0);
pager=new ViewPager2(activity);
@@ -214,6 +248,11 @@ public class PhotoViewer implements ZoomPanView.Listener{
public void onPageSelected(int position){
onPageChanged(position);
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels){
updateBackgroundColor(position, positionOffset);
}
});
windowView.addView(pager);
pager.setMotionEventSplittingEnabled(false);
@@ -222,19 +261,22 @@ public class PhotoViewer implements ZoomPanView.Listener{
uiOverlay.setStatusBarColor(0x80000000);
uiOverlay.setNavigationBarColor(0x80000000);
toolbarWrap=uiOverlay.findViewById(R.id.toolbar_wrap);
toolbar=uiOverlay.findViewById(R.id.toolbar);
toolbar.setNavigationOnClickListener(v->onStartSwipeToDismissTransition(0));
if(status!=null)
toolbar.getMenu().add(R.string.info).setIcon(R.drawable.ic_info_24px).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
else
toolbar.getMenu().add(R.string.download).setIcon(R.drawable.ic_download_24px).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
toolbar.setOnMenuItemClickListener(item->{
if(status!=null)
showInfoSheet();
else
saveCurrentFile();
return true;
});
backButton=uiOverlay.findViewById(R.id.btn_back);
backButton.setOnClickListener(v->onStartSwipeToDismissTransition(0));
downloadButton=uiOverlay.findViewById(R.id.btn_download);
downloadButton.setOnClickListener(v->saveCurrentFile());
bottomBar=uiOverlay.findViewById(R.id.bottom_bar);
postActions=uiOverlay.findViewById(R.id.post_actions);
replyBtn=uiOverlay.findViewById(R.id.reply_btn);
boostBtn=uiOverlay.findViewById(R.id.boost_btn);
favoriteBtn=uiOverlay.findViewById(R.id.favorite_btn);
bookmarkBtn=uiOverlay.findViewById(R.id.bookmark_btn);
shareBtn=uiOverlay.findViewById(R.id.share_btn);
replyText=uiOverlay.findViewById(R.id.reply);
boostText=uiOverlay.findViewById(R.id.boost);
favoriteText=uiOverlay.findViewById(R.id.favorite);
uiOverlay.setAlpha(0f);
videoControls=uiOverlay.findViewById(R.id.video_player_controls);
videoSeekBar=uiOverlay.findViewById(R.id.seekbar);
@@ -247,6 +289,25 @@ public class PhotoViewer implements ZoomPanView.Listener{
videoLastTimeUpdatePosition=-1;
updateVideoTimeText(0);
}
altText=uiOverlay.findViewById(R.id.alt_text);
altText.setOnClickListener(v->showAltTextSheet());
updateAltText();
updateBackgroundColor(currentIndex, 0);
if(status==null){
bottomBar.setVisibility(View.GONE);
}else{
Paint paint=new Paint();
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.ADD));
postActions.setLayerType(View.LAYER_TYPE_HARDWARE, paint);
updatePostActions();
replyBtn.setOnClickListener(this::onPostActionClick);
boostBtn.setOnClickListener(this::onPostActionClick);
favoriteBtn.setOnClickListener(this::onPostActionClick);
bookmarkBtn.setOnClickListener(this::onPostActionClick);
shareBtn.setOnClickListener(this::onPostActionClick);
}
WindowManager.LayoutParams wlp=new WindowManager.LayoutParams(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT);
wlp.type=WindowManager.LayoutParams.TYPE_APPLICATION;
@@ -296,6 +357,11 @@ public class PhotoViewer implements ZoomPanView.Listener{
if(fromUser){
float p=progress/10000f;
updateVideoTimeText(Math.round(p*videoDuration));
// This moves the time view in sync with the seekbar thumb, but also makes sure it doesn't go off screen
// (there must be at least 16dp between the time and the edge of the screen)
float timeX=p*(seekBar.getWidth()-V.dp(32))+V.dp(16)-videoTimeView.getWidth()/2f;
videoTimeView.setTranslationX(Math.max(-(videoTimeView.getLeft()-V.dp(16)), Math.min(timeX, videoControls.getWidth()-V.dp(16)-videoTimeView.getWidth()-videoTimeView.getLeft())));
}
}
@@ -305,6 +371,14 @@ public class PhotoViewer implements ZoomPanView.Listener{
if(!uiVisible) // If dragging started during hide animation
toggleUI();
windowView.removeCallbacks(uiAutoHider);
V.setVisibilityAnimated(videoTimeView, View.VISIBLE);
postActions.animate().alpha(0f).setDuration(300).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
altText.animate().alpha(0f).setDuration(300).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
if(altText.getVisibility()==View.VISIBLE){
videoTimeView.setTranslationY(seekBar.getHeight()+V.dp(12));
}else{
videoTimeView.setTranslationY(-videoTimeView.getHeight()-V.dp(12));
}
}
@Override
@@ -312,15 +386,24 @@ public class PhotoViewer implements ZoomPanView.Listener{
MediaPlayer player=findCurrentVideoPlayer();
if(player!=null){
float progress=seekBar.getProgress()/10000f;
player.seekTo(Math.round(progress*player.getDuration()));
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O)
player.seekTo(Math.round(progress*player.getDuration()), MediaPlayer.SEEK_CLOSEST);
else
player.seekTo(Math.round(progress*player.getDuration()));
}
hideUiDelayed();
V.setVisibilityAnimated(videoTimeView, View.INVISIBLE);
postActions.animate().alpha(1f).setDuration(300).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
altText.animate().alpha(1f).setDuration(300).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
}
});
videoSeekBar.setThumb(new VideoPlayerSeekBarThumbDrawable());
E.register(this);
}
public void removeMenu(){
toolbar.getMenu().clear();
downloadButton.setVisibility(View.GONE);
}
@Override
@@ -371,7 +454,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
.alpha(0)
.setDuration(300)
.setInterpolator(CubicBezierInterpolator.DEFAULT)
.withEndAction(()->wm.removeView(windowView))
.withEndAction(this::onDismissed)
.start();
}
}
@@ -399,6 +482,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
if(receiverRegistered){
activity.unregisterReceiver(downloadCompletedReceiver);
}
E.unregister(this);
}
@Override
@@ -407,21 +491,45 @@ public class PhotoViewer implements ZoomPanView.Listener{
}
private void toggleUI(){
if(currentUiVisibilityAnimation!=null)
currentUiVisibilityAnimation.cancel();
if(uiVisible){
uiOverlay.animate()
.alpha(0f)
.setDuration(250)
.setInterpolator(CubicBezierInterpolator.DEFAULT)
.withEndAction(()->uiOverlay.setVisibility(View.GONE))
.start();
AnimatorSet set=new AnimatorSet();
set.playTogether(
ObjectAnimator.ofFloat(uiOverlay, View.ALPHA, 0f),
ObjectAnimator.ofFloat(toolbarWrap, View.TRANSLATION_Y, V.dp(-32)),
ObjectAnimator.ofFloat(bottomBar, View.TRANSLATION_Y, V.dp(32))
);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.setDuration(250);
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
uiOverlay.setVisibility(View.GONE);
currentUiVisibilityAnimation=null;
}
});
currentUiVisibilityAnimation=set;
set.start();
windowView.setSystemUiVisibility(windowView.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN);
}else{
uiOverlay.setVisibility(View.VISIBLE);
uiOverlay.animate()
.alpha(1f)
.setDuration(300)
.setInterpolator(CubicBezierInterpolator.DEFAULT)
.start();
AnimatorSet set=new AnimatorSet();
set.playTogether(
ObjectAnimator.ofFloat(uiOverlay, View.ALPHA, 1f),
ObjectAnimator.ofFloat(toolbarWrap, View.TRANSLATION_Y, 0),
ObjectAnimator.ofFloat(bottomBar, View.TRANSLATION_Y, 0)
);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.setDuration(300);
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
currentUiVisibilityAnimation=null;
}
});
currentUiVisibilityAnimation=set;
set.start();
windowView.setSystemUiVisibility(windowView.getSystemUiVisibility() & ~(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN));
if(attachments.get(currentIndex).type==Attachment.Type.VIDEO)
hideUiDelayed(5000);
@@ -448,6 +556,105 @@ public class PhotoViewer implements ZoomPanView.Listener{
videoLastTimeUpdatePosition=-1;
updateVideoTimeText(0);
}
updateAltText();
}
private void updateAltText(){
Attachment att=attachments.get(currentIndex);
if(TextUtils.isEmpty(att.description)){
altText.setVisibility(View.GONE);
}else{
altText.setVisibility(View.VISIBLE);
altText.setText(att.description);
altText.setMaxLines(att.type==Attachment.Type.VIDEO ? 3 : 4);
}
}
private void updateBackgroundColor(int position, float positionOffset){
int color;
if(positionOffset==0){
color=backgroundColors[position];
}else{
color=UiUtils.alphaBlendColors(backgroundColors[position], backgroundColors[position+1], positionOffset);
}
int alpha=background.getAlpha();
background.setColor(color);
background.setAlpha(alpha);
uiOverlay.setStatusBarColor(color & 0xe6ffffff);
uiOverlay.setNavigationBarColor(color & 0xe6ffffff);
bottomBar.setBackgroundTintList(ColorStateList.valueOf(color));
}
private void updatePostActions(){
bindActionButton(replyText, status.repliesCount);
bindActionButton(boostText, status.reblogsCount);
bindActionButton(favoriteText, status.favouritesCount);
boostBtn.setSelected(status.reblogged);
favoriteBtn.setSelected(status.favourited);
bookmarkBtn.setSelected(status.bookmarked);
bookmarkBtn.setContentDescription(activity.getString(status.bookmarked ? R.string.remove_bookmark : R.string.add_bookmark));
boolean isOwn=status.account.id.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id);
boostBtn.setEnabled(status.visibility==StatusPrivacy.PUBLIC || status.visibility==StatusPrivacy.UNLISTED
|| (status.visibility==StatusPrivacy.PRIVATE && isOwn));
boostBtn.setAlpha(boostBtn.isEnabled() ? 1 : 0.5f);
Drawable d=activity.getResources().getDrawable(switch(status.visibility){
case PUBLIC, UNLISTED -> R.drawable.ic_boost;
case PRIVATE -> isOwn ? R.drawable.ic_boost_private : R.drawable.ic_boost_disabled_24px;
case DIRECT -> R.drawable.ic_boost_disabled_24px;
}, activity.getTheme());
d.setBounds(0, 0, V.dp(20), V.dp(20));
boostText.setCompoundDrawablesRelative(d, null, null, null);
}
private void bindActionButton(TextView btn, long count){
if(count>0){
btn.setText(UiUtils.abbreviateNumber(count));
btn.setCompoundDrawablePadding(V.dp(6));
}else{
btn.setText("");
btn.setCompoundDrawablePadding(0);
}
}
private void onPostActionClick(View view){
int id=view.getId();
if(id==R.id.boost_btn){
if(status!=null){
AccountSessionManager.get(accountID).getStatusInteractionController().setReblogged(status, !status.reblogged);
}
}else if(id==R.id.favorite_btn){
if(status!=null){
AccountSessionManager.get(accountID).getStatusInteractionController().setFavorited(status, !status.favourited);
}
}else if(id==R.id.share_btn){
if(status!=null){
UiUtils.openSystemShareSheet(activity, status);
}
}else if(id==R.id.bookmark_btn){
if(status!=null){
AccountSessionManager.get(accountID).getStatusInteractionController().setBookmarked(status, !status.bookmarked);
}
}else if(id==R.id.reply_btn){
parentFragment.maybeShowPreReplySheet(status, ()->{
onDismissed();
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("replyTo", Parcels.wrap(status));
Nav.go(activity, ComposeFragment.class, args);
});
}
}
@Subscribe
public void onStatusCountersUpdated(StatusCountersUpdatedEvent ev){
if(status!=null && ev.id.equals(status.id)){
status.reblogsCount=ev.reblogs;
status.favouritesCount=ev.favorites;
status.reblogged=ev.reblogged;
status.favourited=ev.favorited;
status.bookmarked=ev.bookmarked;
updatePostActions();
}
}
/**
@@ -690,91 +897,12 @@ public class PhotoViewer implements ZoomPanView.Listener{
}
}
private void showInfoSheet(){
private void showAltTextSheet(){
pauseVideo();
PhotoViewerInfoSheet sheet=new PhotoViewerInfoSheet(new ContextThemeWrapper(activity, R.style.Theme_Mastodon_Dark), attachments.get(currentIndex), toolbar.getHeight(), new PhotoViewerInfoSheet.Listener(){
private boolean ignoreBeforeDismiss;
@Override
public void onBeforeDismiss(int duration){
if(ignoreBeforeDismiss)
return;
if(currentSheetRelatedToolbarAnimation!=null)
currentSheetRelatedToolbarAnimation.cancel();
AnimatorSet set=new AnimatorSet();
set.playTogether(
ObjectAnimator.ofFloat(pager, View.TRANSLATION_Y, 0),
ObjectAnimator.ofFloat(toolbarWrap, View.ALPHA, 1f),
ObjectAnimator.ofArgb(uiOverlay, STATUS_BAR_COLOR_PROPERTY, 0x80000000)
);
set.setDuration(duration);
set.setInterpolator(CubicBezierInterpolator.EASE_OUT);
currentSheetRelatedToolbarAnimation=set;
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
currentSheetRelatedToolbarAnimation=null;
}
});
set.start();
}
@Override
public void onDismissEntireViewer(){
ignoreBeforeDismiss=true;
onStartSwipeToDismissTransition(0);
}
@Override
public void onButtonClick(int id){
if(id==R.id.btn_boost){
if(status!=null){
AccountSessionManager.get(accountID).getStatusInteractionController().setReblogged(status, !status.reblogged);
}
}else if(id==R.id.btn_favorite){
if(status!=null){
AccountSessionManager.get(accountID).getStatusInteractionController().setFavorited(status, !status.favourited);
}
}else if(id==R.id.btn_share){
if(status!=null){
UiUtils.openSystemShareSheet(activity, status);
}
}else if(id==R.id.btn_bookmark){
if(status!=null){
AccountSessionManager.get(accountID).getStatusInteractionController().setBookmarked(status, !status.bookmarked);
}
}else if(id==R.id.btn_download){
saveCurrentFile();
}
}
});
sheet.setStatus(status);
BottomSheet sheet=new AltTextSheet(new ContextThemeWrapper(activity, UiUtils.getThemeForUserPreference(activity, GlobalUserPreferences.ThemePreference.DARK)),
attachments.get(currentIndex));
sheet.show();
if(currentSheetRelatedToolbarAnimation!=null)
currentSheetRelatedToolbarAnimation.cancel();
sheet.getWindow().getDecorView().getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
sheet.getWindow().getDecorView().getViewTreeObserver().removeOnPreDrawListener(this);
AnimatorSet set=new AnimatorSet();
set.playTogether(
ObjectAnimator.ofFloat(pager, View.TRANSLATION_Y, -pager.getHeight()*0.2f),
ObjectAnimator.ofFloat(toolbarWrap, View.ALPHA, 0f),
ObjectAnimator.ofArgb(uiOverlay, STATUS_BAR_COLOR_PROPERTY, 0)
);
set.setDuration(300);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
currentSheetRelatedToolbarAnimation=set;
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
currentSheetRelatedToolbarAnimation=null;
}
});
set.start();
return true;
}
});
sheet.getWindow().getDecorView().setSystemUiVisibility(sheet.getWindow().getDecorView().getSystemUiVisibility() & ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
}
public interface Listener{

View File

@@ -1,182 +0,0 @@
package org.joinmastodon.android.ui.photoviewer;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.ColorDrawable;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.TextView;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusPrivacy;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import androidx.annotation.NonNull;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.BottomSheet;
public class PhotoViewerInfoSheet extends BottomSheet{
private final Attachment attachment;
private final View buttonsContainer;
private final TextView altText;
private final ImageButton backButton, infoButton;
private final Button boostBtn, favoriteBtn, bookmarkBtn;
private final Listener listener;
private String statusID;
public PhotoViewerInfoSheet(@NonNull Context context, Attachment attachment, int toolbarHeight, Listener listener){
super(context);
this.attachment=attachment;
this.listener=listener;
dimAmount=0;
View content=context.getSystemService(LayoutInflater.class).inflate(R.layout.sheet_photo_viewer_info, null);
setContentView(content);
setNavigationBarBackground(new ColorDrawable(UiUtils.alphaBlendColors(UiUtils.getThemeColor(context, R.attr.colorM3Surface),
UiUtils.getThemeColor(context, R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
buttonsContainer=findViewById(R.id.buttons_container);
altText=findViewById(R.id.alt_text);
if(TextUtils.isEmpty(attachment.description)){
findViewById(R.id.alt_text).setVisibility(View.GONE);
findViewById(R.id.alt_text_title).setVisibility(View.GONE);
findViewById(R.id.divider).setVisibility(View.GONE);
}else{
altText.setText(attachment.description);
findViewById(R.id.alt_text_help).setOnClickListener(v->showAltTextHelp());
}
backButton=new ImageButton(context);
backButton.setImageResource(me.grishka.appkit.R.drawable.ic_arrow_back);
backButton.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(context, R.attr.colorM3OnSurfaceVariant)));
backButton.setBackgroundResource(R.drawable.bg_button_m3_tonal_icon);
backButton.setOutlineProvider(ViewOutlineProvider.BACKGROUND);
backButton.setElevation(V.dp(2));
backButton.setAlpha(0f);
backButton.setContentDescription(context.getString(R.string.back));
backButton.setOnClickListener(v->{
listener.onDismissEntireViewer();
dismiss();
});
infoButton=new ImageButton(context);
infoButton.setImageResource(R.drawable.ic_info_fill1_24px);
infoButton.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(context, R.attr.colorM3OnPrimary)));
infoButton.setBackgroundResource(R.drawable.bg_button_m3_filled_icon);
infoButton.setOutlineProvider(ViewOutlineProvider.BACKGROUND);
infoButton.setElevation(V.dp(2));
infoButton.setAlpha(0f);
infoButton.setSelected(true);
infoButton.setContentDescription(context.getString(R.string.info));
infoButton.setOnClickListener(v->dismiss());
FrameLayout.LayoutParams lp=new FrameLayout.LayoutParams(V.dp(48), V.dp(48));
lp.topMargin=toolbarHeight/2-V.dp(24);
lp.leftMargin=lp.rightMargin=V.dp(4);
lp.gravity=Gravity.START | Gravity.TOP;
container.addView(backButton, lp);
lp=new FrameLayout.LayoutParams(lp);
lp.leftMargin=lp.rightMargin=0;
lp.gravity=Gravity.END | Gravity.TOP;
container.addView(infoButton, lp);
boostBtn=findViewById(R.id.btn_boost);
favoriteBtn=findViewById(R.id.btn_favorite);
bookmarkBtn=findViewById(R.id.btn_bookmark);
View.OnClickListener clickListener=v->listener.onButtonClick(v.getId());
boostBtn.setOnClickListener(clickListener);
favoriteBtn.setOnClickListener(clickListener);
findViewById(R.id.btn_share).setOnClickListener(clickListener);
bookmarkBtn.setOnClickListener(clickListener);
findViewById(R.id.btn_download).setOnClickListener(clickListener);
}
private void showAltTextHelp(){
new M3AlertDialogBuilder(getContext())
.setTitle(R.string.what_is_alt_text)
.setMessage(UiUtils.fixBulletListInString(getContext(), R.string.alt_text_help))
.setPositiveButton(R.string.ok, null)
.show();
}
@Override
public void dismiss(){
if(dismissed)
return;
int height=content.getHeight();
int duration=Math.max(60, (int) (180 * (height - content.getTranslationY()) / (float) height));
listener.onBeforeDismiss(duration);
backButton.animate().alpha(0).setDuration(duration).setInterpolator(CubicBezierInterpolator.EASE_OUT).start();
infoButton.animate().alpha(0).setDuration(duration).setInterpolator(CubicBezierInterpolator.EASE_OUT).start();
super.dismiss();
E.unregister(this);
}
@Override
public void show(){
super.show();
E.register(this);
content.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
content.getViewTreeObserver().removeOnPreDrawListener(this);
backButton.animate().alpha(1).setDuration(300).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
infoButton.animate().alpha(1).setDuration(300).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
return true;
}
});
}
public void setStatus(Status status){
statusID=status.id;
boostBtn.setCompoundDrawablesWithIntrinsicBounds(0, switch(status.visibility){
case DIRECT -> R.drawable.ic_boost_disabled_24px;
case PUBLIC, UNLISTED -> R.drawable.ic_boost;
case PRIVATE -> R.drawable.ic_boost_private;
}, 0, 0);
boostBtn.setEnabled(status.visibility!=StatusPrivacy.DIRECT);
setButtonStates(status.reblogged, status.favourited, status.bookmarked);
}
@Subscribe
public void onCountersUpdated(StatusCountersUpdatedEvent ev){
if(ev.id.equals(statusID)){
setButtonStates(ev.reblogged, ev.favorited, ev.bookmarked);
}
}
private void setButtonStates(boolean reblogged, boolean favorited, boolean bookmarked){
boostBtn.setText(reblogged ? R.string.button_reblogged : R.string.button_reblog);
boostBtn.setSelected(reblogged);
favoriteBtn.setText(favorited ? R.string.button_favorited : R.string.button_favorite);
favoriteBtn.setSelected(favorited);
bookmarkBtn.setText(bookmarked ? R.string.bookmarked : R.string.add_bookmark);
bookmarkBtn.setSelected(bookmarked);
}
public interface Listener{
void onBeforeDismiss(int duration);
void onDismissEntireViewer();
void onButtonClick(int id);
}
}

View File

@@ -46,6 +46,8 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
private float lastScaleCenterX, lastScaleCenterY;
private boolean canScrollLeft, canScrollRight;
private ArrayList<SpringAnimation> runningTransformAnimations=new ArrayList<>(), runningTransitionAnimations=new ArrayList<>();
private boolean fill; // whether the image should fill the viewport at min scale
private boolean swipeToDismissEnabled=true;
private RectF tmpRect=new RectF(), tmpRect2=new RectF();
// the initial/final crop rect for open/close transitions, in child coordinates
@@ -116,14 +118,19 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
if(child==null)
return;
int width=right-left;
int height=bottom-top;
int width=right-left-getPaddingLeft()-getPaddingRight();
int height=bottom-top-getPaddingTop()-getPaddingBottom();
if(width==0 || height==0 || child.getWidth()==0 || child.getWidth()==0){
matrix.reset();
return;
}
float scale=Math.min(width/(float)child.getWidth(), height/(float)child.getHeight());
float scale;
if(fill){
scale=Math.max(width/(float)child.getWidth(), height/(float)child.getHeight());
}else{
scale=Math.min(width/(float)child.getWidth(), height/(float)child.getHeight());
}
minScale=scale;
maxScale=Math.max(3f, height/(float)child.getHeight());
matrix.setScale(scale, scale);
@@ -323,14 +330,14 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
private void updateLimits(float targetScale){
float scaledWidth=child.getWidth()*targetScale;
float scaledHeight=child.getHeight()*targetScale;
if(scaledWidth>getWidth()){
minTransX=(getWidth()-Math.round(scaledWidth))/2f;
if(scaledWidth>getInsetWidth()){
minTransX=(getInsetWidth()-Math.round(scaledWidth))/2f;
maxTransX=-minTransX;
}else{
minTransX=maxTransX=0f;
}
if(scaledHeight>getHeight()){
minTransY=(getHeight()-Math.round(scaledHeight))/2f;
if(scaledHeight>getInsetHeight()){
minTransY=(getInsetHeight()-Math.round(scaledHeight))/2f;
maxTransY=-minTransY;
}else{
minTransY=maxTransY=0f;
@@ -468,10 +475,10 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
@Override
public boolean onScale(ScaleGestureDetector detector){
float factor=detector.getScaleFactor();
matrix.postScale(factor, factor, detector.getFocusX()-getWidth()/2f, detector.getFocusY()-getHeight()/2f);
matrix.postScale(factor, factor, detector.getFocusX()-getInsetWidth()/2f-getPaddingLeft(), detector.getFocusY()-getInsetHeight()/2f-getPaddingTop());
updateViewTransform(false);
lastScaleCenterX=detector.getFocusX()-getWidth()/2f;
lastScaleCenterY=detector.getFocusY()-getHeight()/2f;
lastScaleCenterX=detector.getFocusX()-getInsetWidth()/2f-getPaddingLeft();
lastScaleCenterY=detector.getFocusY()-getInsetHeight()/2f-getPaddingTop();
return true;
}
@@ -510,7 +517,7 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
return false;
if(child.getScaleX()<maxScale){
float scale=maxScale/child.getScaleX();
matrix.postScale(scale, scale, e.getX()-getWidth()/2f, e.getY()-getHeight()/2f);
matrix.postScale(scale, scale, e.getX()-getInsetWidth()/2f-getPaddingLeft(), e.getY()-getInsetHeight()/2f-getPaddingTop());
matrix.getValues(matrixValues);
transX=matrixValues[Matrix.MTRANS_X];
transY=matrixValues[Matrix.MTRANS_Y];
@@ -554,7 +561,7 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY){
if(minTransY==maxTransY && minTransY==0f){
if(minTransX==maxTransX && minTransX==0f){
if(Math.abs(totalScrollY)>Math.abs(totalScrollX)){
if(Math.abs(totalScrollY)>Math.abs(totalScrollX) && swipeToDismissEnabled){
if(!swipingToDismiss){
swipingToDismiss=true;
matrix.postTranslate(-totalScrollX, 0);
@@ -630,6 +637,38 @@ public class ZoomPanView extends FrameLayout implements ScaleGestureDetector.OnS
}
}
public int getInsetWidth(){
return getWidth()-getPaddingLeft()-getPaddingRight();
}
public int getInsetHeight(){
return getHeight()-getPaddingTop()-getPaddingBottom();
}
public void setFill(boolean fill){
this.fill=fill;
}
public void endAllAnimations(){
if(!runningTransformAnimations.isEmpty()){
endTransformAnimations();
}else{
springBack();
endTransformAnimations();
}
updateViewTransform(false);
}
public void setSwipeToDismissEnabled(boolean swipeToDismissEnabled){
this.swipeToDismissEnabled=swipeToDismissEnabled;
}
private void endTransformAnimations(){
for(SpringAnimation anim:new ArrayList<>(runningTransformAnimations)){
anim.skipToEnd();
}
}
public interface Listener{
void onTransitionAnimationUpdate(float translateX, float translateY, float scale);
void onTransitionAnimationFinished();

View File

@@ -72,6 +72,7 @@ public class AccountSwitcherSheet extends BottomSheet{
imgLoader=new ListImageLoaderWrapper(activity, list, list, null);
list.setClipToPadding(false);
list.setLayoutManager(new LinearLayoutManager(activity));
list.setOverScrollMode(View.OVER_SCROLL_IF_CONTENT_SCROLLS);
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
View handle=new View(activity);

View File

@@ -0,0 +1,40 @@
package org.joinmastodon.android.ui.text;
import android.content.Context;
import android.text.TextPaint;
import android.text.style.TypefaceSpan;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.ColorContrastMode;
import org.joinmastodon.android.ui.utils.UiUtils;
import androidx.annotation.NonNull;
import me.grishka.appkit.utils.V;
public abstract class BaseMonospaceSpan extends TypefaceSpan{
private final Context context;
public BaseMonospaceSpan(Context context){
super("monospace");
this.context=context;
}
@Override
public void updateDrawState(@NonNull TextPaint paint){
super.updateDrawState(paint);
if(!UiUtils.isDarkTheme() && UiUtils.getColorContrastMode(context)==ColorContrastMode.HIGH){
}else{
paint.setColor(UiUtils.getThemeColor(context, R.attr.colorRichTextText));
}
paint.setTextSize(paint.getTextSize()*0.9375f);
paint.baselineShift=V.dp(-1);
}
@Override
public void updateMeasureState(@NonNull TextPaint paint){
super.updateMeasureState(paint);
paint.setTextSize(paint.getTextSize()*0.9375f);
paint.baselineShift=V.dp(-1);
}
}

View File

@@ -0,0 +1,74 @@
package org.joinmastodon.android.ui.text;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.text.Layout;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.style.CharacterStyle;
import android.text.style.LeadingMarginSpan;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.utils.UiUtils;
import androidx.annotation.NonNull;
import me.grishka.appkit.utils.V;
public class BlockQuoteSpan extends CharacterStyle implements LeadingMarginSpan{
private final Context context;
private Drawable icon;
private boolean firstLevel;
private Paint paint=new Paint();
public BlockQuoteSpan(Context context, boolean firstLevel){
this.context=context;
icon=context.getResources().getDrawable(R.drawable.quote, context.getTheme()).mutate();
this.firstLevel=firstLevel;
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(V.dp(3));
}
@Override
public int getLeadingMargin(boolean first){
return V.dp(firstLevel ? 32 : 18);
}
@Override
public void drawLeadingMargin(@NonNull Canvas c, @NonNull Paint p, int x, int dir, int top, int baseline, int bottom, @NonNull CharSequence text, int start, int end, boolean first, @NonNull Layout layout){
if(text instanceof Spanned s && s.getSpanStart(this)==start){
int color=UiUtils.getThemeColor(context, R.attr.colorRichTextDecorations);
int level=s.getSpans(start, end, LeadingMarginSpan.class).length-1;
if(dir<0){ // RTL
if(level==0){
icon.setColorFilter(color, PorterDuff.Mode.SRC_IN);
icon.setBounds(layout.getWidth()-icon.getIntrinsicWidth(), top, layout.getWidth(), top+icon.getIntrinsicHeight());
icon.draw(c);
}else{
paint.setColor(color);
float xOffset=layout.getWidth()-V.dp(32+18*(level-1)+1.5f);
c.drawLine(xOffset, top, xOffset, layout.getLineBottom(layout.getLineForOffset(s.getSpanEnd(this))), paint);
}
}else{
if(level==0){
icon.setColorFilter(color, PorterDuff.Mode.SRC_IN);
icon.setBounds(x, top, x+icon.getIntrinsicWidth(), top+icon.getIntrinsicHeight());
icon.draw(c);
}else{
paint.setColor(color);
float xOffset=x+V.dp(32+18*(level-1)+1.5f);
c.drawLine(xOffset, top, xOffset, layout.getLineBottom(layout.getLineForOffset(s.getSpanEnd(this))), paint);
}
}
}
}
@Override
public void updateDrawState(TextPaint tp){
tp.setColor(UiUtils.getThemeColor(context, R.attr.colorRichTextText));
}
}

View File

@@ -0,0 +1,9 @@
package org.joinmastodon.android.ui.text;
import android.content.Context;
public class CodeBlockSpan extends BaseMonospaceSpan{
public CodeBlockSpan(Context context){
super(context);
}
}

View File

@@ -1,11 +1,15 @@
package org.joinmastodon.android.ui.text;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Typeface;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import android.widget.TextView;
import com.twitter.twittertext.Regex;
@@ -34,6 +38,7 @@ import java.util.regex.Pattern;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import me.grishka.appkit.utils.V;
public class HtmlParser{
private static final String TAG="HtmlParser";
@@ -69,7 +74,7 @@ public class HtmlParser{
* @param emojis Custom emojis that are present in source as <code>:code:</code>
* @return a spanned string
*/
public static SpannableStringBuilder parse(String source, List<Emoji> emojis, List<Mention> mentions, List<Hashtag> tags, String accountID, Object parentObject){
public static SpannableStringBuilder parse(String source, List<Emoji> emojis, List<Mention> mentions, List<Hashtag> tags, String accountID, Object parentObject, Context context){
class SpanInfo{
public Object span;
public int start;
@@ -88,15 +93,53 @@ public class HtmlParser{
Map<String, Hashtag> tagsByTag=tags.stream().distinct().collect(Collectors.toMap(t->t.name.toLowerCase(), Function.identity()));
Map<String, Mention> mentionsByID=mentions.stream().distinct().collect(Collectors.toMap(m->m.id, Function.identity()));
source=source.replaceAll("[\u2028\u2029]", "<br>");
final SpannableStringBuilder ssb=new SpannableStringBuilder();
Jsoup.parseBodyFragment(source).body().traverse(new NodeVisitor(){
private final ArrayList<SpanInfo> openSpans=new ArrayList<>();
private boolean lastElementWasBlock=false;
private boolean isInsidePre(){
for(SpanInfo si:openSpans){
if(si.span instanceof CodeBlockSpan)
return true;
}
return false;
}
private boolean isInsideBlockquote(){
for(SpanInfo si:openSpans){
if(si.span instanceof BlockQuoteSpan)
return true;
}
return false;
}
@SuppressLint("DefaultLocale")
@Override
public void head(@NonNull Node node, int depth){
if(node instanceof TextNode textNode){
ssb.append(textNode.text());
if(lastElementWasBlock){
lastElementWasBlock=false;
if(!textNode.text().trim().isEmpty()){
ssb.append('\n');
ssb.append("\n", new SpacerSpan(1, V.dp(8)), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
if(isInsidePre()){
ssb.append(textNode.getWholeText().stripTrailing());
}else{
String text=textNode.text();
if(ssb.length()==0 || ssb.charAt(ssb.length()-1)=='\n')
text=text.stripLeading();
ssb.append(text);
}
}else if(node instanceof Element el){
if(lastElementWasBlock || (el.isBlock() && !"li".equals(el.nodeName()) && !"ul".equals(el.nodeName()) && !"ol".equals(el.nodeName()) && ssb.length()>0 && ssb.charAt(ssb.length()-1)!='\n')){
lastElementWasBlock=false;
ssb.append('\n');
ssb.append("\n", new SpacerSpan(1, V.dp(8)), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
switch(el.nodeName()){
case "a" -> {
Object linkObject=null;
@@ -131,6 +174,44 @@ public class HtmlParser{
openSpans.add(new SpanInfo(new InvisibleSpan(), ssb.length(), el));
}
}
case "b", "strong" -> openSpans.add(new SpanInfo(new StyleSpan(Typeface.BOLD), ssb.length(), el));
case "i", "em" -> openSpans.add(new SpanInfo(new StyleSpan(Typeface.ITALIC), ssb.length(), el));
case "s", "del" -> openSpans.add(new SpanInfo(new StrikethroughSpan(), ssb.length(), el));
case "code" -> {
if(!isInsidePre()){
openSpans.add(new SpanInfo(new MonospaceSpan(context), ssb.length(), el));
ssb.append(" ", new SpacerSpan(V.dp(4), 0), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
case "pre" -> openSpans.add(new SpanInfo(new CodeBlockSpan(context), ssb.length(), el));
case "li" -> {
Element parent=el.parent();
if(parent==null)
return;
if(ssb.length()>0 && ssb.charAt(ssb.length()-1)!='\n')
ssb.append('\n');
String markerText;
if("ol".equals(parent.nodeName())){
markerText=String.format("%d.", (parent.hasAttr("start") ? safeParseInt(parent.attr("start")) : 1)+el.elementSiblingIndex());
}else{
markerText="";
}
openSpans.add(new SpanInfo(new ListItemMarkerSpan(markerText), ssb.length(), el));
StringBuilder copyableText=new StringBuilder();
for(SpanInfo si:openSpans){
if(si.span instanceof ListItemMarkerSpan ims){
copyableText.append(ims.text);
}
}
copyableText.append(' ');
ssb.append(copyableText.toString(), new InvisibleSpan(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
case "blockquote" -> {
if(ssb.length()>0 && ssb.charAt(ssb.length()-1)!='\n')
ssb.append('\n');
openSpans.add(new SpanInfo(new BlockQuoteSpan(context, !isInsideBlockquote()), ssb.length(), el));
}
}
}
}
@@ -138,26 +219,45 @@ public class HtmlParser{
@Override
public void tail(@NonNull Node node, int depth){
if(node instanceof Element el){
if("span".equals(el.nodeName()) && el.hasClass("ellipsis")){
String name=el.nodeName();
lastElementWasBlock|=el.isBlock();
if("span".equals(name) && 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()){
}
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);
if(si.span!=null){
if(si.span instanceof MonospaceSpan){
ssb.append(" ", new SpacerSpan(V.dp(4), 0), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
ssb.setSpan(si.span, si.start, ssb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
openSpans.remove(openSpans.size()-1);
}
}
}
}
});
int trailingTrimLength=0;
for(int i=ssb.length()-1;i>=0 && Character.isWhitespace(ssb.charAt(i));i--){
trailingTrimLength++;
}
if(trailingTrimLength>0)
ssb.replace(ssb.length()-trailingTrimLength, ssb.length(), "");
if(!emojis.isEmpty())
parseCustomEmoji(ssb, emojis);
return ssb;
}
private static int safeParseInt(String s){
try{
return Integer.parseInt(s);
}catch(NumberFormatException x){
return 0;
}
}
public static void parseCustomEmoji(SpannableStringBuilder ssb, List<Emoji> emojis){
Map<String, Emoji> emojiByCode =
emojis.stream()

View File

@@ -0,0 +1,34 @@
package org.joinmastodon.android.ui.text;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.Layout;
import android.text.Spanned;
import android.text.style.LeadingMarginSpan;
import me.grishka.appkit.utils.V;
public class ListItemMarkerSpan implements LeadingMarginSpan{
public String text;
public ListItemMarkerSpan(String text){
this.text=text;
}
@Override
public int getLeadingMargin(boolean first){
return V.dp(32);
}
@Override
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout){
if(text instanceof Spanned s && s.getSpanStart(this)==start){
int level=s.getSpans(start, end, LeadingMarginSpan.class).length-1;
if(dir<0){ // RTL
c.drawText(this.text, layout.getWidth()-V.dp(32*level)-p.measureText(this.text), baseline, p);
}else{
c.drawText(this.text, x+V.dp(32*level), baseline, p);
}
}
}
}

View File

@@ -0,0 +1,9 @@
package org.joinmastodon.android.ui.text;
import android.content.Context;
public class MonospaceSpan extends BaseMonospaceSpan{
public MonospaceSpan(Context context){
super(context);
}
}

View File

@@ -17,7 +17,12 @@ public class SpacerSpan extends ReplacementSpan{
@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm){
// TODO height
if(fm!=null && height>0){
fm.ascent=-height;
fm.descent=0;
fm.top=fm.ascent;
fm.bottom=0;
}
return width;
}

View File

@@ -58,6 +58,12 @@ public class BlurHashDecoder{
return composeBitmap(width, height, numCompX, numCompY, colors, useCache);
}
public static int decodeToSingleColor(String hash){
if(hash.length()<6)
return 0;
return decode83(hash, 2, 6) & 0xFFFFFF;
}
private static int decode83(String str, int from, int to){
int result=0;
for(int i=from;i<to;i++){

View File

@@ -1,7 +1,9 @@
package org.joinmastodon.android.ui.utils;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.UiModeManager;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ComponentName;
@@ -24,6 +26,8 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.os.ext.SdkExtensions;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
@@ -75,6 +79,7 @@ import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.SearchResults;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.ColorContrastMode;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.Snackbar;
import org.joinmastodon.android.ui.sheets.BlockAccountConfirmationSheet;
@@ -713,18 +718,54 @@ public class UiUtils{
item.setIcon(icon);
SpannableStringBuilder ssb=new SpannableStringBuilder(item.getTitle());
ssb.insert(0, " ");
ssb.setSpan(new SpacerSpan(V.dp(24), 1), 0, 1, 0);
ssb.append(" ", new SpacerSpan(V.dp(8), 1), 0);
ssb.setSpan(new SpacerSpan(V.dp(24), 0), 0, 1, 0);
ssb.append(" ", new SpacerSpan(V.dp(8), 0), 0);
item.setTitle(ssb);
}
}
public static void setUserPreferredTheme(Context context){
context.setTheme(switch(GlobalUserPreferences.theme){
case AUTO -> R.style.Theme_Mastodon_AutoLightDark;
case LIGHT -> R.style.Theme_Mastodon_Light;
case DARK -> R.style.Theme_Mastodon_Dark;
});
context.setTheme(getThemeForUserPreference(context, GlobalUserPreferences.theme));
}
public static int getThemeForUserPreference(Context context, GlobalUserPreferences.ThemePreference pref){
if(GlobalUserPreferences.useDynamicColors){
return switch(pref){
case AUTO -> switch(getColorContrastMode(context)){
case DEFAULT -> R.style.Theme_Mastodon_AutoLightDark;
case MEDIUM -> R.style.Theme_Mastodon_AutoLightDark_MediumContrast;
case HIGH -> R.style.Theme_Mastodon_AutoLightDark_HighContrast;
};
case LIGHT -> switch(getColorContrastMode(context)){
case DEFAULT -> R.style.Theme_Mastodon_Light;
case MEDIUM -> R.style.Theme_Mastodon_Light_MediumContrast;
case HIGH -> R.style.Theme_Mastodon_Light_HighContrast;
};
case DARK -> switch(getColorContrastMode(context)){
case DEFAULT -> R.style.Theme_Mastodon_Dark;
case MEDIUM -> R.style.Theme_Mastodon_Dark_MediumContrast;
case HIGH -> R.style.Theme_Mastodon_Dark_HighContrast;
};
};
}else{
return switch(pref){
case AUTO -> switch(getColorContrastMode(context)){
case DEFAULT -> R.style.Theme_Mastodon_AutoLightDark_Masterial;
case MEDIUM -> R.style.Theme_Mastodon_AutoLightDark_MediumContrast_Masterial;
case HIGH -> R.style.Theme_Mastodon_AutoLightDark_HighContrast_Masterial;
};
case LIGHT -> switch(getColorContrastMode(context)){
case DEFAULT -> R.style.Theme_Mastodon_Light_Masterial;
case MEDIUM -> R.style.Theme_Mastodon_Light_MediumContrast_Masterial;
case HIGH -> R.style.Theme_Mastodon_Light_HighContrast_Masterial;
};
case DARK -> switch(getColorContrastMode(context)){
case DEFAULT -> R.style.Theme_Mastodon_Dark_Masterial;
case MEDIUM -> R.style.Theme_Mastodon_Dark_MediumContrast_Masterial;
case HIGH -> R.style.Theme_Mastodon_Dark_HighContrast_Masterial;
};
};
}
}
public static boolean isDarkTheme(){
@@ -1086,4 +1127,20 @@ public class UiUtils{
rv.scrollBy(0, -topItemOffset);
}
}
public static ColorContrastMode getColorContrastMode(Context context){
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
return ColorContrastMode.DEFAULT;
return ColorContrastMode.fromContrastValue(context.getSystemService(UiModeManager.class).getContrast());
}
@TargetApi(Build.VERSION_CODES.R)
public static boolean playVibrationEffectIfSupported(Context context, int effect){
Vibrator vibrator=context.getSystemService(Vibrator.class);
if(vibrator.areAllPrimitivesSupported(effect)){
vibrator.vibrate(VibrationEffect.startComposition().addPrimitive(effect).compose());
return true;
}
return false;
}
}

View File

@@ -244,7 +244,7 @@ public class ComposeAutocompleteViewController{
if(mode!=Mode.USERS)
return;
List<AccountViewModel> oldList=users;
users=result.accounts.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList());
users=result.accounts.stream().map(a->new AccountViewModel(a, accountID, activity)).collect(Collectors.toList());
if(isLoading){
isLoading=false;
if(users.size()>=LOADING_FAKE_USER_COUNT){

View File

@@ -9,12 +9,10 @@ import android.graphics.RectF;
import android.os.Build;
import android.os.Bundle;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Checkable;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
@@ -350,11 +348,7 @@ public class ComposePollViewController{
pollOptionsView.removeView(view);
addPollOptionBtn.setEnabled(pollOptions.size()<maxPollOptions);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.R){
Vibrator vibrator=fragment.getActivity().getSystemService(Vibrator.class);
if(vibrator.areAllPrimitivesSupported(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE)){
VibrationEffect effect=VibrationEffect.startComposition().addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE).compose();
vibrator.vibrate(effect);
}
UiUtils.playVibrationEffectIfSupported(fragment.getActivity(), VibrationEffect.Composition.PRIMITIVE_QUICK_RISE);
}
return;
}

View File

@@ -93,11 +93,9 @@ public class LinkCardHolder<T extends LinkCardHolder.LinkCardProvider> extends S
authorChip.setVisibility(View.VISIBLE);
authorBefore.setVisibility(View.VISIBLE);
String[] authorParts=itemView.getContext().getString(R.string.article_by_author, "{author}").split("\\{author\\}");
String before=authorParts[0].trim();
String before=authorParts.length>0 ? authorParts[0].trim() : "";
String after=authorParts.length>1 ? authorParts[1].trim() : "";
if(!TextUtils.isEmpty(before)){
authorBefore.setText(before);
}
authorBefore.setText(before);
if(TextUtils.isEmpty(after)){
authorAfter.setVisibility(View.GONE);
}else{

View File

@@ -24,6 +24,10 @@ public class CheckIconSelectableTextView extends TextView{
super(context, attrs, defStyle);
}
protected int getCheckmarkColorAttribute(){
return R.attr.colorM3OnSurface;
}
@Override
protected void drawableStateChanged(){
super.drawableStateChanged();
@@ -32,7 +36,7 @@ public class CheckIconSelectableTextView extends TextView{
currentlySelected=isSelected();
Drawable start=currentlySelected ? getResources().getDrawable(R.drawable.ic_baseline_check_18, getContext().getTheme()).mutate() : null;
if(start!=null)
start.setTint(UiUtils.getThemeColor(getContext(), R.attr.colorM3OnSurface));
start.setTintList(getTextColors());
Drawable end=getCompoundDrawablesRelative()[2];
setCompoundDrawablesRelativeWithIntrinsicBounds(start, null, end, null);
}

View File

@@ -35,6 +35,10 @@ public class FilterChipView extends CheckIconSelectableTextView{
updatePadding();
}
protected int getCheckmarkColorAttribute(){
return R.attr.colorM3OnSecondaryContainer;
}
private void updatePadding(){
int vertical=V.dp(6);
Drawable[] drawables=getCompoundDrawablesRelative();

View File

@@ -4,6 +4,10 @@ import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.CornerPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.text.Layout;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.util.AttributeSet;
@@ -14,14 +18,22 @@ import android.view.MenuItem;
import android.view.MotionEvent;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.text.ClickableLinksDelegate;
import org.joinmastodon.android.ui.text.CodeBlockSpan;
import org.joinmastodon.android.ui.text.DeleteWhenCopiedSpan;
import org.joinmastodon.android.ui.text.MonospaceSpan;
import org.joinmastodon.android.ui.utils.UiUtils;
import me.grishka.appkit.utils.V;
public class LinkedTextView extends TextView{
private ClickableLinksDelegate delegate=new ClickableLinksDelegate(this);
private boolean needInvalidate;
private ActionMode currentActionMode;
private Paint bgPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
private Path tmpPath=new Path();
public LinkedTextView(Context context){
this(context, null);
@@ -56,6 +68,8 @@ public class LinkedTextView extends TextView{
currentActionMode=null;
}
});
bgPaint.setColor(UiUtils.getThemeColor(context, R.attr.colorRichTextContainer));
bgPaint.setPathEffect(new CornerPathEffect(V.dp(2)));
}
public boolean onTouchEvent(MotionEvent ev){
@@ -64,6 +78,22 @@ public class LinkedTextView extends TextView{
}
public void onDraw(Canvas c){
if(getText() instanceof Spanned spanned){
c.save();
c.translate(getTotalPaddingLeft(), getTotalPaddingTop());
Layout layout=getLayout();
MonospaceSpan[] monospaceSpans=spanned.getSpans(0, spanned.length(), MonospaceSpan.class);
for(MonospaceSpan span:monospaceSpans){
layout.getSelectionPath(spanned.getSpanStart(span), spanned.getSpanEnd(span), tmpPath);
c.drawPath(tmpPath, bgPaint);
}
CodeBlockSpan[] blockSpans=spanned.getSpans(0, spanned.length(), CodeBlockSpan.class);
for(CodeBlockSpan span:blockSpans){
c.drawRoundRect(V.dp(-4), layout.getLineTop(layout.getLineForOffset(spanned.getSpanStart(span)))-V.dp(8), layout.getWidth()+V.dp(4),
layout.getLineBottom(layout.getLineForOffset(spanned.getSpanEnd(span)))+V.dp(4), V.dp(2), V.dp(2), bgPaint);
}
c.restore();
}
super.onDraw(c);
delegate.onDraw(c);
if(needInvalidate)

View File

@@ -51,6 +51,7 @@ public class NewPostsButtonContainer extends FrameLayout{
@Override
public boolean onInterceptTouchEvent(MotionEvent ev){
getParent().requestDisallowInterceptTouchEvent(true);
if(gestureDetector.onTouchEvent(ev))
return true;
return super.onInterceptTouchEvent(ev);

View File

@@ -0,0 +1,72 @@
package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Shader;
import android.graphics.Typeface;
import android.text.Layout;
import android.util.AttributeSet;
import android.widget.TextView;
import org.joinmastodon.android.R;
import me.grishka.appkit.utils.CustomViewHelper;
public class PhotoViewerAltTextView extends TextView implements CustomViewHelper{
private String moreText;
private Paint morePaint=new Paint(), clearPaint=new Paint();
private Matrix matrix=new Matrix();
private LinearGradient gradient, rtlGradient;
public PhotoViewerAltTextView(Context context){
this(context, null);
}
public PhotoViewerAltTextView(Context context, AttributeSet attrs){
this(context, attrs, 0);
}
public PhotoViewerAltTextView(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
moreText=context.getString(R.string.text_show_more).toUpperCase();
clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
gradient=new LinearGradient(0, 0, dp(56), 0, 0x00ffffff, 0xffffffff, Shader.TileMode.CLAMP);
rtlGradient=new LinearGradient(0, 0, dp(56), 0, 0xffffffff, 0x00ffffff, Shader.TileMode.CLAMP);
setLayerType(LAYER_TYPE_HARDWARE, null);
}
@Override
protected void onDraw(Canvas canvas){
super.onDraw(canvas);
Layout layout=getLayout();
if(layout.getLineCount()>=getMaxLines() && layout.getEllipsisCount(layout.getLineCount()-1)>0){
int lastLine=layout.getLineCount()-1;
morePaint.set(getPaint());
morePaint.setTypeface(Typeface.DEFAULT_BOLD);
float moreWidth=morePaint.measureText(moreText);
int lineTop=layout.getLineTop(lastLine);
int lineBottom=layout.getLineBottom(lastLine);
int viewRight=getWidth()-getPaddingRight();
int gradientWidth=dp(56);
if(layout.getParagraphDirection(lastLine)==Layout.DIR_RIGHT_TO_LEFT){
matrix.setTranslate(getPaddingLeft()+moreWidth, lineTop);
rtlGradient.setLocalMatrix(matrix);
clearPaint.setShader(rtlGradient);
canvas.drawRect(getPaddingLeft(), lineTop, getPaddingLeft()+moreWidth+gradientWidth, lineBottom, clearPaint);
canvas.drawText(moreText, getPaddingLeft(), layout.getLineBaseline(lastLine), morePaint);
}else{
matrix.setTranslate(viewRight-moreWidth-gradientWidth, lineTop);
gradient.setLocalMatrix(matrix);
clearPaint.setShader(gradient);
canvas.drawRect(viewRight-moreWidth-gradientWidth, lineTop, viewRight, lineBottom, clearPaint);
canvas.drawText(moreText, viewRight-moreWidth, layout.getLineBaseline(lastLine), morePaint);
}
}
}
}

View File

@@ -0,0 +1,44 @@
package org.joinmastodon.android.ui.views;
import android.content.Context;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.WindowInsets;
import android.widget.FrameLayout;
public class WindowRootFrameLayout extends FrameLayout{
private OnKeyListener dispatchKeyEventListener;
private OnApplyWindowInsetsListener dispatchApplyWindowInsetsListener;
public WindowRootFrameLayout(Context context){
this(context, null);
}
public WindowRootFrameLayout(Context context, AttributeSet attrs){
this(context, attrs, 0);
}
public WindowRootFrameLayout(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
}
@Override
public boolean dispatchKeyEvent(KeyEvent event){
return (dispatchKeyEventListener!=null && dispatchKeyEventListener.onKey(this, event.getKeyCode(), event)) || super.dispatchKeyEvent(event);
}
public void setDispatchKeyEventListener(OnKeyListener dispatchKeyEventListener){
this.dispatchKeyEventListener=dispatchKeyEventListener;
}
@Override
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets){
if(dispatchApplyWindowInsetsListener!=null)
return dispatchApplyWindowInsetsListener.onApplyWindowInsets(this, insets);
return super.dispatchApplyWindowInsets(insets);
}
public void setDispatchApplyWindowInsetsListener(OnApplyWindowInsetsListener dispatchApplyWindowInsetsListener){
this.dispatchApplyWindowInsetsListener=dispatchApplyWindowInsetsListener;
}
}

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorM3Surface" android:alpha="0.95"/>
<item android:color="@android:color/system_accent3_700" android:lStar="25"/>
</selector>

View File

@@ -1,4 +1,4 @@
<?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_check_20px"/>
<item android:color="@android:color/system_accent3_800" android:lStar="15"/>
</selector>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorM3PrimaryContainer" android:alpha="?composePollStyleInactiveAlpha"/>
</selector>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?colorM3Outline" android:alpha="@dimen/overlay_ripple_alpha"/>
</selector>

View File

@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android" android:drawable="@drawable/bg_button_m3_filled" android:inset="4dp">
</inset>

View File

@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<inset xmlns:android="http://schemas.android.com/apk/res/android" android:drawable="@drawable/bg_button_m3_tonal" android:inset="4dp">
</inset>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:width="24dp" android:height="24dp" android:gravity="start|center_vertical" android:start="6dp">
<ripple android:color="@color/m3_white_overlay"/>
</item>
</layer-list>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:width="24dp" android:height="24dp" android:gravity="center">
<ripple android:color="@color/m3_white_overlay"/>
</item>
</layer-list>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:type="linear"
android:startColor="#00000000"
android:endColor="#E6000000"
android:angle="270"/>
</shape>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="@color/m3_white_overlay">
<item android:gravity="center" android:width="32dp" android:height="32dp">
<shape android:shape="oval">
<solid android:color="#80000000"/>
</shape>
</item>
</ripple>

View File

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

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="@color/m3_on_surface_overlay">
<item android:id="@android:id/mask">
<shape>
<solid android:color="#000"/>
</shape>
</item>
</ripple>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:width="24dp" android:height="24dp" android:gravity="start|center_vertical" android:start="6dp">
<ripple android:color="@color/m3_outline_overlay"/>
</item>
</layer-list>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:width="24dp" android:height="24dp" android:gravity="center">
<ripple android:color="@color/m3_outline_overlay"/>
</item>
</layer-list>

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:start="16dp">
<shape>
<solid android:color="?colorM3OutlineVariant"/>
<size android:height="0.5dp"/>
</shape>
</item>
</layer-list>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M11,20V7.825L5.4,13.425L4,12L12,4L20,12L18.6,13.425L13,7.825V20Z"/>
</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="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M240,816L240,216Q240,186.3 261.15,165.15Q282.3,144 312,144L648,144Q677.7,144 698.85,165.15Q720,186.3 720,216L720,816L480,720L240,816ZM312,709L480,642L648,709L648,216Q648,216 648,216Q648,216 648,216L312,216Q312,216 312,216Q312,216 312,216L312,709ZM312,216L312,216Q312,216 312,216Q312,216 312,216L648,216Q648,216 648,216Q648,216 648,216L648,216L480,216L312,216Z"/>
</vector>

View File

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

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M240,816L240,216Q240,186 261,165Q282,144 312,144L648,144Q678,144 699,165Q720,186 720,216L720,816L480,720L240,816Z"/>
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M5,21V5Q5,4.175 5.588,3.587Q6.175,3 7,3H17Q17.825,3 18.413,3.587Q19,4.175 19,5V21L12,18Z"/>
</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="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M291,720L240,669L429,480L240,291L291,240L480,429L669,240L720,291L531,480L720,669L669,720L480,531L291,720Z"/>
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M6,20Q5.175,20 4.588,19.413Q4,18.825 4,18V15H6V18Q6,18 6,18Q6,18 6,18H18Q18,18 18,18Q18,18 18,18V15H20V18Q20,18.825 19.413,19.413Q18.825,20 18,20ZM12,16 L7,11 8.4,9.55 11,12.15V4H13V12.15L15.6,9.55L17,11Z"/>
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M11,17H13V11H11ZM12,9Q12.425,9 12.713,8.712Q13,8.425 13,8Q13,7.575 12.713,7.287Q12.425,7 12,7Q11.575,7 11.288,7.287Q11,7.575 11,8Q11,8.425 11.288,8.712Q11.575,9 12,9ZM12,22Q9.925,22 8.1,21.212Q6.275,20.425 4.925,19.075Q3.575,17.725 2.788,15.9Q2,14.075 2,12Q2,9.925 2.788,8.1Q3.575,6.275 4.925,4.925Q6.275,3.575 8.1,2.787Q9.925,2 12,2Q14.075,2 15.9,2.787Q17.725,3.575 19.075,4.925Q20.425,6.275 21.212,8.1Q22,9.925 22,12Q22,14.075 21.212,15.9Q20.425,17.725 19.075,19.075Q17.725,20.425 15.9,21.212Q14.075,22 12,22Z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M480,880Q398,880 325,848.5Q252,817 197.5,762.5Q143,708 111.5,635Q80,562 80,480Q80,397 112.5,324Q145,251 200.5,197Q256,143 330,111.5Q404,80 488,80Q568,80 639,107.5Q710,135 763.5,183.5Q817,232 848.5,298.5Q880,365 880,442Q880,557 810,618.5Q740,680 640,680L566,680Q557,680 553.5,685Q550,690 550,696Q550,708 565,730.5Q580,753 580,782Q580,832 552.5,856Q525,880 480,880ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480ZM260,520Q286,520 303,503Q320,486 320,460Q320,434 303,417Q286,400 260,400Q234,400 217,417Q200,434 200,460Q200,486 217,503Q234,520 260,520ZM380,360Q406,360 423,343Q440,326 440,300Q440,274 423,257Q406,240 380,240Q354,240 337,257Q320,274 320,300Q320,326 337,343Q354,360 380,360ZM580,360Q606,360 623,343Q640,326 640,300Q640,274 623,257Q606,240 580,240Q554,240 537,257Q520,274 520,300Q520,326 537,343Q554,360 580,360ZM700,520Q726,520 743,503Q760,486 760,460Q760,434 743,417Q726,400 700,400Q674,400 657,417Q640,434 640,460Q640,486 657,503Q674,520 700,520ZM480,800Q489,800 494.5,795Q500,790 500,782Q500,768 485,749Q470,730 470,692Q470,650 499,625Q528,600 570,600L640,600Q706,600 753,561.5Q800,523 800,442Q800,321 707.5,240.5Q615,160 488,160Q352,160 256,253Q160,346 160,480Q160,613 253.5,706.5Q347,800 480,800Z"/>
</vector>

View File

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

View File

@@ -33,7 +33,7 @@
<layer-list>
<item>
<shape android:shape="oval">
<solid android:color="?colorM3PrimaryContainer"/>
<solid android:color="@color/bg_compose_poll_style_inactive"/>
</shape>
</item>
<item>

View File

@@ -40,7 +40,7 @@
<layer-list>
<item>
<shape android:shape="oval">
<solid android:color="?colorM3PrimaryContainer"/>
<solid android:color="@color/bg_compose_poll_style_inactive"/>
</shape>
</item>
<item>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="20dp"
android:viewportWidth="24"
android:viewportHeight="20">
<path
android:fillColor="#000"
android:pathData="M23.933,2.824C22.324,4.079 21.073,5.357 20.179,6.657C19.33,7.912 18.905,9.1 18.905,10.221C19.084,10.131 19.307,10.086 19.575,10.086C19.888,10.041 20.156,10.019 20.38,10.019C21.408,10.019 22.257,10.445 22.927,11.297C23.642,12.103 24,13.112 24,14.322C24,15.802 23.508,17.035 22.525,18.021C21.542,19.007 20.313,19.5 18.838,19.5C17.274,19.5 16.045,18.94 15.151,17.819C14.257,16.653 13.81,15.107 13.81,13.179C13.81,10.893 14.503,8.629 15.888,6.388C17.274,4.147 19.307,2.017 21.989,0L23.933,2.824ZM10.123,2.824C8.514,4.079 7.263,5.357 6.369,6.657C5.52,7.912 5.095,9.1 5.095,10.221C5.274,10.131 5.497,10.086 5.765,10.086C6.078,10.041 6.346,10.019 6.57,10.019C7.598,10.019 8.447,10.445 9.117,11.297C9.832,12.103 10.19,13.112 10.19,14.322C10.19,15.802 9.698,17.035 8.715,18.021C7.732,19.007 6.503,19.5 5.028,19.5C3.464,19.5 2.235,18.94 1.341,17.819C0.447,16.653 0,15.107 0,13.179C0,10.893 0.693,8.629 2.078,6.388C3.464,4.147 5.497,2.017 8.179,0L10.123,2.824Z"/>
</vector>

View File

@@ -1,27 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:gravity="center_vertical">
<item android:gravity="center_vertical" android:left="-4dp" android:right="-4dp">
<shape>
<solid android:color="#69ffffff"/>
<corners android:radius="1dp"/>
<size android:height="2dp"/>
<solid android:color="#40ffffff"/>
<corners android:radius="16dp"/>
<size android:height="8dp"/>
</shape>
</item>
<item android:gravity="center_vertical" android:id="@android:id/secondaryProgress">
<clip>
<shape>
<solid android:color="#40ffffff"/>
<corners android:radius="1dp"/>
<size android:height="2dp"/>
</shape>
</clip>
<item android:gravity="center_vertical|end" android:width="4dp" android:height="4dp" android:end="-2dp">
<shape android:shape="oval">
<solid android:color="#fff"/>
</shape>
</item>
<item android:gravity="center_vertical" android:id="@android:id/progress">
<item android:gravity="center_vertical" android:id="@android:id/progress" android:left="-4dp" android:right="-4dp">
<clip>
<shape>
<solid android:color="#fff"/>
<corners android:radius="1dp"/>
<size android:height="2dp"/>
<corners android:radius="16dp"/>
<size android:height="8dp"/>
</shape>
</clip>
</item>

Some files were not shown because too many files have changed in this diff Show More