Compare commits
322 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
357041b995 | ||
|
|
6754f004f2 | ||
|
|
3487b0b70d | ||
|
|
f91b887586 | ||
|
|
62cd754f3a | ||
|
|
be82274bee | ||
|
|
6779b5cc43 | ||
|
|
86e369201a | ||
|
|
6b2fd26961 | ||
|
|
423718b4f2 | ||
|
|
f6ed37f80f | ||
|
|
71a986b280 | ||
|
|
12375609e3 | ||
|
|
02659407f8 | ||
|
|
a4f0e577f5 | ||
|
|
9bc4d81321 | ||
|
|
72dd2e1b65 | ||
|
|
58e74708f5 | ||
|
|
77855154d3 | ||
|
|
d77fcff253 | ||
|
|
5358c889e4 | ||
|
|
bcbf3412a7 | ||
|
|
17a481e797 | ||
|
|
dce8d38237 | ||
|
|
1cf250765b | ||
|
|
d136060b6f | ||
|
|
6dd7d779dd | ||
|
|
aca2843991 | ||
|
|
04f46cbecc | ||
|
|
9fbca3a7d6 | ||
|
|
a6a71ce8b6 | ||
|
|
31d8a85e35 | ||
|
|
63a3069041 | ||
|
|
96750bb80a | ||
|
|
e53df3d1b1 | ||
|
|
8a5a681b86 | ||
|
|
06d2df4773 | ||
|
|
723bdb9fed | ||
|
|
774601bc6c | ||
|
|
909fbcb54a | ||
|
|
1dd422918a | ||
|
|
7c433ccea9 | ||
|
|
56602a88e1 | ||
|
|
dd838689c5 | ||
|
|
8b54f2960b | ||
|
|
782f828073 | ||
|
|
32c0a3985e | ||
|
|
6bfee114db | ||
|
|
fb0bbe3b11 | ||
|
|
64d0adc3ff | ||
|
|
942c9998ba | ||
|
|
864d6fb7a5 | ||
|
|
9286de4580 | ||
|
|
6e4590caf2 | ||
|
|
ea1216b352 | ||
|
|
c2cd886844 | ||
|
|
83e5a041a3 | ||
|
|
a10bbab6d2 | ||
|
|
ed36774f16 | ||
|
|
6663c1c2be | ||
|
|
6d448fbb5c | ||
|
|
25a49971bb | ||
|
|
3300660321 | ||
|
|
39cf4b6678 | ||
|
|
0edf1a5653 | ||
|
|
52473e69cc | ||
|
|
0981ef5d68 | ||
|
|
4ed29cde81 | ||
|
|
25c05dcc51 | ||
|
|
afab3a04ca | ||
|
|
8a6e00d2bb | ||
|
|
678dc12595 | ||
|
|
7390550cd2 | ||
|
|
e4d193eb6c | ||
|
|
ebe19a4393 | ||
|
|
fe0ae9a425 | ||
|
|
9b82c8b066 | ||
|
|
0a1df389e7 | ||
|
|
1735a3ba9a | ||
|
|
7ba6d37ebf | ||
|
|
7d59ca27fc | ||
|
|
323ede05a9 | ||
|
|
2c2dcf3c28 | ||
|
|
1f5b1ba3f9 | ||
|
|
ec81a53e88 | ||
|
|
324ef1cce9 | ||
|
|
f4cae9c51f | ||
|
|
0e92626755 | ||
|
|
0d6658ca72 | ||
|
|
ac812704f0 | ||
|
|
5dbcf68aca | ||
|
|
6a2395a3c5 | ||
|
|
be020bd742 | ||
|
|
1e19fbe4f9 | ||
|
|
b18fecf9d8 | ||
|
|
3cde5435c0 | ||
|
|
b988a455e3 | ||
|
|
8c3ebaf784 | ||
|
|
a381d338ef | ||
|
|
c04733411a | ||
|
|
9218cb728e | ||
|
|
e00bb3f6e9 | ||
|
|
6727ec5119 | ||
|
|
4d7a6d9476 | ||
|
|
858cc34298 | ||
|
|
5ac37290df | ||
|
|
efd9690d10 | ||
|
|
2985c5490b | ||
|
|
f70101279d | ||
|
|
9fe52e7853 | ||
|
|
f8059b2810 | ||
|
|
372add3cf8 | ||
|
|
134bd13d60 | ||
|
|
a53959d707 | ||
|
|
2a6d87a513 | ||
|
|
b2a9ce998d | ||
|
|
7c63943814 | ||
|
|
4b5f84d781 | ||
|
|
5c75ada632 | ||
|
|
964b84ec79 | ||
|
|
55db777906 | ||
|
|
16b8724045 | ||
|
|
87c425b89c | ||
|
|
20ed47032e | ||
|
|
91d65b4e27 | ||
|
|
5a0ed9b7c1 | ||
|
|
b62963c4f3 | ||
|
|
f0a00f6919 | ||
|
|
8ea049c956 | ||
|
|
ff5e281db7 | ||
|
|
65189753eb | ||
|
|
0944682bef | ||
|
|
aa41b9bef2 | ||
|
|
668286215b | ||
|
|
bf1d709892 | ||
|
|
4ae49029d0 | ||
|
|
90a1860a1c | ||
|
|
f3e8a0c6c9 | ||
|
|
661a54f006 | ||
|
|
9f9e3f8eba | ||
|
|
6a9fb76677 | ||
|
|
c81288e694 | ||
|
|
594f044068 | ||
|
|
147485633c | ||
|
|
4498a0f226 | ||
|
|
1df56fc320 | ||
|
|
1e2f47dd2a | ||
|
|
26f5fbf3b8 | ||
|
|
ecd2cecfa6 | ||
|
|
f6ecd25e1d | ||
|
|
1ac088f5a3 | ||
|
|
0030a06cd3 | ||
|
|
6b7d1b0981 | ||
|
|
f1140a0197 | ||
|
|
179534e931 | ||
|
|
be2db34586 | ||
|
|
50b81c3b98 | ||
|
|
31ae0b6e08 | ||
|
|
36fdecd22e | ||
|
|
d33f1d59ca | ||
|
|
b6a978f376 | ||
|
|
a1da9be14f | ||
|
|
692ebed8fb | ||
|
|
000294d2fa | ||
|
|
6406d25938 | ||
|
|
9c95d5f6e5 | ||
|
|
c15ef07d53 | ||
|
|
7182647574 | ||
|
|
2a08b27667 | ||
|
|
56a2510564 | ||
|
|
ddaab49976 | ||
|
|
508ea32701 | ||
|
|
1b17600835 | ||
|
|
c60d06950f | ||
|
|
bbdc3d4038 | ||
|
|
5e2a292aeb | ||
|
|
1f6c48edf8 | ||
|
|
a72acf7d32 | ||
|
|
162bc86ebe | ||
|
|
af8f042f10 | ||
|
|
7aae8b03af | ||
|
|
b28c095226 | ||
|
|
9432fc9b8c | ||
|
|
37df47c7cd | ||
|
|
62602839db | ||
|
|
52c56db1ce | ||
|
|
cdc3b37ee4 | ||
|
|
46bd36b65d | ||
|
|
70eb5bf68c | ||
|
|
c2f6b16aff | ||
|
|
603c058ec9 | ||
|
|
3582d7bdad | ||
|
|
d988e1aecf | ||
|
|
7bf322d48a | ||
|
|
5c4a450ef0 | ||
|
|
c370fab1b4 | ||
|
|
c79cba96ec | ||
|
|
ca4aed3dc2 | ||
|
|
c20237d32c | ||
|
|
6082a0bcd8 | ||
|
|
3de494f9e9 | ||
|
|
1b6c299251 | ||
|
|
01ae5b915d | ||
|
|
d0ca465194 | ||
|
|
07564f2964 | ||
|
|
eb45b59cac | ||
|
|
6e4c4c86f6 | ||
|
|
b52dd603a1 | ||
|
|
573e13f39f | ||
|
|
5848dc0e67 | ||
|
|
7ae5546113 | ||
|
|
a126a078b4 | ||
|
|
808dab6f50 | ||
|
|
70dc5aece0 | ||
|
|
80f76d0f05 | ||
|
|
d2a96af886 | ||
|
|
6b6e720ca5 | ||
|
|
9eacb7b067 | ||
|
|
36cce87ffc | ||
|
|
b028c3ad38 | ||
|
|
eada060f57 | ||
|
|
53cfbcb5b0 | ||
|
|
23c624f575 | ||
|
|
669f3a50c8 | ||
|
|
fa3f4f6eda | ||
|
|
d97ffc32aa | ||
|
|
1aecf4021f | ||
|
|
f3d76a26f7 | ||
|
|
e5dc62db6f | ||
|
|
6008368045 | ||
|
|
83af61a758 | ||
|
|
eefcc34277 | ||
|
|
4a65976eea | ||
|
|
cb8aea258a | ||
|
|
e5297b023d | ||
|
|
21c6f41013 | ||
|
|
19f8d908c7 | ||
|
|
fae7f73f7a | ||
|
|
41da8cc1d7 | ||
|
|
39d5a32494 | ||
|
|
c885b5a85e | ||
|
|
76f2b63171 | ||
|
|
58e35d8da3 | ||
|
|
a5e03357df | ||
|
|
5a7ab6be70 | ||
|
|
e977b46392 | ||
|
|
66de2f4b87 | ||
|
|
1a48277cf2 | ||
|
|
6a55bd2248 | ||
|
|
3a82395428 | ||
|
|
a98b93feec | ||
|
|
0cff2658f3 | ||
|
|
d3d95d49de | ||
|
|
71ca9b6f3d | ||
|
|
bf9c3d4d8d | ||
|
|
4b304629b7 | ||
|
|
c4314f100e | ||
|
|
14c250446c | ||
|
|
8bc1c8e79c | ||
|
|
5e6781817d | ||
|
|
5b5e4fbbd2 | ||
|
|
3dcc6d0013 | ||
|
|
2ad50cd972 | ||
|
|
ced5fe4ee0 | ||
|
|
db4afd4c8f | ||
|
|
a0d3bd83f2 | ||
|
|
2092a6b8fe | ||
|
|
ad04433944 | ||
|
|
57da77b642 | ||
|
|
f80e3771d1 | ||
|
|
6a18d8ef03 | ||
|
|
a312018441 | ||
|
|
3026bd5c51 | ||
|
|
5afde48052 | ||
|
|
14209dc785 | ||
|
|
f1b30f251c | ||
|
|
6a849d654f | ||
|
|
2f9c5fe210 | ||
|
|
43f096420f | ||
|
|
5b848ed1ca | ||
|
|
9e1cf330d7 | ||
|
|
1ad2d08e27 | ||
|
|
42658add38 | ||
|
|
b211789847 | ||
|
|
9c88183366 | ||
|
|
c76dba3a8c | ||
|
|
29bee87f2a | ||
|
|
c139f85b99 | ||
|
|
3247d4f2f5 | ||
|
|
77b2f98f17 | ||
|
|
82c6c8076a | ||
|
|
4177faa553 | ||
|
|
92ec125661 | ||
|
|
513a57663b | ||
|
|
20e7f716f1 | ||
|
|
71f92cb66c | ||
|
|
77b2abd0cb | ||
|
|
15385dd924 | ||
|
|
08847ec641 | ||
|
|
805fc5d8c7 | ||
|
|
3d7a95d336 | ||
|
|
c1869386ff | ||
|
|
7a728c52cf | ||
|
|
22f3aad538 | ||
|
|
42da6dcf48 | ||
|
|
c0f18b1f61 | ||
|
|
492d851d89 | ||
|
|
853f9dc8e4 | ||
|
|
0e2ae4d3c0 | ||
|
|
3ea1412faa | ||
|
|
e83f0749ee | ||
|
|
343d958677 | ||
|
|
2f32378978 | ||
|
|
cfc5683f75 | ||
|
|
823e2f6ac6 | ||
|
|
5f5fcdde46 | ||
|
|
cf74e252ce | ||
|
|
010ca587d2 | ||
|
|
2957ac813f | ||
|
|
5aa117e2e3 | ||
|
|
f1df4e72d2 | ||
|
|
afee257211 |
@@ -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
|
||||
)
|
||||
|
||||
7
fastlane/metadata/android/en-US/changelogs/125.txt
Normal file
7
fastlane/metadata/android/en-US/changelogs/125.txt
Normal 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
|
||||
6
fastlane/metadata/android/en-US/changelogs/128.txt
Normal file
6
fastlane/metadata/android/en-US/changelogs/128.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
- Settings are now split into global settings and account-specific settings for each account you're logged into
|
||||
- Profiles now show who of the people you follow also follows this person
|
||||
- More easily swap between posts, profiles and hashtags when using search
|
||||
- We now tell you exactly how many followers you'd lose when blocking a whole server so you can avoid costly mistakes
|
||||
- Bookmarks and favourites have moved to a new tab on your profile
|
||||
- The button that loads missing posts is now more upfront about which direction it's going to load in
|
||||
@@ -1 +1 @@
|
||||
116.txt
|
||||
128.txt
|
||||
@@ -1,6 +1,6 @@
|
||||
ماستودون بهترین راه برای پیگیری اتفاقات است. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
|
||||
|
||||
این کاره رسمی اندروید برای ماستودون است. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
|
||||
این کاره رسمی اندروید برای ماستودون است. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. در کاره ما، شما میتوانید:
|
||||
|
||||
گشت و گذار
|
||||
|
||||
@@ -32,7 +32,7 @@ CURATE
|
||||
|
||||
یک سکوی انتشار قدرتمند
|
||||
|
||||
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, they’ll see it.
|
||||
You no longer have to try and appease an opaque algorithm that decides if your friends are going to see what you posted. اگر آنها شما را پی بگیرند، آن را خواهند دید.
|
||||
|
||||
If you publish it to the open web, it’s accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
|
||||
|
||||
@@ -44,7 +44,7 @@ Between threads, polls, high quality images, videos, audio, and content warnings
|
||||
|
||||
به لطف خوراک زمانی خانه، تشخیص اینکه چه زمانی از همه بهروزرسانیها مطلع شدهاید و میتوانید به چیز دیگری بروید، آسان است.
|
||||
|
||||
No need to worry that a misclick will ruin your recommendations forever. ما حدس نمیزنیم که چه چیزی میخواهید ببینید، به شما اجازه میدهیم آن را کنترل کنید.
|
||||
لازم نیست نگران باشید که یک کلیک اشتباه توصیه های شما را برای همیشه خراب می کند. ما حدس نمیزنیم که چه چیزی میخواهید ببینید، به شما اجازه میدهیم آن را کنترل کنید.
|
||||
|
||||
شیوهنامهها، بدون سکوها
|
||||
|
||||
|
||||
@@ -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 what’s 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 don’t 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, they’ll see it.
|
||||
|
||||
|
||||
@@ -34,28 +34,28 @@ 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 don’t need to show you ads, so we don’t 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, it’s easy to tell when you’ve caught up on all updates and can move on to something else.
|
||||
Þökk sé heimastreymi í tímaröð, þá er auðvelt að sjá þegar þú lýkur við að skoða allar nýjar færslur og getur snúið þér að einhverju öðru.
|
||||
|
||||
No need to worry that a misclick will ruin your recommendations forever. Við eru ekkert að giska á hvað þú viljir sjá, við látum þér eftir að stýra því.
|
||||
Þú þarft ekkert að hafa áhyggjur að einn smellur á rangan stað skemmi orðspor þitt að eilífu. Við eru ekkert að giska á hvað þú viljir sjá, við látum þér eftir að stýra því.
|
||||
|
||||
SAMSKIPTAMÁTAR, EKKI KERFI
|
||||
|
||||
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
|
||||
Mastodon er ekki hefðbundinn samfélagsmiðill, heldur byggist í kringum samskiptamáta sem ekki er með miðlæga stýringu. Þú getur skráð þig á opinbera netþjóninum okkar eða valið utanaðkomandi þjón til að hýsa gögnin þín og hafa umsjón með umhverfinu þínu.
|
||||
|
||||
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But there’s more: With just one account, you can communicate with people from other fediverse platforms.
|
||||
Þökk sé sameiginlegum samskiptamáta, þá skiptir ekki máli hvaða netþjón þú velur; þú átt að eiga hindranalaus samskipti við fólk á öðrum Mastodon-þjónum. En það er meira til: Með einum notandaaðgangi geturðu átt í samskiptum við önnut tengd kerfi sem tilheyra fedi-heiminum.
|
||||
|
||||
Ekki ánægð/ur með valið þitt? Þú getur alltaf skipt yfir á annan Mastodon-þjón og tekið fylgjendurna þína með þér. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
|
||||
Ekki ánægð/ur með valið þitt? Þú getur alltaf skipt yfir á annan Mastodon-þjón og tekið fylgjendurna þína með þér. Vanir tölvulæsir notendur geta meira að segja hýst sitt eigið kerfi, enda er Mastodon opinn hugbúnaður.
|
||||
|
||||
ÁN HAGNAÐARMARKMIÐA INN AÐ BEINI
|
||||
|
||||
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 what’s best for the platform.
|
||||
Mastodon er skráð sem samtök án hagnaðarmarkmiða í BNA og Þýskalandi. Við höfum engan hvata til að ná peningum út úr kerfinu, heldur liggur áhuginn í því sem er best fyrir kerfið.
|
||||
|
||||
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.
|
||||
@@ -1,16 +1,16 @@
|
||||
Mastodon is the best way to keep up with what’s happening. Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.
|
||||
Olan biteni takip etmenin en iyi yolu. Herhangi birini federe ağında takip edin ve her şeyi kronolojik sırayla görün. Görünürde algoritma, reklam veya tıklama tuzağı yok.
|
||||
|
||||
This is the official Android app for Mastodon. It is blazing fast and stunningly beautiful, designed to be not just powerful but also easy to use. In our app, you can:
|
||||
Bu, aşağıdakiler için resmi uygulamasıdır. Sadece güçlü değil aynı zamanda kullanımı kolay olacak şekilde tasarlanmış, son derece hızlı ve şaşırtıcı derecede güzel. Uygulamamızda şunları yapabilirsiniz:
|
||||
|
||||
EXPLORE
|
||||
KEŞFET
|
||||
|
||||
■ Discover new writers, journalists, artists, photographers, scientists and more
|
||||
■ See what’s happening in the world
|
||||
■ Yeni yazarlar, gazeteciler, sanatçılar, fotoğrafçılar, bilim insanları ve daha fazlasını keşfedin
|
||||
■ Dünyada neler olup bittiğini görün
|
||||
|
||||
READ
|
||||
OKU
|
||||
|
||||
■ 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
|
||||
■ Önem verdiğiniz kişileri kesintisiz bir kronolojik akışta takip edin
|
||||
■ Belirli konuları gerçek zamanlı olarak takip etmek için etiketleri takip edin
|
||||
|
||||
CREATE
|
||||
|
||||
@@ -25,37 +25,37 @@ CURATE
|
||||
AND MORE!
|
||||
|
||||
■ 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
|
||||
■ Profillerini başkalarıyla hızlıca paylaşmak için QR kodlarını paylaşın ve tarayın
|
||||
■ Giriş yapın ve birden fazla hesap arasında geçiş yapın
|
||||
■ Zil butonu ile belirli bir kişi paylaşım yaptığında bildirim alın
|
||||
■ İpucu yok! Yayınlarınızı içerik uyarılarının arkasına koyabilirsiniz
|
||||
|
||||
A POWERFUL PUBLISHING PLATFORM
|
||||
GÜÇLÜ BIR YAYINCILIK PLATFORMU
|
||||
|
||||
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, they’ll see it.
|
||||
Artık arkadaşlarınızın paylaştıklarını görüp görmeyeceğine karar veren şeffaf olmayan bir algoritmayı yatıştırmaya çalışmak zorunda değilsiniz. Sizi takip ederlerse görürler.
|
||||
|
||||
If you publish it to the open web, it’s accessible on the open web. You can safely share links to Mastodon in the knowledge that anyone will be able to read them without logging in.
|
||||
Eğer bunu açık olarak yayınlarsanız, herkese açık olarak erişilebilir olur. Herkesin giriş yapmadan okuyabileceğinden emin olarak bağlantılarını güvenle paylaşabilirsiniz.
|
||||
|
||||
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.
|
||||
Başlıklar, anketler, yüksek kaliteli görüntüler, videolar, sesler ve içerik uyarıları arasında, kendinizi size uygun bir şekilde ifade etmenin birçok yolunu sunar.
|
||||
|
||||
A POWERFUL READING PLATFORM
|
||||
GÜÇLÜ BİR OKUMA PLATFORMU
|
||||
|
||||
We don’t need to show you ads, so we don’t 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.
|
||||
Size reklam göstermemize gerek yok, bu yüzden sizi uygulamamızda tutmamıza da gerek yok. Sosyal ağımız, size en uygun deneyimi seçebileceğiniz en zengin 3. parti uygulama ve entegrasyon seçeneklerine sahiptir.
|
||||
|
||||
Thanks to the chronological home feed, it’s easy to tell when you’ve caught up on all updates and can move on to something else.
|
||||
Kronolojik ana sayfa akışı sayesinde, tüm güncellemeleri ne zaman yakaladığınızı ve başka bir şeye geçebileceğinizi anlamak kolaydır.
|
||||
|
||||
No need to worry that a misclick will ruin your recommendations forever. We don’t guess what you want to see, we let you control it.
|
||||
Yanlış bir tıklamanın önerilerinizi sonsuza dek mahvedeceğinden endişelenmenize gerek yok. Ne görmek istediğinizi tahmin etmiyoruz, sizin kontrol etmenize izin veriyoruz.
|
||||
|
||||
PROTOCOLS, NOT PLATFORMS
|
||||
PLATFORMLAR DEĞİL, PROTOKOLLER
|
||||
|
||||
Mastodon is not like a traditional social media platform, but is built on a decentralized protocol. You can sign up on our official server, or choose a 3rd party to host your data and moderate your experience.
|
||||
Sosyal ağımız geleneksel bir sosyal medya platformu gibi değil, merkezi olmayan bir protokol üzerine inşa edilmiştir. Resmi sunucumuza kayıt olabilir veya verilerinizi barındırmak ve deneyiminizi denetlemek için 3. bir taraf seçebilirsiniz.
|
||||
|
||||
Thanks to the common protocol, no matter what you choose, you can communicate seamlessly with people on other Mastodon servers. But there’s more: With just one account, you can communicate with people from other fediverse platforms.
|
||||
Ortak protokol sayesinde, neyi seçerseniz seçin, diğer sunucularındaki kişilerle sorunsuz bir şekilde iletişim kurabilirsiniz. Ama dahası da var: Sadece tek bir hesapla, diğer federe platformlarındaki kişilerle iletişim kurabilirsiniz.
|
||||
|
||||
Not happy with your choice? You can always switch to a different Mastodon server while taking your followers with you. For advanced users, you can even host your data on your own infrastructure, since Mastodon is open-source.
|
||||
Seçimden memnun değil misiniz? Takipçilerini yanınızda götürürken her zaman farklı bir sunucusuna geçebilirsiniz. İleri düzey kullanıcılar için, Açık kaynaklı olduğundan verilerinizi kendi altyapınızda bile barındırabilir.
|
||||
|
||||
NON-PROFIT IN NATURE
|
||||
KÂR AMACI GÜTMEYEN NİTELİKTE
|
||||
|
||||
Mastodon is a registered non-profit in the US and Germany. We are not motivated by extracting monetary value from the platform, but by what’s best for the platform.
|
||||
Mastıdon,ABD ve Almanya'da kâr amacı gütmeyen kayıtlı bir kuruluştur. Platformdan parasal değer elde etmek için değil, platform için en iyi olanı yapmak için motive oluyoruz.
|
||||
|
||||
AS FEATURED IN: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, and more.
|
||||
YER ALDIĞI GİBİ: TIME, Forbes, Wired, The Guardian, CNN, The Verge, TechCrunch, Financial Times, Gizmodo, PCMAG.com, ve dahası.
|
||||
@@ -1 +1 @@
|
||||
Where conversations happen
|
||||
Konuşmaların gerçekleştiği yer
|
||||
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "org.joinmastodon.android"
|
||||
minSdk 23
|
||||
targetSdk 34
|
||||
versionCode 119
|
||||
versionName "2.7.0"
|
||||
versionCode 128
|
||||
versionName "2.9.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.0'
|
||||
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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.joinmastodon.android;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
|
||||
@@ -11,6 +12,11 @@ public class GlobalUserPreferences{
|
||||
public static boolean useCustomTabs;
|
||||
public static boolean altTextReminders, confirmUnfollow, confirmBoost, confirmDeletePost;
|
||||
public static ThemePreference theme=ThemePreference.AUTO;
|
||||
public static boolean useDynamicColors;
|
||||
public static boolean showInteractionCounts;
|
||||
public static boolean customEmojiInNames;
|
||||
public static boolean showCWs;
|
||||
public static boolean hideSensitiveMedia;
|
||||
|
||||
private static SharedPreferences getPrefs(){
|
||||
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
|
||||
@@ -29,6 +35,24 @@ 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);
|
||||
showInteractionCounts=prefs.getBoolean("interactionCounts", true);
|
||||
customEmojiInNames=prefs.getBoolean("emojiInNames", true);
|
||||
showCWs=prefs.getBoolean("showCWs", true);
|
||||
hideSensitiveMedia=prefs.getBoolean("hideSensitive", true);
|
||||
if(!prefs.getBoolean("perAccountMigrationDone", false)){
|
||||
AccountSession account=AccountSessionManager.getInstance().getLastActiveAccount();
|
||||
if(account!=null){
|
||||
SharedPreferences accPrefs=account.getRawLocalPreferences();
|
||||
showInteractionCounts=accPrefs.getBoolean("interactionCounts", true);
|
||||
customEmojiInNames=accPrefs.getBoolean("emojiInNames", true);
|
||||
showCWs=accPrefs.getBoolean("showCWs", true);
|
||||
hideSensitiveMedia=accPrefs.getBoolean("hideSensitive", true);
|
||||
save();
|
||||
}
|
||||
// Also applies to new app installs
|
||||
prefs.edit().putBoolean("perAccountMigrationDone", true).apply();
|
||||
}
|
||||
}
|
||||
|
||||
public static void save(){
|
||||
@@ -40,6 +64,11 @@ public class GlobalUserPreferences{
|
||||
.putBoolean("confirmUnfollow", confirmUnfollow)
|
||||
.putBoolean("confirmBoost", confirmBoost)
|
||||
.putBoolean("confirmDeletePost", confirmDeletePost)
|
||||
.putBoolean("useDynamicColors", useDynamicColors)
|
||||
.putBoolean("interactionCounts", showInteractionCounts)
|
||||
.putBoolean("emojiInNames", customEmojiInNames)
|
||||
.putBoolean("showCWs", showCWs)
|
||||
.putBoolean("hideSensitive", hideSensitiveMedia)
|
||||
.apply();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,6 +130,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
Notification.Builder builder;
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
|
||||
boolean hasGroup=false;
|
||||
int version=AccountSessionManager.get(accountID).getRawLocalPreferences().getInt("notificationChannelsVersion", 1);
|
||||
List<NotificationChannelGroup> channelGroups=nm.getNotificationChannelGroups();
|
||||
for(NotificationChannelGroup group:channelGroups){
|
||||
if(group.getId().equals(accountID)){
|
||||
@@ -137,7 +138,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!hasGroup){
|
||||
if(!hasGroup || version!=2){
|
||||
NotificationChannelGroup group=new NotificationChannelGroup(accountID, accountName);
|
||||
nm.createNotificationChannelGroup(group);
|
||||
List<NotificationChannel> channels=Arrays.stream(PushNotification.Type.values())
|
||||
@@ -150,6 +151,7 @@ public class PushNotificationReceiver extends BroadcastReceiver{
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
nm.createNotificationChannels(channels);
|
||||
AccountSessionManager.get(accountID).getRawLocalPreferences().edit().putInt("notificationChannelsVersion", 2).apply();
|
||||
}
|
||||
builder=new Notification.Builder(context, accountID+"_"+pn.notificationType);
|
||||
}else{
|
||||
|
||||
@@ -148,7 +148,7 @@ public class CacheController{
|
||||
.map(ng->{
|
||||
NotificationViewModel nvm=new NotificationViewModel();
|
||||
nvm.notification=ng;
|
||||
nvm.accounts=ng.sampleAccountIds.stream().map(accounts::get).collect(Collectors.toList());
|
||||
nvm.accounts=ng.sampleAccountIds.stream().map(accounts::get).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
if(nvm.accounts.size()!=ng.sampleAccountIds.size())
|
||||
return null;
|
||||
if(ng.statusId!=null){
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.FamiliarFollowers;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public class GetAccountFamiliarFollowers extends MastodonAPIRequest<List<FamiliarFollowers>>{
|
||||
public GetAccountFamiliarFollowers(Collection<String> ids){
|
||||
super(HttpMethod.GET, "/accounts/familiar_followers", new TypeToken<>(){});
|
||||
for(String id:ids){
|
||||
addQueryParameter("id[]", id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
|
||||
public class GetDomainBlockPreview extends MastodonAPIRequest<GetDomainBlockPreview.Response>{
|
||||
public GetDomainBlockPreview(String domain){
|
||||
super(HttpMethod.GET, "/domain_blocks/preview", Response.class);
|
||||
addQueryParameter("domain", domain);
|
||||
}
|
||||
|
||||
public static class Response{
|
||||
public int followingCount;
|
||||
public int followersCount;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -5,18 +5,10 @@ import android.content.SharedPreferences;
|
||||
public class AccountLocalPreferences{
|
||||
private final SharedPreferences prefs;
|
||||
|
||||
public boolean showInteractionCounts;
|
||||
public boolean customEmojiInNames;
|
||||
public boolean showCWs;
|
||||
public boolean hideSensitiveMedia;
|
||||
public boolean serverSideFiltersSupported;
|
||||
|
||||
public AccountLocalPreferences(SharedPreferences prefs){
|
||||
this.prefs=prefs;
|
||||
showInteractionCounts=prefs.getBoolean("interactionCounts", true);
|
||||
customEmojiInNames=prefs.getBoolean("emojiInNames", true);
|
||||
showCWs=prefs.getBoolean("showCWs", true);
|
||||
hideSensitiveMedia=prefs.getBoolean("hideSensitive", true);
|
||||
serverSideFiltersSupported=prefs.getBoolean("serverSideFilters", false);
|
||||
}
|
||||
|
||||
@@ -30,10 +22,6 @@ public class AccountLocalPreferences{
|
||||
|
||||
public void save(){
|
||||
prefs.edit()
|
||||
.putBoolean("interactionCounts", showInteractionCounts)
|
||||
.putBoolean("emojiInNames", customEmojiInNames)
|
||||
.putBoolean("showCWs", showCWs)
|
||||
.putBoolean("hideSensitive", hideSensitiveMedia)
|
||||
.putBoolean("serverSideFilters", serverSideFiltersSupported)
|
||||
.apply();
|
||||
}
|
||||
|
||||
@@ -119,9 +119,11 @@ public class AccountSession{
|
||||
activated=(flags & FLAG_ACTIVATED)==FLAG_ACTIVATED;
|
||||
needUpdatePushSettings=(flags & FLAG_NEED_UPDATE_PUSH_SETTINGS)==FLAG_NEED_UPDATE_PUSH_SETTINGS;
|
||||
JsonObject pushKeys=JsonParser.parseString(values.getAsString("push_keys")).getAsJsonObject();
|
||||
pushAuthKey=pushKeys.get("auth").getAsString();
|
||||
pushPrivateKey=pushKeys.get("private").getAsString();
|
||||
pushPublicKey=pushKeys.get("public").getAsString();
|
||||
if(!pushKeys.get("auth").isJsonNull() && !pushKeys.get("private").isJsonNull() && !pushKeys.get("public").isJsonNull()){
|
||||
pushAuthKey=pushKeys.get("auth").getAsString();
|
||||
pushPrivateKey=pushKeys.get("private").getAsString();
|
||||
pushPublicKey=pushKeys.get("public").getAsString();
|
||||
}
|
||||
pushSubscription=MastodonAPIController.gson.fromJson(values.getAsString("push_subscription"), PushSubscription.class);
|
||||
JsonObject legacyFilters=JsonParser.parseString(values.getAsString("legacy_filters")).getAsJsonObject();
|
||||
wordFilters=MastodonAPIController.gson.fromJson(legacyFilters.getAsJsonArray("filters"), new TypeToken<List<LegacyFilter>>(){}.getType());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -380,21 +382,40 @@ public class AccountSessionManager{
|
||||
}
|
||||
|
||||
private void readInstanceInfo(SQLiteDatabase db, Set<String> domains){
|
||||
try(Cursor cursor=db.query("instances", null, "`domain` IN ("+String.join(", ", Collections.nCopies(domains.size(), "?"))+")", domains.toArray(new String[0]), null, null, null)){
|
||||
ContentValues values=new ContentValues();
|
||||
while(cursor.moveToNext()){
|
||||
DatabaseUtils.cursorRowToContentValues(cursor, values);
|
||||
String domain=values.getAsString("domain");
|
||||
int version=values.getAsInteger("version");
|
||||
Instance instance=MastodonAPIController.gson.fromJson(values.getAsString("instance_obj"), switch(version){
|
||||
case 1 -> InstanceV1.class;
|
||||
case 2 -> InstanceV2.class;
|
||||
default -> throw new IllegalStateException("Unexpected value: " + version);
|
||||
});
|
||||
List<Emoji> emojis=MastodonAPIController.gson.fromJson(values.getAsString("emojis"), new TypeToken<List<Emoji>>(){}.getType());
|
||||
instances.put(domain, instance);
|
||||
customEmojis.put(domain, groupCustomEmojis(emojis));
|
||||
instancesLastUpdated.put(domain, values.getAsLong("last_updated"));
|
||||
for(String domain : domains){
|
||||
final int maxEmojiLength=500000;
|
||||
try(Cursor cursor=db.rawQuery("SELECT domain, instance_obj, substring(emojis,0,?) AS emojis, length(emojis) AS emoji_length, last_updated, version FROM instances WHERE `domain` = ?",
|
||||
new String[]{String.valueOf(maxEmojiLength) , domain})) {
|
||||
ContentValues values=new ContentValues();
|
||||
while(cursor.moveToNext()){
|
||||
DatabaseUtils.cursorRowToContentValues(cursor, values);
|
||||
int version=values.getAsInteger("version");
|
||||
Instance instance=MastodonAPIController.gson.fromJson(values.getAsString("instance_obj"), switch(version){
|
||||
case 1 -> InstanceV1.class;
|
||||
case 2 -> InstanceV2.class;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+version);
|
||||
});
|
||||
StringBuilder emojiSB=new StringBuilder();
|
||||
emojiSB.append(values.getAsString("emojis"));
|
||||
//get emoji in chunks of 1MB if it didn't fit in the first query
|
||||
int emojiStringLength=values.getAsInteger("emoji_length");
|
||||
if(emojiStringLength>maxEmojiLength){
|
||||
final int pagesize=1000000;
|
||||
for(int start=maxEmojiLength; start<emojiStringLength; start+=pagesize){
|
||||
try(Cursor emojiCursor=db.rawQuery("SELECT substr(emojis,?, ?) FROM instances WHERE `domain` = ?", new String[]{String.valueOf(start), String.valueOf(pagesize), domain})){
|
||||
emojiCursor.moveToNext();
|
||||
emojiSB.append(emojiCursor.getString(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
List<Emoji> emojis=MastodonAPIController.gson.fromJson(emojiSB.toString(), new TypeToken<List<Emoji>>(){}.getType());
|
||||
instances.put(domain, instance);
|
||||
customEmojis.put(domain, groupCustomEmojis(emojis));
|
||||
instancesLastUpdated.put(domain, values.getAsLong("last_updated"));
|
||||
}
|
||||
}catch(Exception ex){
|
||||
Log.d(TAG, "readInstanceInfo failed", ex);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(!loadedInstances){
|
||||
@@ -581,6 +602,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);
|
||||
@@ -714,8 +741,7 @@ public class AccountSessionManager{
|
||||
File file=new File(MastodonApp.context.getFilesDir(), "instance_"+domain.replace('.', '_')+".json");
|
||||
try(FileInputStream in=new FileInputStream(file)){
|
||||
JsonObject jobj=JsonParser.parseReader(new InputStreamReader(in, StandardCharsets.UTF_8)).getAsJsonObject();
|
||||
|
||||
insertInstanceIntoDatabase(db, domain, MastodonAPIController.gson.fromJson(jobj.get("instance"), Instance.class),
|
||||
insertInstanceIntoDatabase(db, domain, MastodonAPIController.gson.fromJson(jobj.get(jobj.has("instance") ? "instance" : "a"), Instance.class),
|
||||
MastodonAPIController.gson.fromJson(jobj.get("emojis"), new TypeToken<>(){}.getType()), jobj.get("last_updated").getAsLong());
|
||||
}catch(Exception x){
|
||||
Log.w(TAG, "Error reading instance info file for "+domain, x);
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.assist.AssistContent;
|
||||
|
||||
public interface AssistContentProviderFragment{
|
||||
void onProvideAssistContent(AssistContent content);
|
||||
}
|
||||
@@ -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
|
||||
@@ -500,7 +500,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
int itemCount=spoilerItem.contentItems.size();
|
||||
displayItems.addAll(index+1, spoilerItem.contentItems);
|
||||
if(spoilerItem.spoilerType==Status.SpoilerType.FILTER && spoilerItem.contentItems.get(0) instanceof SpoilerStatusDisplayItem nestedSpoiler
|
||||
&& nestedSpoiler.spoilerType==Status.SpoilerType.CONTENT_WARNING && !AccountSessionManager.get(accountID).getLocalPreferences().showCWs){
|
||||
&& nestedSpoiler.spoilerType==Status.SpoilerType.CONTENT_WARNING && !GlobalUserPreferences.showCWs){
|
||||
status.revealedSpoilers.add(Status.SpoilerType.CONTENT_WARNING);
|
||||
displayItems.addAll(index+1+itemCount, nestedSpoiler.contentItems);
|
||||
itemCount+=nestedSpoiler.contentItems.size();
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetBookmarkedStatuses;
|
||||
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class BookmarkedStatusListFragment extends StatusListFragment{
|
||||
private String nextMaxID;
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
setTitle(R.string.bookmarks);
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetBookmarkedStatuses(offset==0 ? null : nextMaxID, count)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Status> result){
|
||||
if(result.nextPageUri!=null)
|
||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||
else
|
||||
nextMaxID=null;
|
||||
onDataLoaded(result, nextMaxID!=null);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetFavoritedStatuses;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class FavoritedStatusListFragment extends StatusListFragment{
|
||||
private String nextMaxID;
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
setTitle(R.string.your_favorites);
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetFavoritedStatuses(offset==0 ? null : nextMaxID, count)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Status> result){
|
||||
if(result.nextPageUri!=null)
|
||||
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
|
||||
else
|
||||
nextMaxID=null;
|
||||
onDataLoaded(result, nextMaxID!=null);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,54 @@ 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;
|
||||
holder.text.setText(gap.enteredFromTop ? R.string.load_missing_posts_above : R.string.load_missing_posts_below);
|
||||
}
|
||||
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 +353,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 +369,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 +457,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 +483,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 +517,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 +530,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 +548,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 +566,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 +756,7 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
|
||||
private void onNewPostsBtnClick(View v){
|
||||
if(newPostsBtnShown){
|
||||
hideNewPostsButton();
|
||||
scrollToTop();
|
||||
smoothScrollRecyclerViewToTop(list);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -723,6 +863,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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -165,7 +165,10 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
|
||||
for(int i=0;i<parent.getChildCount();i++){
|
||||
View child=parent.getChildAt(i);
|
||||
if(parent.getChildViewHolder(child) instanceof StatusDisplayItem.Holder<?> holder){
|
||||
String itemID=getNotificationByID(holder.getItemID()).notification.pageMaxId;
|
||||
NotificationViewModel n=getNotificationByID(holder.getItemID());
|
||||
if(n==null)
|
||||
continue;
|
||||
String itemID=n.notification.pageMaxId;
|
||||
if(ObjectIdComparator.INSTANCE.compare(itemID, unreadMarker)>0){
|
||||
parent.getDecoratedBoundsWithMargins(child, tmpRect);
|
||||
c.drawRect(tmpRect, paint);
|
||||
@@ -241,7 +244,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 +260,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 +293,7 @@ public class NotificationsListFragment extends BaseNotificationsListFragment{
|
||||
return;
|
||||
for(NotificationViewModel n:items){
|
||||
if(ObjectIdComparator.INSTANCE.compare(n.notification.pageMinId, realUnreadMarker)<=0){
|
||||
markAsRead();
|
||||
markAsRead(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -49,23 +51,28 @@ import android.widget.Toolbar;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountByID;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountFamiliarFollowers;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
|
||||
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
||||
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.account_list.FamiliarFollowerListFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.FollowerListFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.FollowingListFragment;
|
||||
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.AccountField;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.FamiliarFollowers;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
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;
|
||||
@@ -89,6 +96,8 @@ import java.time.format.FormatStyle;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -106,7 +115,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;
|
||||
|
||||
@@ -120,6 +129,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
|
||||
private ProfileFeaturedFragment featuredFragment;
|
||||
private AccountTimelineFragment timelineFragment;
|
||||
private ProfileAboutFragment aboutFragment;
|
||||
private SavedPostsTimelineFragment savedFragment;
|
||||
private TabLayout tabbar;
|
||||
private SwipeRefreshLayout refreshLayout;
|
||||
private View followersBtn, followingBtn;
|
||||
@@ -136,12 +146,16 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
|
||||
private ImageButton qrCodeButton;
|
||||
private ProgressBar innerProgress;
|
||||
private View actions;
|
||||
private View familiarFollowersRow;
|
||||
private ImageView[] familiarFollowersAvatars;
|
||||
private TextView familiarFollowersLabel;
|
||||
|
||||
private Account account;
|
||||
private String accountID;
|
||||
private Relationship relationship;
|
||||
private boolean isOwnProfile;
|
||||
private ArrayList<AccountField> fields=new ArrayList<>();
|
||||
private List<Account> familiarFollowers=List.of();
|
||||
|
||||
private boolean isInEditMode, editDirty;
|
||||
private Uri editNewAvatar, editNewCover;
|
||||
@@ -222,6 +236,13 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
|
||||
qrCodeButton=content.findViewById(R.id.qr_code);
|
||||
innerProgress=content.findViewById(R.id.profile_progress);
|
||||
actions=content.findViewById(R.id.profile_actions);
|
||||
familiarFollowersRow=content.findViewById(R.id.familiar_followers);
|
||||
familiarFollowersAvatars=new ImageView[]{
|
||||
content.findViewById(R.id.familiar_followers_ava1),
|
||||
content.findViewById(R.id.familiar_followers_ava2),
|
||||
content.findViewById(R.id.familiar_followers_ava3),
|
||||
};
|
||||
familiarFollowersLabel=content.findViewById(R.id.familiar_followers_label);
|
||||
|
||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(24));
|
||||
avatar.setClipToOutline(true);
|
||||
@@ -234,13 +255,14 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
|
||||
}
|
||||
};
|
||||
|
||||
tabViews=new FrameLayout[3];
|
||||
tabViews=new FrameLayout[4];
|
||||
for(int i=0;i<tabViews.length;i++){
|
||||
FrameLayout tabView=new FrameLayout(getActivity());
|
||||
tabView.setId(switch(i){
|
||||
case 0 -> R.id.profile_featured;
|
||||
case 1 -> R.id.profile_timeline;
|
||||
case 2 -> R.id.profile_about;
|
||||
case 3 -> R.id.profile_saved;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+i);
|
||||
});
|
||||
tabView.setVisibility(View.GONE);
|
||||
@@ -248,7 +270,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
|
||||
tabViews[i]=tabView;
|
||||
}
|
||||
|
||||
pager.setOffscreenPageLimit(4);
|
||||
pager.setOffscreenPageLimit(10);
|
||||
pager.setAdapter(new ProfilePagerAdapter());
|
||||
pager.getLayoutParams().height=getResources().getDisplayMetrics().heightPixels;
|
||||
|
||||
@@ -262,6 +284,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
|
||||
case 0 -> R.string.profile_featured;
|
||||
case 1 -> R.string.profile_timeline;
|
||||
case 2 -> R.string.profile_about;
|
||||
case 3 -> R.string.profile_saved_posts;
|
||||
default -> throw new IllegalStateException();
|
||||
}));
|
||||
tabbar.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener(){
|
||||
@@ -290,11 +313,13 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
|
||||
cover.setOnClickListener(this::onCoverClick);
|
||||
refreshLayout.setOnRefreshListener(this);
|
||||
fab.setOnClickListener(this::onFabClick);
|
||||
familiarFollowersRow.setOnClickListener(this::onFamiliarFollowersClick);
|
||||
|
||||
if(savedInstanceState!=null){
|
||||
featuredFragment=(ProfileFeaturedFragment) getChildFragmentManager().getFragment(savedInstanceState, "featured");
|
||||
timelineFragment=(AccountTimelineFragment) getChildFragmentManager().getFragment(savedInstanceState, "timeline");
|
||||
aboutFragment=(ProfileAboutFragment) getChildFragmentManager().getFragment(savedInstanceState, "about");
|
||||
savedFragment=(SavedPostsTimelineFragment) getChildFragmentManager().getFragment(savedInstanceState, "saved");
|
||||
}
|
||||
|
||||
if(loaded){
|
||||
@@ -349,6 +374,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
|
||||
qf.setArguments(args);
|
||||
qf.show(getChildFragmentManager(), "qrDialog");
|
||||
});
|
||||
familiarFollowersRow.setVisibility(View.GONE);
|
||||
|
||||
return sizeWrapper;
|
||||
}
|
||||
@@ -376,6 +402,8 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
|
||||
timelineFragment.onRefresh();
|
||||
if(featuredFragment.loaded)
|
||||
featuredFragment.onRefresh();
|
||||
if(savedFragment!=null && savedFragment.loaded)
|
||||
savedFragment.onRefresh();
|
||||
}
|
||||
V.setVisibilityAnimated(fab, View.VISIBLE);
|
||||
}
|
||||
@@ -411,16 +439,21 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
|
||||
aboutFragment=new ProfileAboutFragment();
|
||||
aboutFragment.setFields(fields);
|
||||
}
|
||||
pager.getAdapter().notifyDataSetChanged();
|
||||
pager.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
pager.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
pager.setCurrentItem(1, false);
|
||||
tabbar.selectTab(tabbar.getTabAt(1));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if(savedFragment==null && isOwnProfile){
|
||||
savedFragment=SavedPostsTimelineFragment.newInstance(accountID, account, false);
|
||||
}
|
||||
if(!refreshing){
|
||||
pager.getAdapter().notifyDataSetChanged();
|
||||
pager.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
|
||||
@Override
|
||||
public boolean onPreDraw(){
|
||||
pager.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
pager.setCurrentItem(1, false);
|
||||
tabbar.selectTab(tabbar.getTabAt(1));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
super.dataLoaded();
|
||||
}
|
||||
|
||||
@@ -486,6 +519,8 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
|
||||
getChildFragmentManager().putFragment(outState, "timeline", timelineFragment);
|
||||
if(aboutFragment.isAdded())
|
||||
getChildFragmentManager().putFragment(outState, "about", aboutFragment);
|
||||
if(savedFragment!=null && savedFragment.isAdded())
|
||||
getChildFragmentManager().putFragment(outState, "saved", savedFragment);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -514,6 +549,8 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
|
||||
if(timelineFragment!=null && timelineFragment.isAdded() && childInsets!=null){
|
||||
timelineFragment.onApplyWindowInsets(childInsets);
|
||||
featuredFragment.onApplyWindowInsets(childInsets);
|
||||
if(savedFragment!=null)
|
||||
savedFragment.onApplyWindowInsets(childInsets);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -554,10 +591,10 @@ 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)
|
||||
if(GlobalUserPreferences.customEmojiInNames)
|
||||
HtmlParser.parseCustomEmoji(ssb, account.emojis);
|
||||
name.setText(ssb);
|
||||
setTitle(ssb);
|
||||
@@ -580,7 +617,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 +652,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);
|
||||
@@ -642,6 +679,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
|
||||
if(onScrollListener!=null){
|
||||
onScrollListener.setViews(getToolbar());
|
||||
}
|
||||
getToolbar().setTranslationZ(tabBarIsAtTop ? 0 : V.dp(3));
|
||||
}
|
||||
|
||||
private CharSequence makeRedString(CharSequence s){
|
||||
@@ -729,14 +767,6 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, false)
|
||||
.exec(accountID);
|
||||
}else if(id==R.id.bookmarks){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), BookmarkedStatusListFragment.class, args);
|
||||
}else if(id==R.id.favorites){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), FavoritedStatusListFragment.class, args);
|
||||
}else if(id==R.id.save){
|
||||
if(isInEditMode)
|
||||
saveAndExitEditMode();
|
||||
@@ -763,6 +793,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;
|
||||
}
|
||||
@@ -784,6 +817,25 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
new GetAccountFamiliarFollowers(Set.of(account.id))
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<FamiliarFollowers> result){
|
||||
for(FamiliarFollowers ff:result){
|
||||
if(ff.id.equals(account.id)){
|
||||
familiarFollowers=ff.accounts;
|
||||
updateFamiliarFollowers();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void updateRelationship(){
|
||||
@@ -794,6 +846,38 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
|
||||
followsYouView.setVisibility(relationship.followedBy ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private void updateFamiliarFollowers(){
|
||||
if(!familiarFollowers.isEmpty()){
|
||||
familiarFollowersRow.setVisibility(View.VISIBLE);
|
||||
List<AccountViewModel> followers=familiarFollowers.stream().limit(3).map(a->new AccountViewModel(a, accountID, false, getActivity())).collect(Collectors.toList());
|
||||
String template=switch(familiarFollowers.size()){
|
||||
case 1 -> getString(R.string.familiar_followers_one, "{first}");
|
||||
case 2 -> getString(R.string.familiar_followers_two, "{first}", "{second}");
|
||||
default -> getResources().getQuantityString(R.plurals.familiar_followers_many, familiarFollowers.size()-2, "{first}", "{second}", familiarFollowers.size()-2);
|
||||
};
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder(template);
|
||||
if(familiarFollowers.size()>1){
|
||||
int index=template.indexOf("{second}");
|
||||
ssb.replace(index, index+8, followers.get(1).parsedName);
|
||||
template=template.replace("{second}", "#".repeat(followers.get(1).parsedName.length()));
|
||||
}
|
||||
int index=template.indexOf("{first}");
|
||||
ssb.replace(index, index+7, followers.get(0).parsedName);
|
||||
familiarFollowersLabel.setText(ssb);
|
||||
UiUtils.loadCustomEmojiInTextView(familiarFollowersLabel);
|
||||
if(familiarFollowers.size()<3)
|
||||
familiarFollowersAvatars[2].setVisibility(View.GONE);
|
||||
if(familiarFollowers.size()<2)
|
||||
familiarFollowersAvatars[1].setVisibility(View.GONE);
|
||||
|
||||
int i=0;
|
||||
for(AccountViewModel avm:followers){
|
||||
ViewImageLoader.loadWithoutAnimation(familiarFollowersAvatars[i], getResources().getDrawable(R.drawable.image_placeholder, getActivity().getTheme()), avm.avaRequest);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onScrollChanged(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY){
|
||||
if(scrollY>cover.getHeight()){
|
||||
cover.setTranslationY(scrollY-(cover.getHeight()));
|
||||
@@ -847,6 +931,9 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
|
||||
editSaveMenuItem.setVisible(!buttonInView);
|
||||
}
|
||||
}
|
||||
if((scrollY==0 && oldScrollY!=0) || (scrollY!=0 && oldScrollY==0)){
|
||||
refreshLayout.setEnabled(scrollY==0);
|
||||
}
|
||||
}
|
||||
|
||||
private Fragment getFragmentForPage(int page){
|
||||
@@ -854,6 +941,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
|
||||
case 0 -> featuredFragment;
|
||||
case 1 -> timelineFragment;
|
||||
case 2 -> aboutFragment;
|
||||
case 3 -> savedFragment;
|
||||
default -> throw new IllegalStateException();
|
||||
};
|
||||
}
|
||||
@@ -917,7 +1005,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
|
||||
pager.setUserInputEnabled(false);
|
||||
actionButton.setText(R.string.save_changes);
|
||||
pager.setCurrentItem(2);
|
||||
for(int i=0;i<3;i++){
|
||||
for(int i=0;i<4;i++){
|
||||
tabbar.getTabAt(i).view.setEnabled(false);
|
||||
}
|
||||
Drawable overlay=getResources().getDrawable(R.drawable.edit_avatar_overlay).mutate();
|
||||
@@ -994,7 +1082,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
|
||||
|
||||
invalidateOptionsMenu();
|
||||
actionButton.setText(R.string.edit_profile);
|
||||
for(int i=0;i<3;i++){
|
||||
for(int i=0;i<4;i++){
|
||||
tabbar.getTabAt(i).view.setEnabled(true);
|
||||
}
|
||||
pager.setUserInputEnabled(true);
|
||||
@@ -1134,7 +1222,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 +1236,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)));
|
||||
}
|
||||
}
|
||||
@@ -1162,6 +1250,14 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
|
||||
Nav.go(getActivity(), ComposeFragment.class, args);
|
||||
}
|
||||
|
||||
private void onFamiliarFollowersClick(View v){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("targetAccount", Parcels.wrap(account));
|
||||
args.putInt("count", familiarFollowers.size());
|
||||
Nav.go(getActivity(), FamiliarFollowerListFragment.class, args);
|
||||
}
|
||||
|
||||
private void startImagePicker(int requestCode){
|
||||
Intent intent=UiUtils.getMediaPickerIntent(new String[]{"image/*"}, 1);
|
||||
startActivityForResult(intent, requestCode);
|
||||
@@ -1171,9 +1267,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 +1309,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
|
||||
@@ -1241,7 +1351,7 @@ public class ProfileFragment extends LoaderFragment implements ScrollableToTop{
|
||||
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return loaded ? 3 : 0;
|
||||
return loaded ? (isOwnProfile ? 4 : 3) : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetBookmarkedStatuses;
|
||||
import org.joinmastodon.android.api.requests.statuses.GetFavoritedStatuses;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.drawables.EmptyDrawable;
|
||||
import org.joinmastodon.android.ui.views.FilterChipView;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class SavedPostsTimelineFragment extends StatusListFragment{
|
||||
private Account user;
|
||||
private Mode mode;
|
||||
private HorizontalScrollView filtersBar;
|
||||
private FilterChipView favoritesChip, bookmarksChip;
|
||||
|
||||
public SavedPostsTimelineFragment(){
|
||||
setListLayoutId(R.layout.recycler_fragment_no_refresh);
|
||||
}
|
||||
|
||||
public static SavedPostsTimelineFragment newInstance(String accountID, Account profileAccount, boolean load){
|
||||
SavedPostsTimelineFragment f=new SavedPostsTimelineFragment();
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(profileAccount));
|
||||
if(!load)
|
||||
args.putBoolean("noAutoLoad", true);
|
||||
args.putBoolean("__is_tab", true);
|
||||
f.setArguments(args);
|
||||
return f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
user=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
|
||||
mode=Mode.FAVORITES;
|
||||
super.onAttach(activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=(switch(mode){
|
||||
case FAVORITES -> new GetFavoritedStatuses(offset>0 ? getMaxID() : null, count);
|
||||
case BOOKMARKS -> new GetBookmarkedStatuses(offset>0 ? getMaxID() : null, count);
|
||||
}).setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Status> result){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
onDataLoaded(result, result.nextPageUri!=null);
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
view.setBackground(null); // prevents unnecessary overdraw
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
|
||||
loadData();
|
||||
}
|
||||
|
||||
protected void onStatusCreated(Status status){
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter getAdapter(){
|
||||
filtersBar=new HorizontalScrollView(getActivity());
|
||||
LinearLayout filtersLayout=new LinearLayout(getActivity());
|
||||
filtersBar.addView(filtersLayout);
|
||||
filtersLayout.setOrientation(LinearLayout.HORIZONTAL);
|
||||
filtersLayout.setPadding(V.dp(16), 0, V.dp(16), V.dp(8));
|
||||
filtersLayout.setDividerDrawable(new EmptyDrawable(V.dp(8), 1));
|
||||
filtersLayout.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
|
||||
|
||||
favoritesChip=new FilterChipView(getActivity());
|
||||
favoritesChip.setText(R.string.your_favorites);
|
||||
favoritesChip.setTag(Mode.FAVORITES);
|
||||
favoritesChip.setSelected(mode==Mode.FAVORITES);
|
||||
favoritesChip.setOnClickListener(this::onFilterClick);
|
||||
filtersLayout.addView(favoritesChip);
|
||||
|
||||
bookmarksChip=new FilterChipView(getActivity());
|
||||
bookmarksChip.setText(R.string.bookmarks);
|
||||
bookmarksChip.setTag(Mode.BOOKMARKS);
|
||||
bookmarksChip.setSelected(mode==Mode.BOOKMARKS);
|
||||
bookmarksChip.setOnClickListener(this::onFilterClick);
|
||||
filtersLayout.addView(bookmarksChip);
|
||||
|
||||
View banner=getActivity().getLayoutInflater().inflate(R.layout.discover_info_banner, list, false);
|
||||
TextView text=banner.findViewById(R.id.banner_text);
|
||||
text.setText(R.string.profile_saved_posts_explanation);
|
||||
ImageView icon=banner.findViewById(R.id.icon);
|
||||
icon.setImageResource(R.drawable.ic_lock_24px);
|
||||
|
||||
// Prevents margins messing up things
|
||||
FrameLayout bannerW=new FrameLayout(getActivity());
|
||||
bannerW.addView(banner);
|
||||
|
||||
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
|
||||
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(bannerW));
|
||||
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(filtersBar));
|
||||
mergeAdapter.addAdapter(super.getAdapter());
|
||||
return mergeAdapter;
|
||||
}
|
||||
|
||||
private FilterChipView getViewForMode(Mode mode){
|
||||
return switch(mode){
|
||||
case FAVORITES -> favoritesChip;
|
||||
case BOOKMARKS -> bookmarksChip;
|
||||
};
|
||||
}
|
||||
|
||||
private void onFilterClick(View v){
|
||||
Mode newMode=(Mode) v.getTag();
|
||||
if(newMode==mode)
|
||||
return;
|
||||
if(currentRequest!=null){
|
||||
currentRequest.cancel();
|
||||
currentRequest=null;
|
||||
}
|
||||
getViewForMode(mode).setSelected(false);
|
||||
mode=newMode;
|
||||
v.setSelected(true);
|
||||
data.clear();
|
||||
preloadedData.clear();
|
||||
int size=displayItems.size();
|
||||
displayItems.clear();
|
||||
adapter.notifyItemRangeRemoved(0, size);
|
||||
loaded=false;
|
||||
dataLoading=true;
|
||||
doLoadData();
|
||||
}
|
||||
|
||||
private enum Mode{
|
||||
FAVORITES,
|
||||
BOOKMARKS
|
||||
}
|
||||
}
|
||||
@@ -243,7 +243,7 @@ public class SplashFragment extends AppKitFragment{
|
||||
|
||||
@Override
|
||||
public boolean wantsLightNavigationBar(){
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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;
|
||||
@@ -66,7 +68,7 @@ public class ThreadFragment extends StatusListFragment{
|
||||
knownAccounts.put(inReplyToAccount.id, inReplyToAccount);
|
||||
data.add(mainStatus);
|
||||
onAppendItems(Collections.singletonList(mainStatus));
|
||||
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
|
||||
if(GlobalUserPreferences.customEmojiInNames)
|
||||
setTitle(HtmlParser.parseCustomEmoji(getString(R.string.post_from_user, mainStatus.account.displayName), mainStatus.account.emojis));
|
||||
else
|
||||
setTitle(getString(R.string.post_from_user, mainStatus.account.displayName));
|
||||
@@ -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){
|
||||
|
||||
@@ -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(){
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.joinmastodon.android.fragments.account_list;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountFamiliarFollowers;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.FamiliarFollowers;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class FamiliarFollowerListFragment extends BaseAccountListFragment{
|
||||
protected Account account;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
|
||||
setTitle("@"+account.acct);
|
||||
int count=getArguments().getInt("count");
|
||||
setSubtitle(getResources().getQuantityString(R.plurals.x_followers_you_know, count, count));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetAccountFamiliarFollowers(Set.of(account.id))
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<FamiliarFollowers> result){
|
||||
onDataLoaded(result.get(0).accounts.stream().map(a->new AccountViewModel(a, accountID, getActivity())).collect(Collectors.toList()), false);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume(){
|
||||
super.onResume();
|
||||
if(!loaded && !dataLoading)
|
||||
loadData();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop{
|
||||
private static final int QUERY_RESULT=937;
|
||||
private static final int SCAN_RESULT=456;
|
||||
|
||||
private TabLayout tabLayout;
|
||||
private TabLayout tabLayout, searchTabLayout;
|
||||
private ViewPager2 pager;
|
||||
private FrameLayout[] tabViews;
|
||||
private TabLayoutMediator tabLayoutMediator;
|
||||
@@ -62,6 +62,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop{
|
||||
private String currentQuery;
|
||||
private Intent scannerIntent;
|
||||
private Runnable searchExitCallback=this::exitSearch;
|
||||
private SearchResult.Type searchFilter;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -79,6 +80,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop{
|
||||
LinearLayout view=(LinearLayout) inflater.inflate(R.layout.fragment_discover, container, false);
|
||||
|
||||
tabLayout=view.findViewById(R.id.tabbar);
|
||||
searchTabLayout=view.findViewById(R.id.search_tabbar);
|
||||
pager=view.findViewById(R.id.pager);
|
||||
|
||||
tabViews=new FrameLayout[4];
|
||||
@@ -205,6 +207,30 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop{
|
||||
});
|
||||
tabsDivider=view.findViewById(R.id.tabs_divider);
|
||||
|
||||
searchTabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
|
||||
searchTabLayout.setTabTextSize(V.dp(14));
|
||||
searchTabLayout.addTab(searchTabLayout.newTab().setText(R.string.posts));
|
||||
searchTabLayout.addTab(searchTabLayout.newTab().setText(R.string.hashtags));
|
||||
searchTabLayout.addTab(searchTabLayout.newTab().setText(R.string.search_people));
|
||||
searchTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener(){
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab tab){
|
||||
searchFilter=switch(tab.getPosition()){
|
||||
case 0 -> SearchResult.Type.STATUS;
|
||||
case 1 -> SearchResult.Type.HASHTAG;
|
||||
case 2 -> SearchResult.Type.ACCOUNT;
|
||||
default -> throw new IllegalStateException("Unexpected value: " + tab.getPosition());
|
||||
};
|
||||
searchFragment.setQuery(currentQuery, searchFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabUnselected(TabLayout.Tab tab){}
|
||||
|
||||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab){}
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@@ -227,11 +253,11 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop{
|
||||
searchActive=true;
|
||||
pager.setVisibility(View.GONE);
|
||||
tabLayout.setVisibility(View.GONE);
|
||||
searchTabLayout.setVisibility(View.VISIBLE);
|
||||
searchView.setVisibility(View.VISIBLE);
|
||||
searchBack.setImageResource(me.grishka.appkit.R.drawable.ic_arrow_back);
|
||||
searchBack.setEnabled(true);
|
||||
searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
|
||||
tabsDivider.setVisibility(View.GONE);
|
||||
addBackCallback(searchExitCallback);
|
||||
}
|
||||
}
|
||||
@@ -242,12 +268,12 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop{
|
||||
searchActive=false;
|
||||
pager.setVisibility(View.VISIBLE);
|
||||
tabLayout.setVisibility(View.VISIBLE);
|
||||
searchTabLayout.setVisibility(View.GONE);
|
||||
searchView.setVisibility(View.GONE);
|
||||
searchText.setText(R.string.search_mastodon);
|
||||
searchBack.setImageResource(R.drawable.ic_search_24px);
|
||||
searchBack.setEnabled(false);
|
||||
searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
tabsDivider.setVisibility(View.VISIBLE);
|
||||
currentQuery=null;
|
||||
removeBackCallback(searchExitCallback);
|
||||
}
|
||||
@@ -267,14 +293,14 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop{
|
||||
if(reqCode==QUERY_RESULT && success){
|
||||
enterSearch();
|
||||
currentQuery=result.getString("query");
|
||||
SearchResult.Type type;
|
||||
if(result.containsKey("filter")){
|
||||
type=SearchResult.Type.values()[result.getInt("filter")];
|
||||
searchFilter=SearchResult.Type.values()[result.getInt("filter")];
|
||||
}else{
|
||||
type=null;
|
||||
searchFilter=SearchResult.Type.STATUS;
|
||||
}
|
||||
searchFragment.setQuery(currentQuery, type);
|
||||
searchFragment.setQuery(currentQuery, searchFilter);
|
||||
searchText.setText(currentQuery);
|
||||
updateSearchTabBar();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,6 +326,17 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop{
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSearchTabBar(){
|
||||
int tab=switch(searchFilter){
|
||||
case STATUS -> 0;
|
||||
case HASHTAG -> 1;
|
||||
case ACCOUNT -> 2;
|
||||
};
|
||||
if(searchTabLayout.getSelectedTabPosition()==tab)
|
||||
return;
|
||||
searchTabLayout.selectTab(searchTabLayout.getTabAt(tab));
|
||||
}
|
||||
|
||||
private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
|
||||
@NonNull
|
||||
@Override
|
||||
|
||||
@@ -95,7 +95,10 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
|
||||
goToAccountItem=new ListItem<>("", null, R.drawable.ic_person_24px, this::onGoToAccountClick);
|
||||
goToStatusSearchItem=new ListItem<>("", null, R.drawable.ic_search_24px, this::onGoToStatusSearchClick);
|
||||
goToAccountSearchItem=new ListItem<>("", null, R.drawable.ic_group_24px, this::onGoToAccountSearchClick);
|
||||
currentQuery=getArguments().getString("query");
|
||||
if(savedInstanceState!=null)
|
||||
currentQuery=savedInstanceState.getString("query");
|
||||
else
|
||||
currentQuery=getArguments().getString("query");
|
||||
|
||||
dataLoaded();
|
||||
doLoadData(0, 0);
|
||||
@@ -109,7 +112,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 +129,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));
|
||||
}
|
||||
@@ -181,6 +184,9 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
|
||||
setNavigationBarColor(color);
|
||||
if(currentQuery!=null){
|
||||
searchViewHelper.setQuery(currentQuery);
|
||||
}
|
||||
if(savedInstanceState!=null || currentQuery!=null){
|
||||
searchIcon.setAlpha(0);
|
||||
searchIcon.setAlpha(0);
|
||||
}
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 1, 0, 0, vh->!isInRecentMode() && vh.getAbsoluteAdapterPosition()==topOptions.size()-1));
|
||||
@@ -285,6 +291,12 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
|
||||
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(getActivity().getWindow().getDecorView().getWindowToken(), 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState){
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putString("query", currentQuery);
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.S)
|
||||
private float getScreenCornerRadius(WindowInsets insets, int pos){
|
||||
RoundedCorner corner=insets.getRoundedCorner(pos);
|
||||
@@ -296,6 +308,8 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
|
||||
private Animator createTransition(View prev, View container, boolean enter){
|
||||
int[] loc={0, 0};
|
||||
View searchBtn=prev.findViewById(R.id.search_wrap);
|
||||
if(searchBtn==null || searchBtn.getWidth()==0 || searchBtn.getHeight()==0)
|
||||
return null;
|
||||
searchBtn.getLocationInWindow(loc);
|
||||
int btnLeft=loc[0], btnTop=loc[1];
|
||||
container.getLocationInWindow(loc);
|
||||
@@ -323,21 +337,22 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
|
||||
ObjectAnimator boundsAnim;
|
||||
|
||||
Toolbar toolbar=getToolbar();
|
||||
float toolbarTX=offX-toolbar.getX();
|
||||
boolean isRTL=container.getLayoutDirection()==View.LAYOUT_DIRECTION_RTL;
|
||||
float toolbarTX=isRTL ? (toolbar.getX()-offX) : (offX-toolbar.getX());
|
||||
float toolbarTY=offY-toolbar.getY()+(searchBtn.getHeight()-toolbar.getHeight())/2f;
|
||||
ArrayList<Animator> anims=new ArrayList<>();
|
||||
int searchLayoutXOffset=isRTL ? V.dp(4) : V.dp(-4);
|
||||
anims.add(boundsAnim=ObjectAnimator.ofFloat(outlineProvider, "boundsFraction", 0f, 1f));
|
||||
anims.add(ObjectAnimator.ofFloat(outlineProvider, "radius", enter ? buttonRadius : screenRadius, enter ? screenRadius : buttonRadius));
|
||||
anims.add(ObjectAnimator.ofFloat(toolbar, View.TRANSLATION_X, enter ? toolbarTX : 0, enter ? 0 : toolbarTX));
|
||||
anims.add(ObjectAnimator.ofFloat(toolbar, View.TRANSLATION_Y, enter ? toolbarTY : 0, enter ? 0 : toolbarTY));
|
||||
anims.add(ObjectAnimator.ofFloat(searchViewHelper.getSearchLayout(), View.TRANSLATION_X, enter ? V.dp(-4) : 0, enter ? 0 : V.dp(-4)));
|
||||
anims.add(ObjectAnimator.ofFloat(searchViewHelper.getSearchLayout(), View.TRANSLATION_X, enter ? searchLayoutXOffset : 0, enter ? 0 : searchLayoutXOffset));
|
||||
anims.add(ObjectAnimator.ofFloat(searchViewHelper.getDivider(), View.ALPHA, enter ? 0 : 1, enter ? 1 : 0));
|
||||
View parentContent=prev.findViewById(R.id.discover_content);
|
||||
View parentContentParent=(View) parentContent.getParent();
|
||||
parentContentParent.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Surface));
|
||||
if(enter){
|
||||
anims.add(ObjectAnimator.ofFloat(contentWrap, View.TRANSLATION_Y, V.dp(-16), 0));
|
||||
}else{
|
||||
}
|
||||
anims.add(ObjectAnimator.ofFloat(contentWrap, View.ALPHA, enter ? 0 : 1, enter ? 1 : 0));
|
||||
for(Animator anim:anims){
|
||||
|
||||
@@ -131,7 +131,7 @@ public class AccountActivationFragment extends ToolbarFragment{
|
||||
private void onOpenEmailClick(View v){
|
||||
try{
|
||||
startActivity(Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, Intent.CATEGORY_APP_EMAIL).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
|
||||
}catch(ActivityNotFoundException x){
|
||||
}catch(ActivityNotFoundException|IllegalArgumentException x){
|
||||
Toast.makeText(getActivity(), R.string.no_app_to_handle_action, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(){
|
||||
|
||||
@@ -42,6 +42,7 @@ import org.jsoup.nodes.TextNode;
|
||||
import org.jsoup.select.NodeVisitor;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
@@ -237,6 +238,11 @@ public class SignupFragment extends ToolbarFragment{
|
||||
fakeAccount.acct=fakeAccount.username=username;
|
||||
fakeAccount.id="tmp"+System.currentTimeMillis();
|
||||
fakeAccount.displayName=displayName.getText().toString();
|
||||
fakeAccount.createdAt=Instant.now();
|
||||
fakeAccount.username=username;
|
||||
fakeAccount.acct=username;
|
||||
fakeAccount.url="https://"+instance.getDomain()+"/@"+username;
|
||||
fakeAccount.avatar=fakeAccount.avatarStatic=fakeAccount.headerStatic=fakeAccount.header=fakeAccount.note="";
|
||||
AccountSessionManager.getInstance().addAccount(instance, result, fakeAccount, apiApplication, new AccountActivationInfo(email, System.currentTimeMillis()));
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", AccountSessionManager.getInstance().getLastActiveAccountID());
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.catalog.GetDonationCampaigns;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.donations.DonationCampaign;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.SectionHeaderListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.sheets.DonationSheet;
|
||||
import org.joinmastodon.android.ui.sheets.DonationSuccessfulSheet;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.viewcontrollers.ComposeLanguageAlertViewController;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class SettingsAccountFragment extends BaseSettingsFragment<Void>{
|
||||
private static final int DONATION_RESULT=433;
|
||||
private DonationSheet donationSheet;
|
||||
private boolean loggedOut;
|
||||
private ListItem<Void> languageItem;
|
||||
private Locale postLanguage;
|
||||
private ComposeLanguageAlertViewController.SelectedOption newPostLanguage;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
AccountSession account=AccountSessionManager.get(accountID);
|
||||
setTitle(account.getFullUsername());
|
||||
if(account.preferences!=null && account.preferences.postingDefaultLanguage!=null){
|
||||
postLanguage=Locale.forLanguageTag(account.preferences.postingDefaultLanguage);
|
||||
}
|
||||
|
||||
ArrayList<ListItem<Void>> items=new ArrayList<>();
|
||||
items.add(new SectionHeaderListItem(R.string.account_settings));
|
||||
|
||||
items.add(new ListItem<>(R.string.settings_privacy, 0, R.drawable.ic_privacy_tip_24px, this::onPrivacyClick));
|
||||
items.add(new ListItem<>(R.string.settings_filters, 0, R.drawable.ic_filter_alt_24px, this::onFiltersClick));
|
||||
items.add(new ListItem<>(R.string.settings_notifications, 0, R.drawable.ic_notifications_24px, this::onNotificationsClick));
|
||||
items.add(languageItem=new ListItem<>(getString(R.string.default_post_language), postLanguage!=null ? postLanguage.getDisplayName(Locale.getDefault()) : null, R.drawable.ic_language_24px, this::onDefaultLanguageClick));
|
||||
|
||||
items.add(new SectionHeaderListItem(account.domain));
|
||||
items.add(new ListItem<>(getString(R.string.settings_about_this_server), getString(R.string.settings_server_explanation), R.drawable.ic_dns_24px, this::onServerClick));
|
||||
if(account.isEligibleForDonations()){
|
||||
items.add(new ListItem<>(R.string.settings_donate, 0, R.drawable.ic_volunteer_activism_24px, this::onDonateClick));
|
||||
}
|
||||
|
||||
items.add(new SectionHeaderListItem(R.string.manage_account));
|
||||
items.add(new ListItem<>(R.string.switch_to_this_account, 0, R.drawable.ic_switch_account_24px, AccountSessionManager.getInstance().getLastActiveAccountID().equals(accountID) ? null : this::onSwitchAccountClick));
|
||||
items.add(new ListItem<>(R.string.delete_account, 0, R.drawable.ic_delete_forever_24px, this::onDeleteAccountClick, R.attr.colorM3Error, false));
|
||||
items.add(new ListItem<>(R.string.log_out, 0, R.drawable.ic_logout_24px, this::onLogOutClick, R.attr.colorM3Error, false));
|
||||
|
||||
onDataLoaded(items);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data){
|
||||
if(requestCode==DONATION_RESULT){
|
||||
if(donationSheet!=null)
|
||||
donationSheet.dismissWithoutAnimation();
|
||||
if(resultCode==Activity.RESULT_OK){
|
||||
new DonationSuccessfulSheet(getActivity(), accountID, data.getStringExtra("postText")).showWithoutAnimation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHidden(){
|
||||
super.onHidden();
|
||||
if(!loggedOut){
|
||||
if(newPostLanguage!=null){
|
||||
AccountSession s=AccountSessionManager.get(accountID);
|
||||
if(s.preferences==null)
|
||||
s.preferences=new Preferences();
|
||||
s.preferences.postingDefaultLanguage=newPostLanguage.locale.toLanguageTag();
|
||||
s.savePreferencesLater();
|
||||
}
|
||||
AccountSessionManager.get(accountID).savePreferencesIfPending();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
toolbarTitleView.setAlpha(list.getChildCount()==0 || list.getChildAdapterPosition(list.getChildAt(0))==0 ? 0 : 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
TextView largeTitle=(TextView) getToolbarLayoutInflater().inflate(R.layout.large_title, list, false);
|
||||
largeTitle.setText(getTitle());
|
||||
largeTitle.setPadding(largeTitle.getPaddingLeft(), largeTitle.getPaddingTop(), largeTitle.getPaddingRight(), 0);
|
||||
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(largeTitle));
|
||||
adapter.addAdapter(super.getAdapter());
|
||||
return adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
|
||||
private boolean titleVisible=true;
|
||||
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
|
||||
boolean newTitleVisible=list.getChildAdapterPosition(list.getChildAt(0))==0;
|
||||
if(newTitleVisible!=titleVisible){
|
||||
titleVisible=newTitleVisible;
|
||||
toolbarTitleView.animate().alpha(titleVisible ? 0 : 1).setDuration(250).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Bundle makeFragmentArgs(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
return args;
|
||||
}
|
||||
|
||||
private void onPrivacyClick(ListItem<?> item_){
|
||||
Nav.go(getActivity(), SettingsPrivacyFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onFiltersClick(ListItem<?> item_){
|
||||
Nav.go(getActivity(), SettingsFiltersFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onNotificationsClick(ListItem<?> item_){
|
||||
Nav.go(getActivity(), SettingsNotificationsFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onDefaultLanguageClick(ListItem<?> item){
|
||||
ComposeLanguageAlertViewController vc=new ComposeLanguageAlertViewController(getActivity(), null, newPostLanguage==null ? new ComposeLanguageAlertViewController.SelectedOption(-1, postLanguage, null) : newPostLanguage, null);
|
||||
AlertDialog dlg=new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.default_post_language)
|
||||
.setView(vc.getView())
|
||||
.setPositiveButton(R.string.cancel, null)
|
||||
.show();
|
||||
vc.setSelectionListener(opt->{
|
||||
if(!opt.locale.equals(postLanguage)){
|
||||
newPostLanguage=opt;
|
||||
languageItem.subtitle=newPostLanguage.locale.getDisplayLanguage(Locale.getDefault());
|
||||
rebindItem(languageItem);
|
||||
}
|
||||
dlg.dismiss();
|
||||
});
|
||||
}
|
||||
|
||||
private void onServerClick(ListItem<?> item_){
|
||||
Nav.go(getActivity(), SettingsServerFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private boolean useStagingEnvironmentForDonations(){
|
||||
return (BuildConfig.DEBUG || BuildConfig.BUILD_TYPE.equals("appcenterPrivateBeta")) && getActivity().getSharedPreferences("debug", Context.MODE_PRIVATE).getBoolean("donationsStaging", false);
|
||||
}
|
||||
|
||||
private void onDonateClick(ListItem<?> item){
|
||||
GetDonationCampaigns req=new GetDonationCampaigns(Locale.getDefault().toLanguageTag().replace('-', '_'), String.valueOf(AccountSessionManager.get(accountID).getDonationSeed()), "menu");
|
||||
if(useStagingEnvironmentForDonations()){
|
||||
req.setStaging(true);
|
||||
}
|
||||
req.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(DonationCampaign result){
|
||||
Activity activity=getActivity();
|
||||
if(activity==null)
|
||||
return;
|
||||
if(result==null){
|
||||
Toast.makeText(activity, "No campaign available (server misconfiguration?)", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
donationSheet=new DonationSheet(getActivity(), result, accountID, intent->startActivityForResult(intent, DONATION_RESULT));
|
||||
donationSheet.setOnDismissListener(dialog->donationSheet=null);
|
||||
donationSheet.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, true)
|
||||
.execNoAuth("");
|
||||
}
|
||||
|
||||
private void onSwitchAccountClick(ListItem<?> item){
|
||||
if(AccountSessionManager.getInstance().tryGetAccount(accountID)!=null){
|
||||
AccountSessionManager.getInstance().setLastActiveAccountID(accountID);
|
||||
((MainActivity)getActivity()).restartHomeFragment();
|
||||
}
|
||||
}
|
||||
|
||||
private void onDeleteAccountClick(ListItem<?> item){
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/settings/delete");
|
||||
}
|
||||
|
||||
private void onLogOutClick(ListItem<?> item_){
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setMessage(getString(R.string.confirm_log_out, session.getFullUsername()))
|
||||
.setPositiveButton(R.string.log_out, (dialog, which)->AccountSessionManager.get(accountID).logOut(getActivity(), ()->{
|
||||
loggedOut=true;
|
||||
((MainActivity)getActivity()).restartHomeFragment();
|
||||
}))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
@@ -13,38 +12,25 @@ import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Preferences;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.viewcontrollers.ComposeLanguageAlertViewController;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class SettingsBehaviorFragment extends BaseSettingsFragment<Void>{
|
||||
private ListItem<Void> languageItem, customTabsItem;
|
||||
private ListItem<Void> customTabsItem;
|
||||
private CheckableListItem<Void> altTextItem, playGifsItem, confirmUnfollowItem, confirmBoostItem, confirmDeleteItem;
|
||||
private Locale postLanguage;
|
||||
private ComposeLanguageAlertViewController.SelectedOption newPostLanguage;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.settings_behavior);
|
||||
|
||||
AccountSession s=AccountSessionManager.get(accountID);
|
||||
if(s.preferences!=null && s.preferences.postingDefaultLanguage!=null){
|
||||
postLanguage=Locale.forLanguageTag(s.preferences.postingDefaultLanguage);
|
||||
}
|
||||
|
||||
onDataLoaded(List.of(
|
||||
languageItem=new ListItem<>(getString(R.string.default_post_language), postLanguage!=null ? postLanguage.getDisplayName(Locale.getDefault()) : null, R.drawable.ic_language_24px, this::onDefaultLanguageClick),
|
||||
customTabsItem=new ListItem<>(R.string.settings_custom_tabs, GlobalUserPreferences.useCustomTabs ? R.string.in_app_browser : R.string.system_browser, R.drawable.ic_open_in_browser_24px, this::onCustomTabsClick),
|
||||
altTextItem=new CheckableListItem<>(R.string.settings_alt_text_reminders, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.altTextReminders, R.drawable.ic_alt_24px, this::toggleCheckableItem),
|
||||
playGifsItem=new CheckableListItem<>(R.string.settings_gif, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.playGifs, R.drawable.ic_animation_24px, this::toggleCheckableItem),
|
||||
@@ -57,25 +43,7 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void>{
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
private void onDefaultLanguageClick(ListItem<?> item){
|
||||
ComposeLanguageAlertViewController vc=new ComposeLanguageAlertViewController(getActivity(), null, newPostLanguage==null ? new ComposeLanguageAlertViewController.SelectedOption(-1, postLanguage, null) : newPostLanguage, null);
|
||||
AlertDialog dlg=new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.default_post_language)
|
||||
.setView(vc.getView())
|
||||
.setPositiveButton(R.string.cancel, null)
|
||||
.show();
|
||||
vc.setSelectionListener(opt->{
|
||||
if(!opt.locale.equals(postLanguage)){
|
||||
newPostLanguage=opt;
|
||||
languageItem.subtitle=newPostLanguage.locale.getDisplayLanguage(Locale.getDefault());
|
||||
rebindItem(languageItem);
|
||||
}
|
||||
dlg.dismiss();
|
||||
});
|
||||
}
|
||||
|
||||
private void onCustomTabsClick(ListItem<?> item){
|
||||
// GlobalUserPreferences.useCustomTabs=customTabsItem.checked;
|
||||
Intent intent=new Intent(Intent.ACTION_VIEW, Uri.parse("http://example.com"));
|
||||
ResolveInfo info=getActivity().getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
|
||||
final String browserName;
|
||||
@@ -125,12 +93,5 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void>{
|
||||
GlobalUserPreferences.confirmBoost=confirmBoostItem.checked;
|
||||
GlobalUserPreferences.confirmDeletePost=confirmDeleteItem.checked;
|
||||
GlobalUserPreferences.save();
|
||||
if(newPostLanguage!=null){
|
||||
AccountSession s=AccountSessionManager.get(accountID);
|
||||
if(s.preferences==null)
|
||||
s.preferences=new Preferences();
|
||||
s.preferences.postingDefaultLanguage=newPostLanguage.locale.toLanguageTag();
|
||||
s.savePreferencesLater();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -13,14 +13,12 @@ import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountLocalPreferences;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.StatusDisplaySettingsChangedEvent;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
@@ -29,21 +27,26 @@ 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){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.settings_display);
|
||||
AccountSession s=AccountSessionManager.get(accountID);
|
||||
AccountLocalPreferences lp=s.getLocalPreferences();
|
||||
onDataLoaded(List.of(
|
||||
themeItem=new ListItem<>(R.string.settings_theme, getAppearanceValue(), R.drawable.ic_dark_mode_24px, this::onAppearanceClick),
|
||||
showCWsItem=new CheckableListItem<>(R.string.settings_show_cws, 0, CheckableListItem.Style.SWITCH, lp.showCWs, R.drawable.ic_warning_24px, 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, GlobalUserPreferences.showCWs, R.drawable.ic_warning_24px, this::toggleCheckableItem));
|
||||
items.add(hideSensitiveMediaItem=new CheckableListItem<>(R.string.settings_hide_sensitive_media, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.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, GlobalUserPreferences.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, GlobalUserPreferences.customEmojiInNames, R.drawable.ic_emoticon_24px, this::toggleCheckableItem));
|
||||
onDataLoaded(items);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -62,13 +65,11 @@ public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
|
||||
@Override
|
||||
protected void onHidden(){
|
||||
super.onHidden();
|
||||
AccountSession s=AccountSessionManager.get(accountID);
|
||||
AccountLocalPreferences lp=s.getLocalPreferences();
|
||||
lp.showCWs=showCWsItem.checked;
|
||||
lp.hideSensitiveMedia=hideSensitiveMediaItem.checked;
|
||||
lp.showInteractionCounts=interactionCountsItem.checked;
|
||||
lp.customEmojiInNames=emojiInNamesItem.checked;
|
||||
lp.save();
|
||||
GlobalUserPreferences.showCWs=showCWsItem.checked;
|
||||
GlobalUserPreferences.hideSensitiveMedia=hideSensitiveMediaItem.checked;
|
||||
GlobalUserPreferences.showInteractionCounts=interactionCountsItem.checked;
|
||||
GlobalUserPreferences.customEmojiInNames=emojiInNamesItem.checked;
|
||||
GlobalUserPreferences.save();
|
||||
E.post(new StatusDisplaySettingsChangedEvent(accountID));
|
||||
}
|
||||
|
||||
@@ -109,6 +110,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());
|
||||
|
||||
@@ -1,53 +1,44 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.MainActivity;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.catalog.GetDonationCampaigns;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.model.donations.DonationCampaign;
|
||||
import org.joinmastodon.android.fragments.SplashFragment;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.sheets.DonationSheet;
|
||||
import org.joinmastodon.android.ui.sheets.DonationSuccessfulSheet;
|
||||
import org.joinmastodon.android.model.viewmodel.SectionHeaderListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.SettingsAccountListItem;
|
||||
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class SettingsMainFragment extends BaseSettingsFragment<Void>{
|
||||
private static final int DONATION_RESULT=433;
|
||||
public class SettingsMainFragment extends BaseSettingsFragment<Object>{
|
||||
|
||||
private boolean loggedOut;
|
||||
private HideableSingleViewRecyclerAdapter bannerAdapter;
|
||||
private Button updateButton1, updateButton2;
|
||||
private TextView updateText;
|
||||
private DonationSheet donationSheet;
|
||||
private Runnable updateDownloadProgressUpdater=new Runnable(){
|
||||
@Override
|
||||
public void run(){
|
||||
@@ -63,27 +54,35 @@ public class SettingsMainFragment extends BaseSettingsFragment<Void>{
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.settings);
|
||||
setSubtitle(AccountSessionManager.get(accountID).getFullUsername());
|
||||
ArrayList<ListItem<Void>> items=new ArrayList<>();
|
||||
if(BuildConfig.DEBUG || BuildConfig.BUILD_TYPE.equals("appcenterPrivateBeta")){
|
||||
items.add(new ListItem<>("Debug settings", null, R.drawable.ic_settings_24px, i->Nav.go(getActivity(), SettingsDebugFragment.class, makeFragmentArgs()), null, 0, true));
|
||||
ArrayList<ListItem<?>> items=new ArrayList<>();
|
||||
items.add(new SectionHeaderListItem(R.string.settings_accounts));
|
||||
for(AccountSession session:AccountSessionManager.getInstance().getLoggedInAccounts()){
|
||||
ImageLoaderRequest req;
|
||||
if(session.self.avatar!=null)
|
||||
req=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? session.self.avatar : session.self.avatarStatic, V.dp(50), V.dp(50));
|
||||
else
|
||||
req=null;
|
||||
items.add(new SettingsAccountListItem<>(session.getFullUsername(), null, req, this::onAccountClick, session, false));
|
||||
}
|
||||
|
||||
items.addAll(List.of(
|
||||
new ListItem<>(R.string.settings_behavior, 0, R.drawable.ic_settings_24px, this::onBehaviorClick),
|
||||
new ListItem<>(R.string.settings_display, 0, R.drawable.ic_style_24px, this::onDisplayClick),
|
||||
new ListItem<>(R.string.settings_privacy, 0, R.drawable.ic_privacy_tip_24px, this::onPrivacyClick),
|
||||
new ListItem<>(R.string.settings_filters, 0, R.drawable.ic_filter_alt_24px, this::onFiltersClick),
|
||||
new ListItem<>(R.string.settings_notifications, 0, R.drawable.ic_notifications_24px, this::onNotificationsClick),
|
||||
new ListItem<>(AccountSessionManager.get(accountID).domain, getString(R.string.settings_server_explanation), R.drawable.ic_dns_24px, this::onServerClick),
|
||||
new ListItem<>(getString(R.string.about_app, getString(R.string.app_name)), null, R.drawable.ic_info_24px, this::onAboutClick, null, 0, true)
|
||||
new ListItem<>(R.string.settings_add_account, 0, R.drawable.ic_add_24px, this::onAddAccountClick),
|
||||
|
||||
new SectionHeaderListItem(R.string.settings_app_settings),
|
||||
new ListItem<>(R.string.settings_behavior, 0, R.drawable.ic_tune_24px, this::onBehaviorClick),
|
||||
new ListItem<>(R.string.settings_display, 0, R.drawable.ic_style_24px, this::onDisplayClick)
|
||||
|
||||
));
|
||||
if(AccountSessionManager.get(accountID).isEligibleForDonations()){
|
||||
items.add(new ListItem<>(R.string.settings_donate, 0, R.drawable.ic_volunteer_activism_24px, this::onDonateClick));
|
||||
items.add(new ListItem<>(R.string.settings_manage_donations, 0, R.drawable.ic_settings_heart_24px, this::onManageDonationClick, 0, true));
|
||||
items.add(new ListItem<>(R.string.settings_manage_donations, 0, R.drawable.ic_settings_heart_24px, this::onManageDonationClick));
|
||||
}
|
||||
items.add(new ListItem<>(R.string.manage_accounts, 0, R.drawable.ic_switch_account_24px, this::onManageAccountsClick));
|
||||
items.add(new ListItem<>(R.string.log_out, 0, R.drawable.ic_logout_24px, this::onLogOutClick, R.attr.colorM3Error, false));
|
||||
onDataLoaded(items);
|
||||
items.add(new ListItem<>(getString(R.string.about_app, getString(R.string.app_name)), null, R.drawable.ic_info_24px, this::onAboutClick, null));
|
||||
if(BuildConfig.DEBUG || BuildConfig.BUILD_TYPE.equals("appcenterPrivateBeta")){
|
||||
items.add(new ListItem<>("Debug settings", null, R.drawable.ic_bug_report_24px, i->Nav.go(getActivity(), SettingsDebugFragment.class, makeFragmentArgs()), null));
|
||||
}
|
||||
|
||||
//noinspection unchecked
|
||||
onDataLoaded((List<ListItem<Object>>)(Object)items);
|
||||
|
||||
AccountSession session=AccountSessionManager.get(accountID);
|
||||
session.reloadPreferences(null);
|
||||
@@ -100,13 +99,6 @@ public class SettingsMainFragment extends BaseSettingsFragment<Void>{
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
protected void onHidden(){
|
||||
super.onHidden();
|
||||
if(!loggedOut)
|
||||
AccountSessionManager.get(accountID).savePreferencesIfPending();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
View banner=getActivity().getLayoutInflater().inflate(R.layout.item_settings_banner, list, false);
|
||||
@@ -137,23 +129,22 @@ public class SettingsMainFragment extends BaseSettingsFragment<Void>{
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data){
|
||||
if(requestCode==DONATION_RESULT){
|
||||
if(donationSheet!=null)
|
||||
donationSheet.dismissWithoutAnimation();
|
||||
if(resultCode==Activity.RESULT_OK){
|
||||
new DonationSuccessfulSheet(getActivity(), accountID, data.getStringExtra("postText")).showWithoutAnimation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Bundle makeFragmentArgs(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
return args;
|
||||
}
|
||||
|
||||
private void onAccountClick(SettingsAccountListItem<AccountSession> item){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", item.parentObject.getID());
|
||||
Nav.go(getActivity(), SettingsAccountFragment.class, args);
|
||||
}
|
||||
|
||||
private void onAddAccountClick(ListItem<?> item_){
|
||||
Nav.go(getActivity(), SplashFragment.class, null);
|
||||
}
|
||||
|
||||
private void onBehaviorClick(ListItem<?> item_){
|
||||
Nav.go(getActivity(), SettingsBehaviorFragment.class, makeFragmentArgs());
|
||||
}
|
||||
@@ -162,73 +153,16 @@ public class SettingsMainFragment extends BaseSettingsFragment<Void>{
|
||||
Nav.go(getActivity(), SettingsDisplayFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onPrivacyClick(ListItem<?> item_){
|
||||
Nav.go(getActivity(), SettingsPrivacyFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onFiltersClick(ListItem<?> item_){
|
||||
Nav.go(getActivity(), SettingsFiltersFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onNotificationsClick(ListItem<?> item_){
|
||||
Nav.go(getActivity(), SettingsNotificationsFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onServerClick(ListItem<?> item_){
|
||||
Nav.go(getActivity(), SettingsServerFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onAboutClick(ListItem<?> item_){
|
||||
Nav.go(getActivity(), SettingsAboutAppFragment.class, makeFragmentArgs());
|
||||
}
|
||||
|
||||
private void onManageAccountsClick(ListItem<?> item){
|
||||
new AccountSwitcherSheet(getActivity(), null).setOnLoggedOutCallback(()->loggedOut=true).show();
|
||||
}
|
||||
|
||||
private void onLogOutClick(ListItem<?> item_){
|
||||
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setMessage(getString(R.string.confirm_log_out, session.getFullUsername()))
|
||||
.setPositiveButton(R.string.log_out, (dialog, which)->AccountSessionManager.get(accountID).logOut(getActivity(), ()->{
|
||||
loggedOut=true;
|
||||
((MainActivity)getActivity()).restartHomeFragment();
|
||||
}))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void onDonateClick(ListItem<?> item){
|
||||
GetDonationCampaigns req=new GetDonationCampaigns(Locale.getDefault().toLanguageTag().replace('-', '_'), String.valueOf(AccountSessionManager.get(accountID).getDonationSeed()), "menu");
|
||||
if(getActivity().getSharedPreferences("debug", Context.MODE_PRIVATE).getBoolean("donationsStaging", false)){
|
||||
req.setStaging(true);
|
||||
}
|
||||
req.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(DonationCampaign result){
|
||||
Activity activity=getActivity();
|
||||
if(activity==null)
|
||||
return;
|
||||
if(result==null){
|
||||
Toast.makeText(activity, "No campaign available (server misconfiguration?)", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
donationSheet=new DonationSheet(getActivity(), result, accountID, intent->startActivityForResult(intent, DONATION_RESULT));
|
||||
donationSheet.setOnDismissListener(dialog->donationSheet=null);
|
||||
donationSheet.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, true)
|
||||
.execNoAuth("");
|
||||
private boolean useStagingEnvironmentForDonations(){
|
||||
return (BuildConfig.DEBUG || BuildConfig.BUILD_TYPE.equals("appcenterPrivateBeta")) && getActivity().getSharedPreferences("debug", Context.MODE_PRIVATE).getBoolean("donationsStaging", false);
|
||||
}
|
||||
|
||||
private void onManageDonationClick(ListItem<?> item){
|
||||
UiUtils.launchWebBrowser(getActivity(), "https://sponsor.staging.joinmastodon.org/donate/manage");
|
||||
UiUtils.launchWebBrowser(getActivity(), useStagingEnvironmentForDonations() ? "https://sponsor.staging.joinmastodon.org/donate/manage" : "https://sponsor.joinmastodon.org/donate/manage");
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
||||
@@ -42,7 +42,7 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
|
||||
private TextView bannerText;
|
||||
private Button bannerButton;
|
||||
|
||||
private CheckableListItem<Void> mentionsItem, boostsItem, favoritesItem, followersItem, pollsItem;
|
||||
private CheckableListItem<Void> mentionsItem, boostsItem, favoritesItem, followersItem, pollsItem, statusesItem;
|
||||
private List<CheckableListItem<Void>> typeItems;
|
||||
private boolean needUpdateNotificationSettings;
|
||||
private boolean notificationsAllowed=true;
|
||||
@@ -62,7 +62,8 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
|
||||
boostsItem=new CheckableListItem<>(R.string.notification_type_reblog, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.reblog, this::toggleCheckableItem),
|
||||
favoritesItem=new CheckableListItem<>(R.string.notification_type_favorite, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.favourite, this::toggleCheckableItem),
|
||||
followersItem=new CheckableListItem<>(R.string.notification_type_follow, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.follow, this::toggleCheckableItem),
|
||||
pollsItem=new CheckableListItem<>(R.string.notification_type_poll, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.poll, this::toggleCheckableItem)
|
||||
pollsItem=new CheckableListItem<>(R.string.notification_type_poll, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.poll, this::toggleCheckableItem),
|
||||
statusesItem=new CheckableListItem<>(R.string.notification_type_status, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.status, this::toggleCheckableItem)
|
||||
));
|
||||
|
||||
typeItems=List.of(mentionsItem, boostsItem, favoritesItem, followersItem, pollsItem);
|
||||
@@ -82,13 +83,15 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
|
||||
|| boostsItem.checked!=ps.alerts.reblog
|
||||
|| favoritesItem.checked!=ps.alerts.favourite
|
||||
|| followersItem.checked!=ps.alerts.follow
|
||||
|| pollsItem.checked!=ps.alerts.poll;
|
||||
|| pollsItem.checked!=ps.alerts.poll
|
||||
|| statusesItem.checked!=ps.alerts.status;
|
||||
if(needUpdateNotificationSettings && PushSubscriptionManager.arePushNotificationsAvailable()){
|
||||
ps.alerts.mention=mentionsItem.checked;
|
||||
ps.alerts.reblog=boostsItem.checked;
|
||||
ps.alerts.favourite=favoritesItem.checked;
|
||||
ps.alerts.follow=followersItem.checked;
|
||||
ps.alerts.poll=pollsItem.checked;
|
||||
ps.alerts.status=statusesItem.checked;
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().updatePushSettings(pushSubscription);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.joinmastodon.android.model;
|
||||
|
||||
import org.joinmastodon.android.api.AllFieldsAreRequired;
|
||||
import org.joinmastodon.android.api.ObjectValidationException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@AllFieldsAreRequired
|
||||
public class FamiliarFollowers extends BaseModel{
|
||||
public String id;
|
||||
public List<Account> accounts;
|
||||
|
||||
@Override
|
||||
public void postprocess() throws ObjectValidationException{
|
||||
super.postprocess();
|
||||
for(Account acc:accounts){
|
||||
acc.postprocess();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,9 @@ public class PushNotification extends BaseModel{
|
||||
@SerializedName("follow")
|
||||
FOLLOW(R.string.notification_type_follow),
|
||||
@SerializedName("poll")
|
||||
POLL(R.string.notification_type_poll);
|
||||
POLL(R.string.notification_type_poll),
|
||||
@SerializedName("status")
|
||||
STATUS(R.string.notification_type_status);
|
||||
|
||||
@StringRes
|
||||
public final int localizedName;
|
||||
|
||||
@@ -41,21 +41,23 @@ public class PushSubscription extends BaseModel implements Cloneable{
|
||||
public boolean reblog;
|
||||
public boolean mention;
|
||||
public boolean poll;
|
||||
public boolean status;
|
||||
|
||||
public static Alerts ofAll(){
|
||||
Alerts alerts=new Alerts();
|
||||
alerts.follow=alerts.favourite=alerts.reblog=alerts.mention=alerts.poll=true;
|
||||
alerts.follow=alerts.favourite=alerts.reblog=alerts.mention=alerts.poll=alerts.status=true;
|
||||
return alerts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(){
|
||||
return "Alerts{"+
|
||||
"follow="+follow+
|
||||
", favourite="+favourite+
|
||||
"favourite="+favourite+
|
||||
", follow="+follow+
|
||||
", reblog="+reblog+
|
||||
", mention="+mention+
|
||||
", poll="+poll+
|
||||
", status="+status+
|
||||
'}';
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package org.joinmastodon.android.model.viewmodel;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.AccountField;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
@@ -24,21 +24,21 @@ 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();
|
||||
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
|
||||
if(GlobalUserPreferences.customEmojiInNames)
|
||||
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
|
||||
else
|
||||
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;
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Card;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
@@ -31,7 +30,7 @@ public class CardViewModel{
|
||||
|
||||
if(authorAccount!=null){
|
||||
parsedAuthorName=new SpannableStringBuilder(authorAccount.displayName);
|
||||
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
|
||||
if(GlobalUserPreferences.customEmojiInNames)
|
||||
HtmlParser.parseCustomEmoji(parsedAuthorName, authorAccount.emojis);
|
||||
authorNameEmojiHelper.setText(parsedAuthorName);
|
||||
authorAvaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? authorAccount.avatar : authorAccount.avatarStatic, V.dp(50), V.dp(50));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.joinmastodon.android.model.viewmodel;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
public class SectionHeaderListItem extends ListItem<Void>{
|
||||
public SectionHeaderListItem(String title){
|
||||
super(title, null, null);
|
||||
}
|
||||
|
||||
public SectionHeaderListItem(@StringRes int title){
|
||||
super(title, 0, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(){
|
||||
return R.id.list_item_section_header;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.joinmastodon.android.model.viewmodel;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
|
||||
public class SettingsAccountListItem<T> extends ListItem<T>{
|
||||
public ImageLoaderRequest avatar;
|
||||
|
||||
public SettingsAccountListItem(String title, String subtitle, ImageLoaderRequest avatar, Consumer<SettingsAccountListItem<T>> onClick, T parentObject, boolean dividerAfter){
|
||||
super(title, subtitle, 0, (Consumer<ListItem<T>>)(Object)onClick, parentObject, 0, dividerAfter);
|
||||
this.avatar=avatar;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(){
|
||||
return R.id.list_item_settings_account;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ public class SearchViewHelper{
|
||||
searchEdit.setTextAppearance(R.style.m3_body_large);
|
||||
searchEdit.setHintTextColor(UiUtils.getThemeColor(toolbarContext, R.attr.colorM3OnSurfaceVariant));
|
||||
searchEdit.setTextColor(UiUtils.getThemeColor(toolbarContext, R.attr.colorM3OnSurface));
|
||||
searchEdit.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START);
|
||||
searchLayout.addView(searchEdit, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
|
||||
|
||||
clearSearchButton=new ImageButton(context);
|
||||
|
||||
@@ -5,17 +5,19 @@ import android.view.ViewGroup;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.model.viewmodel.AvatarPileListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.SettingsAccountListItem;
|
||||
import org.joinmastodon.android.ui.viewholders.AvatarPileListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.CheckboxOrRadioListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.ListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.OptionsListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.SectionHeaderListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.SettingsAccountListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.SimpleListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.viewholders.SwitchListItemViewHolder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||
import me.grishka.appkit.imageloader.ListImageLoaderWrapper;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
@@ -49,6 +51,10 @@ public class GenericListItemsAdapter<T> extends UsableRecyclerView.Adapter<ListI
|
||||
return new OptionsListItemViewHolder(parent.getContext(), parent);
|
||||
if(viewType==R.id.list_item_avatar_pile)
|
||||
return new AvatarPileListItemViewHolder(parent.getContext(), parent);
|
||||
if(viewType==R.id.list_item_settings_account)
|
||||
return new SettingsAccountListItemViewHolder(parent.getContext(), parent);
|
||||
if(viewType==R.id.list_item_section_header)
|
||||
return new SectionHeaderListItemViewHolder(parent.getContext(), parent);
|
||||
|
||||
throw new IllegalArgumentException("Unexpected view type "+viewType);
|
||||
}
|
||||
@@ -74,6 +80,8 @@ public class GenericListItemsAdapter<T> extends UsableRecyclerView.Adapter<ListI
|
||||
ListItem<?> item=items.get(position);
|
||||
if(item instanceof AvatarPileListItem<?> avatarPileListItem)
|
||||
return avatarPileListItem.avatars.size();
|
||||
if(item instanceof SettingsAccountListItem<?>)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -82,6 +90,8 @@ public class GenericListItemsAdapter<T> extends UsableRecyclerView.Adapter<ListI
|
||||
ListItem<?> item=items.get(position);
|
||||
if(item instanceof AvatarPileListItem<?> avatarPileListItem)
|
||||
return avatarPileListItem.avatars.get(image);
|
||||
if(item instanceof SettingsAccountListItem<?> accountItem)
|
||||
return accountItem.avatar;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
@@ -38,6 +40,7 @@ public class GapStatusDisplayItem extends StatusDisplayItem{
|
||||
public void onBind(GapStatusDisplayItem item){
|
||||
text.setVisibility(item.loading ? View.GONE : View.VISIBLE);
|
||||
progress.setVisibility(item.loading ? View.VISIBLE : View.GONE);
|
||||
text.setText(item.enteredFromTop ? R.string.load_missing_posts_above : R.string.load_missing_posts_below);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -75,7 +75,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
this.accountID=accountID;
|
||||
parsedName=new SpannableStringBuilder(user.displayName);
|
||||
this.status=status;
|
||||
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames)
|
||||
if(GlobalUserPreferences.customEmojiInNames)
|
||||
HtmlParser.parseCustomEmoji(parsedName, user.emojis);
|
||||
emojiHelper.setText(parsedName);
|
||||
if(status!=null){
|
||||
|
||||
@@ -9,7 +9,6 @@ import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
@@ -36,10 +35,10 @@ public class InlineStatusStatusDisplayItem extends StatusDisplayItem{
|
||||
this.status=status;
|
||||
|
||||
parsedName=new SpannableStringBuilder(status.account.displayName);
|
||||
if(AccountSessionManager.get(parentFragment.getAccountID()).getLocalPreferences().customEmojiInNames)
|
||||
if(GlobalUserPreferences.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);
|
||||
|
||||
@@ -209,18 +209,22 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
@Override
|
||||
public void setImage(int index, Drawable drawable){
|
||||
controllers.get(index).setImage(drawable);
|
||||
if(index<controllers.size())
|
||||
controllers.get(index).setImage(drawable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearImage(int index){
|
||||
controllers.get(index).clearImage();
|
||||
if(index<controllers.size())
|
||||
controllers.get(index).clearImage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onImageLoadingFailed(int index, Throwable error){
|
||||
controllers.get(index).showFailedOverlay();
|
||||
thereAreFailedImages=true;
|
||||
if(index<controllers.size()){
|
||||
controllers.get(index).showFailedOverlay();
|
||||
thereAreFailedImages=true;
|
||||
}
|
||||
}
|
||||
|
||||
private void onViewClick(View v){
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -7,8 +7,8 @@ import android.text.SpannableStringBuilder;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
@@ -31,7 +31,7 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
|
||||
public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon){
|
||||
super(parentID, parentFragment);
|
||||
SpannableStringBuilder ssb=new SpannableStringBuilder(text);
|
||||
if(AccountSessionManager.get(parentFragment.getAccountID()).getLocalPreferences().customEmojiInNames)
|
||||
if(GlobalUserPreferences.customEmojiInNames)
|
||||
HtmlParser.parseCustomEmoji(ssb, emojis);
|
||||
this.text=ssb;
|
||||
emojiHelper.setText(ssb);
|
||||
|
||||
@@ -8,8 +8,8 @@ import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
@@ -97,7 +97,7 @@ public abstract class StatusDisplayItem{
|
||||
ArrayList<StatusDisplayItem> items=new ArrayList<>();
|
||||
Status statusForContent=status.getContentStatus();
|
||||
HeaderStatusDisplayItem header=null;
|
||||
boolean hideCounts=!AccountSessionManager.get(accountID).getLocalPreferences().showInteractionCounts;
|
||||
boolean hideCounts=!GlobalUserPreferences.showInteractionCounts;
|
||||
if((flags & FLAG_NO_HEADER)==0){
|
||||
if(status.reblog!=null){
|
||||
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.user_boosted, status.account.displayName), status.account.emojis, R.drawable.ic_repeat_wght700_20px));
|
||||
@@ -134,14 +134,14 @@ public abstract class StatusDisplayItem{
|
||||
SpoilerStatusDisplayItem spoilerItem=new SpoilerStatusDisplayItem(parentID, fragment, null, status, statusForContent, Type.SPOILER, Status.SpoilerType.CONTENT_WARNING);
|
||||
contentItems.add(spoilerItem);
|
||||
contentItems=spoilerItem.contentItems;
|
||||
if(!AccountSessionManager.get(accountID).getLocalPreferences().showCWs && !filtered){
|
||||
if(!GlobalUserPreferences.showCWs && !filtered){
|
||||
status.revealedSpoilers.add(Status.SpoilerType.CONTENT_WARNING);
|
||||
needAddCWItems=true;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -158,7 +158,7 @@ public abstract class StatusDisplayItem{
|
||||
if((flags & FLAG_MEDIA_FORCE_HIDDEN)!=0){
|
||||
mediaGrid.sensitiveTitle=fragment.getString(R.string.media_hidden);
|
||||
mediaGrid.sensitiveRevealed=false;
|
||||
}else if(statusForContent.sensitive && !AccountSessionManager.get(accountID).getLocalPreferences().hideSensitiveMedia){
|
||||
}else if(statusForContent.sensitive && !GlobalUserPreferences.hideSensitiveMedia){
|
||||
mediaGrid.sensitiveRevealed=true;
|
||||
}
|
||||
contentItems.add(mediaGrid);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -66,10 +66,10 @@ public abstract class AccountRestrictionConfirmationSheet extends BottomSheet{
|
||||
});
|
||||
}
|
||||
|
||||
protected void addRow(@DrawableRes int icon, CharSequence text){
|
||||
protected TextView addRow(@DrawableRes int icon, CharSequence text){
|
||||
TextView tv=new TextView(getContext());
|
||||
tv.setTextAppearance(R.style.m3_body_large);
|
||||
tv.setTextColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3OnSurfaceVariant));
|
||||
tv.setTextColor(UiUtils.getThemeColor(getContext(), R.attr.colorM3OnSurface));
|
||||
tv.setCompoundDrawableTintList(ColorStateList.valueOf(UiUtils.getThemeColor(getContext(), R.attr.colorM3Primary)));
|
||||
tv.setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
|
||||
tv.setText(text);
|
||||
@@ -78,6 +78,7 @@ public abstract class AccountRestrictionConfirmationSheet extends BottomSheet{
|
||||
tv.setCompoundDrawablesRelative(drawable, null, null, null);
|
||||
tv.setCompoundDrawablePadding(V.dp(16));
|
||||
contentWrap.addView(tv, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
return tv;
|
||||
}
|
||||
|
||||
protected void addRow(@DrawableRes int icon, @StringRes int text){
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
package org.joinmastodon.android.ui.sheets;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Typeface;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetDomainBlockPreview;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import me.grishka.appkit.api.APIRequest;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
public class BlockDomainConfirmationSheet extends AccountRestrictionConfirmationSheet{
|
||||
public BlockDomainConfirmationSheet(@NonNull Context context, Account user, ConfirmCallback confirmCallback, ConfirmCallback blockUserConfirmCallback){
|
||||
private APIRequest<?> currentRequest;
|
||||
|
||||
public BlockDomainConfirmationSheet(@NonNull Context context, Account user, ConfirmCallback confirmCallback, ConfirmCallback blockUserConfirmCallback, String accountID){
|
||||
super(context, user, confirmCallback);
|
||||
titleView.setText(R.string.block_domain_confirm_title);
|
||||
confirmBtn.setText(R.string.do_block_server);
|
||||
secondaryBtn.setText(context.getString(R.string.block_user_x_instead, user.getDisplayUsername()));
|
||||
icon.setImageResource(R.drawable.ic_domain_disabled_24px);
|
||||
subtitleView.setText(user.getDomain());
|
||||
TextView relationsRow=addRow(R.drawable.ic_person_remove_24px, "");
|
||||
addRow(R.drawable.ic_campaign_24px, R.string.users_cant_see_blocked);
|
||||
addRow(R.drawable.ic_visibility_off_24px, R.string.you_wont_see_server_posts);
|
||||
addRow(R.drawable.ic_person_remove_24px, R.string.server_followers_will_be_removed);
|
||||
addRow(R.drawable.ic_reply_24px, R.string.server_cant_mention_or_follow_you);
|
||||
addRow(R.drawable.ic_history_24px, R.string.server_can_interact_with_older);
|
||||
|
||||
@@ -32,5 +41,44 @@ public class BlockDomainConfirmationSheet extends AccountRestrictionConfirmation
|
||||
loading=false;
|
||||
});
|
||||
});
|
||||
|
||||
relationsRow.setVisibility(View.GONE);
|
||||
relationsRow.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
currentRequest=new GetDomainBlockPreview(user.getDomain())
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(GetDomainBlockPreview.Response result){
|
||||
currentRequest=null;
|
||||
if(result.followersCount>0 || result.followingCount>0){
|
||||
UiUtils.beginLayoutTransition(container);
|
||||
relationsRow.setVisibility(View.VISIBLE);
|
||||
if(result.followersCount>0 && result.followingCount>0){
|
||||
relationsRow.setText(context.getString(R.string.server_x_followers_and_following_will_be_removed,
|
||||
context.getResources().getQuantityString(R.plurals.will_lose_x_followers, result.followersCount, result.followersCount),
|
||||
context.getResources().getQuantityString(R.plurals.will_lose_x_following, result.followingCount, result.followingCount)));
|
||||
}else if(result.followersCount>0){
|
||||
relationsRow.setText(context.getResources().getQuantityString(R.plurals.server_x_followers_will_be_removed, result.followersCount, result.followersCount));
|
||||
}else{
|
||||
relationsRow.setText(context.getString(R.string.server_x_following_will_be_removed,
|
||||
context.getResources().getQuantityString(R.plurals.will_lose_x_following, result.followingCount, result.followingCount)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
currentRequest=null;
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dismiss(){
|
||||
if(currentRequest!=null){
|
||||
currentRequest.cancel();
|
||||
currentRequest=null;
|
||||
}
|
||||
super.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
@@ -57,7 +57,7 @@ public class NonMutualPreReplySheet extends PreReplySheet{
|
||||
name.setEllipsize(TextUtils.TruncateAt.END);
|
||||
name.setTextAppearance(R.style.m3_title_medium);
|
||||
name.setTextColor(UiUtils.getThemeColor(context, R.attr.colorM3OnSurface));
|
||||
if(AccountSessionManager.get(accountID).getLocalPreferences().customEmojiInNames){
|
||||
if(GlobalUserPreferences.customEmojiInNames){
|
||||
name.setText(HtmlParser.parseCustomEmoji(account.displayName, account.emojis));
|
||||
UiUtils.loadCustomEmojiInTextView(name);
|
||||
}else{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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++){
|
||||
|
||||
@@ -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;
|
||||
@@ -496,7 +501,7 @@ public class UiUtils{
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}).show();
|
||||
}, accountID).show();
|
||||
}else{
|
||||
new SetDomainBlocked(account.getDomain(), false)
|
||||
.setCallback(new Callback<>(){
|
||||
@@ -646,6 +651,9 @@ public class UiUtils{
|
||||
}
|
||||
|
||||
public static <T> void updateList(List<T> oldList, List<T> newList, RecyclerView list, RecyclerView.Adapter<?> adapter, BiPredicate<T, T> areItemsSame){
|
||||
RecyclerView.ItemAnimator animator=list.getItemAnimator();
|
||||
if(animator!=null)
|
||||
animator.endAnimations();
|
||||
// Save topmost item position and offset because for some reason RecyclerView would scroll the list to weird places when you insert items at the top
|
||||
int topItem, topItemOffset;
|
||||
if(list.getChildCount()==0){
|
||||
@@ -713,18 +721,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 +1130,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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){
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -28,7 +28,7 @@ public abstract class ListItemViewHolder<T extends ListItem<?>> extends Bindable
|
||||
title=findViewById(R.id.title);
|
||||
subtitle=findViewById(R.id.subtitle);
|
||||
icon=findViewById(R.id.icon);
|
||||
view=(LinearLayout) itemView;
|
||||
view=itemView instanceof LinearLayout ll ? ll : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -52,12 +52,7 @@ public abstract class ListItemViewHolder<T extends ListItem<?>> extends Bindable
|
||||
subtitle.setText(item.subtitle);
|
||||
}
|
||||
|
||||
if(item.iconRes!=0){
|
||||
icon.setVisibility(View.VISIBLE);
|
||||
icon.setImageResource(item.iconRes);
|
||||
}else{
|
||||
icon.setVisibility(View.GONE);
|
||||
}
|
||||
bindIcon(item);
|
||||
|
||||
if(item.colorOverrideAttr!=0){
|
||||
int color=UiUtils.getThemeColor(view.getContext(), item.colorOverrideAttr);
|
||||
@@ -68,6 +63,15 @@ public abstract class ListItemViewHolder<T extends ListItem<?>> extends Bindable
|
||||
view.setAlpha(item.isEnabled ? 1 : .4f);
|
||||
}
|
||||
|
||||
protected void bindIcon(T item){
|
||||
if(item.iconRes!=0){
|
||||
icon.setVisibility(View.VISIBLE);
|
||||
icon.setImageResource(item.iconRes);
|
||||
}else{
|
||||
icon.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled(){
|
||||
return item.isEnabled;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user