Compare commits

...

524 Commits

Author SHA1 Message Date
Grishka
0dc23789f5 Merge branch 'l10n_master' 2024-02-24 23:58:29 +03:00
Grishka
3f0ebd4aed Bump version 2024-02-24 23:58:09 +03:00
Grishka
1e501c707c Add labels and animations to the tab bar 2024-02-24 23:03:18 +03:00
Eugen Rochko
d57f97b492 New translations strings.xml (Chinese Traditional) 2024-02-24 17:03:43 +01:00
Eugen Rochko
29f9214869 New translations strings.xml (German) 2024-02-24 14:17:55 +01:00
Eugen Rochko
c6c1fe3595 New translations strings.xml (German) 2024-02-24 12:59:50 +01:00
Eugen Rochko
2ba4c6f443 New translations strings.xml (German) 2024-02-24 11:52:04 +01:00
Eugen Rochko
a9ff948818 New translations full_description.txt (Thai) 2024-02-24 09:44:12 +01:00
Eugen Rochko
7dd9cfa7f0 New translations strings.xml (Thai) 2024-02-24 08:35:56 +01:00
Eugen Rochko
15a415558b New translations strings.xml (Thai) 2024-02-24 07:18:50 +01:00
Grishka
b3e53bc48d AND-133 2024-02-24 03:57:43 +03:00
Grishka
d0e33c8a12 AND-127 2024-02-24 03:48:06 +03:00
Eugen Rochko
060db42e47 New translations strings.xml (Italian) 2024-02-24 01:31:16 +01:00
Grishka
6bcf259de9 AND-129 2024-02-24 03:20:22 +03:00
Grishka
4d0a673209 AND-130 2024-02-24 03:14:41 +03:00
Grishka
be9be6dc35 AND-126 2024-02-24 03:05:21 +03:00
Grishka
3f47497c12 AND-132 2024-02-24 03:04:35 +03:00
Eugen Rochko
268e0af7cb New translations strings.xml (Chinese Traditional) 2024-02-24 00:31:53 +01:00
Eugen Rochko
c611f723b7 New translations strings.xml (Italian) 2024-02-24 00:31:52 +01:00
Eugen Rochko
4c9f500122 New translations strings.xml (Basque) 2024-02-24 00:31:50 +01:00
Eugen Rochko
15147dddf5 New translations strings.xml (Basque) 2024-02-23 23:31:42 +01:00
Eugen Rochko
747c81c269 New translations strings.xml (Thai) 2024-02-23 21:31:03 +01:00
Eugen Rochko
63ad076046 New translations strings.xml (Urdu (India)) 2024-02-23 18:49:13 +01:00
Eugen Rochko
f109033ed2 New translations strings.xml (Kabyle) 2024-02-23 18:49:12 +01:00
Eugen Rochko
9445d3383a New translations strings.xml (Igbo) 2024-02-23 18:49:11 +01:00
Eugen Rochko
5db5537685 New translations strings.xml (Occitan) 2024-02-23 18:49:10 +01:00
Eugen Rochko
1149d1c37f New translations strings.xml (Scottish Gaelic) 2024-02-23 18:49:09 +01:00
Eugen Rochko
e4b77551f7 New translations strings.xml (Sinhala) 2024-02-23 18:49:08 +01:00
Eugen Rochko
401fd298f5 New translations strings.xml (Bosnian) 2024-02-23 18:49:08 +01:00
Eugen Rochko
6a1f7f7238 New translations strings.xml (Filipino) 2024-02-23 18:49:07 +01:00
Eugen Rochko
7c1c0894a8 New translations strings.xml (Burmese) 2024-02-23 18:49:06 +01:00
Eugen Rochko
cb96ae6cbc New translations strings.xml (Croatian) 2024-02-23 18:49:05 +01:00
Eugen Rochko
c15d972359 New translations strings.xml (Thai) 2024-02-23 18:49:04 +01:00
Eugen Rochko
c010e2371c New translations strings.xml (Bengali) 2024-02-23 18:49:03 +01:00
Eugen Rochko
f88a244594 New translations strings.xml (Indonesian) 2024-02-23 18:49:02 +01:00
Eugen Rochko
e2dccda205 New translations strings.xml (Portuguese, Brazilian) 2024-02-23 18:49:01 +01:00
Eugen Rochko
bc6c6bc9a5 New translations strings.xml (Icelandic) 2024-02-23 18:48:59 +01:00
Eugen Rochko
3281eabbe1 New translations strings.xml (Galician) 2024-02-23 18:48:58 +01:00
Eugen Rochko
92de091228 New translations strings.xml (Vietnamese) 2024-02-23 18:48:57 +01:00
Eugen Rochko
94ab8a4e8b New translations strings.xml (Chinese Traditional) 2024-02-23 18:48:56 +01:00
Eugen Rochko
799987205e New translations strings.xml (Chinese Simplified) 2024-02-23 18:48:55 +01:00
Eugen Rochko
f5dfe70ac6 New translations strings.xml (Ukrainian) 2024-02-23 18:48:54 +01:00
Eugen Rochko
69b03e2e59 New translations strings.xml (Turkish) 2024-02-23 18:48:53 +01:00
Eugen Rochko
588ebe11f4 New translations strings.xml (Swedish) 2024-02-23 18:48:52 +01:00
Eugen Rochko
4fe0d6e893 New translations strings.xml (Slovenian) 2024-02-23 18:48:51 +01:00
Eugen Rochko
787c20794f New translations strings.xml (Portuguese) 2024-02-23 18:48:50 +01:00
Eugen Rochko
c462e72279 New translations strings.xml (Norwegian) 2024-02-23 18:48:49 +01:00
Eugen Rochko
be7e6afce2 New translations strings.xml (Lithuanian) 2024-02-23 18:48:48 +01:00
Eugen Rochko
feb9920829 New translations strings.xml (Korean) 2024-02-23 18:48:47 +01:00
Eugen Rochko
4d6a1a705d New translations strings.xml (Georgian) 2024-02-23 18:48:46 +01:00
Eugen Rochko
59d54469dc New translations strings.xml (Japanese) 2024-02-23 18:48:45 +01:00
Eugen Rochko
9cccd28447 New translations strings.xml (Italian) 2024-02-23 18:48:44 +01:00
Eugen Rochko
da13b4602c New translations strings.xml (Armenian) 2024-02-23 18:48:43 +01:00
Eugen Rochko
2084a33192 New translations strings.xml (Hebrew) 2024-02-23 18:48:42 +01:00
Eugen Rochko
542ce40c1a New translations strings.xml (Irish) 2024-02-23 18:48:41 +01:00
Eugen Rochko
ddd4ea329c New translations strings.xml (Finnish) 2024-02-23 18:48:40 +01:00
Eugen Rochko
b5c3875ce9 New translations strings.xml (Basque) 2024-02-23 18:48:39 +01:00
Eugen Rochko
ec8387d9db New translations strings.xml (German) 2024-02-23 18:48:38 +01:00
Eugen Rochko
cf464e8eea New translations strings.xml (Danish) 2024-02-23 18:48:37 +01:00
Eugen Rochko
7b66f1c398 New translations strings.xml (Czech) 2024-02-23 18:48:36 +01:00
Eugen Rochko
814e11c1f4 New translations strings.xml (Arabic) 2024-02-23 18:48:35 +01:00
Eugen Rochko
647277444b New translations strings.xml (Spanish) 2024-02-23 18:48:34 +01:00
Eugen Rochko
bd4ff9c7ec New translations strings.xml (French) 2024-02-23 18:48:33 +01:00
Eugen Rochko
7ed6195cca New translations strings.xml (Romanian) 2024-02-23 18:48:32 +01:00
Eugen Rochko
43397f069a New translations strings.xml (Dutch) 2024-02-23 18:48:31 +01:00
Eugen Rochko
8e34e31b35 New translations strings.xml (Persian) 2024-02-23 18:48:30 +01:00
Eugen Rochko
7ef1bdfc2a New translations strings.xml (Catalan) 2024-02-23 18:48:29 +01:00
Eugen Rochko
726f91ec75 New translations strings.xml (Belarusian) 2024-02-23 18:48:28 +01:00
Eugen Rochko
55a5268a3c New translations strings.xml (Hindi) 2024-02-23 18:48:27 +01:00
Eugen Rochko
4777e3d0b7 New translations strings.xml (Greek) 2024-02-23 18:48:26 +01:00
Eugen Rochko
bfc970abe5 New translations strings.xml (Russian) 2024-02-23 18:48:24 +01:00
Eugen Rochko
d2d35f4f39 New translations strings.xml (Polish) 2024-02-23 18:48:23 +01:00
Eugen Rochko
49cef5ea0a New translations strings.xml (Hungarian) 2024-02-23 18:48:22 +01:00
Grishka
f2fbf55c53 AND-134 2024-02-23 20:45:17 +03:00
Grishka
eacfd2fa4f AND-125 2024-02-23 20:00:35 +03:00
Grishka
51f87848f4 AND-128 2024-02-23 19:36:37 +03:00
Grishka
c3f4637ddc AND-137 2024-02-23 19:31:23 +03:00
Grishka
2361394391 Share sheet previews (AND-139) 2024-02-23 19:25:21 +03:00
Eugen Rochko
38fdf8d53e New translations strings.xml (Italian) 2024-02-23 17:25:02 +01:00
Eugen Rochko
0cd3cab65d New translations strings.xml (Greek) 2024-02-23 15:31:39 +01:00
Eugen Rochko
142c8b55a1 New translations strings.xml (Hungarian) 2024-02-23 09:55:26 +01:00
Eugen Rochko
3274acaba8 New translations strings.xml (Hungarian) 2024-02-23 08:04:37 +01:00
Eugen Rochko
99b7d46ddf New translations full_description.txt (Armenian) 2024-02-23 06:54:22 +01:00
Eugen Rochko
4f7e45211f New translations strings.xml (Chinese Traditional) 2024-02-23 02:02:20 +01:00
Eugen Rochko
221773bf71 New translations strings.xml (Urdu (India)) 2024-02-22 23:36:54 +01:00
Eugen Rochko
305ce2ca42 New translations strings.xml (Kabyle) 2024-02-22 23:36:53 +01:00
Eugen Rochko
7dc2480400 New translations strings.xml (Igbo) 2024-02-22 23:36:52 +01:00
Eugen Rochko
9634ec6616 New translations strings.xml (Occitan) 2024-02-22 23:36:51 +01:00
Eugen Rochko
6f29540170 New translations strings.xml (Scottish Gaelic) 2024-02-22 23:36:50 +01:00
Eugen Rochko
18485c1cf8 New translations strings.xml (Sinhala) 2024-02-22 23:36:49 +01:00
Eugen Rochko
7c36c32e4b New translations strings.xml (Bosnian) 2024-02-22 23:36:48 +01:00
Eugen Rochko
1d0af51813 New translations strings.xml (Filipino) 2024-02-22 23:36:47 +01:00
Eugen Rochko
d50060d602 New translations strings.xml (Burmese) 2024-02-22 23:36:46 +01:00
Eugen Rochko
9d455b2331 New translations strings.xml (Croatian) 2024-02-22 23:36:45 +01:00
Eugen Rochko
5f3b537f7d New translations strings.xml (Thai) 2024-02-22 23:36:44 +01:00
Eugen Rochko
1d43ed4c8a New translations strings.xml (Bengali) 2024-02-22 23:36:43 +01:00
Eugen Rochko
e839323575 New translations strings.xml (Indonesian) 2024-02-22 23:36:42 +01:00
Eugen Rochko
7ee657012e New translations strings.xml (Portuguese, Brazilian) 2024-02-22 23:36:41 +01:00
Eugen Rochko
847fbed956 New translations strings.xml (Icelandic) 2024-02-22 23:36:40 +01:00
Eugen Rochko
a89d5df313 New translations strings.xml (Galician) 2024-02-22 23:36:39 +01:00
Eugen Rochko
5f58789008 New translations strings.xml (Vietnamese) 2024-02-22 23:36:38 +01:00
Eugen Rochko
7b64cad668 New translations strings.xml (Chinese Traditional) 2024-02-22 23:36:37 +01:00
Eugen Rochko
70f60f094d New translations strings.xml (Chinese Simplified) 2024-02-22 23:36:36 +01:00
Eugen Rochko
a5eff14552 New translations strings.xml (Ukrainian) 2024-02-22 23:36:35 +01:00
Eugen Rochko
36fb929387 New translations strings.xml (Turkish) 2024-02-22 23:36:34 +01:00
Eugen Rochko
66aabfb386 New translations strings.xml (Swedish) 2024-02-22 23:36:33 +01:00
Eugen Rochko
f503973db1 New translations strings.xml (Slovenian) 2024-02-22 23:36:32 +01:00
Eugen Rochko
7cec835509 New translations strings.xml (Portuguese) 2024-02-22 23:36:31 +01:00
Eugen Rochko
e300942455 New translations strings.xml (Norwegian) 2024-02-22 23:36:30 +01:00
Eugen Rochko
79476eff85 New translations strings.xml (Lithuanian) 2024-02-22 23:36:29 +01:00
Eugen Rochko
b4ec470691 New translations strings.xml (Korean) 2024-02-22 23:36:28 +01:00
Eugen Rochko
4f72a0c74e New translations strings.xml (Georgian) 2024-02-22 23:36:27 +01:00
Eugen Rochko
bcbb41aa43 New translations strings.xml (Japanese) 2024-02-22 23:36:27 +01:00
Eugen Rochko
5cc38d845a New translations strings.xml (Italian) 2024-02-22 23:36:26 +01:00
Eugen Rochko
36acc1588a New translations strings.xml (Armenian) 2024-02-22 23:36:25 +01:00
Eugen Rochko
349c5200ed New translations strings.xml (Hebrew) 2024-02-22 23:36:24 +01:00
Eugen Rochko
ff71f6a092 New translations strings.xml (Irish) 2024-02-22 23:36:23 +01:00
Eugen Rochko
8863284970 New translations strings.xml (Finnish) 2024-02-22 23:36:22 +01:00
Eugen Rochko
d5feb4e9f9 New translations strings.xml (Basque) 2024-02-22 23:36:21 +01:00
Eugen Rochko
4c284226e5 New translations strings.xml (German) 2024-02-22 23:36:20 +01:00
Eugen Rochko
fef9cf5e64 New translations strings.xml (Danish) 2024-02-22 23:36:19 +01:00
Eugen Rochko
93f3c9a9eb New translations strings.xml (Czech) 2024-02-22 23:36:18 +01:00
Eugen Rochko
5827c77b0c New translations strings.xml (Arabic) 2024-02-22 23:36:17 +01:00
Eugen Rochko
1b36866fba New translations strings.xml (Spanish) 2024-02-22 23:36:16 +01:00
Eugen Rochko
ebc10d5052 New translations strings.xml (French) 2024-02-22 23:36:15 +01:00
Eugen Rochko
8953fa48c7 New translations strings.xml (Romanian) 2024-02-22 23:36:14 +01:00
Eugen Rochko
59e7a296ca New translations strings.xml (Dutch) 2024-02-22 23:36:13 +01:00
Eugen Rochko
86432228a3 New translations strings.xml (Persian) 2024-02-22 23:36:12 +01:00
Eugen Rochko
4518566c37 New translations strings.xml (Catalan) 2024-02-22 23:36:11 +01:00
Eugen Rochko
b392a89350 New translations strings.xml (Belarusian) 2024-02-22 23:36:10 +01:00
Eugen Rochko
225682f35d New translations strings.xml (Hindi) 2024-02-22 23:36:09 +01:00
Eugen Rochko
b41ff2e18f New translations strings.xml (Greek) 2024-02-22 23:36:08 +01:00
Eugen Rochko
6ef76fb5bb New translations strings.xml (Russian) 2024-02-22 23:36:07 +01:00
Eugen Rochko
a36679b032 New translations strings.xml (Polish) 2024-02-22 23:36:06 +01:00
Eugen Rochko
dde91778a2 New translations strings.xml (Hungarian) 2024-02-22 23:36:05 +01:00
Grishka
1cdc6f4fcf Tweaks to QR code and media viewer 2024-02-23 01:20:24 +03:00
Grishka
b8a5346631 Crash fix 2024-02-22 23:15:03 +03:00
Grishka
5cf222379a QR codes for profiles 2024-02-22 21:35:46 +03:00
Eugen Rochko
8f4ff49b32 New translations strings.xml (Basque) 2024-02-21 19:34:39 +01:00
Eugen Rochko
3a68ca3cc0 New translations strings.xml (Basque) 2024-02-21 18:37:46 +01:00
Eugen Rochko
116dc68a38 New translations strings.xml (Basque) 2024-02-21 15:47:30 +01:00
Eugen Rochko
df84b0ac34 New translations strings.xml (Basque) 2024-02-21 14:12:43 +01:00
Eugen Rochko
5ef737766c New translations strings.xml (Lithuanian) 2024-02-21 13:00:51 +01:00
Eugen Rochko
be17ba870b New translations strings.xml (Basque) 2024-02-21 13:00:50 +01:00
Eugen Rochko
747b2d5801 New translations strings.xml (Lithuanian) 2024-02-21 11:13:33 +01:00
Eugen Rochko
b84c9bf948 New translations strings.xml (Icelandic) 2024-02-19 23:55:39 +01:00
Grishka
b1e999cc9c Fix crash in post language selector once again 2024-02-20 00:39:58 +03:00
Eugen Rochko
89951a8547 New translations strings.xml (Icelandic) 2024-02-19 22:29:02 +01:00
Eugen Rochko
1bb0ac1110 New translations strings.xml (Icelandic) 2024-02-19 14:05:01 +01:00
Eugen Rochko
5bb51901f7 New translations strings.xml (Czech) 2024-02-19 14:05:00 +01:00
Eugen Rochko
18562cd3ee New translations strings.xml (Icelandic) 2024-02-19 12:34:48 +01:00
Eugen Rochko
9e01270b1e New translations strings.xml (Persian) 2024-02-19 06:27:29 +01:00
Eugen Rochko
0b1ff9730c New translations strings.xml (Lithuanian) 2024-02-18 00:14:32 +01:00
Eugen Rochko
845cfde58e New translations strings.xml (Dutch) 2024-02-17 22:27:21 +01:00
Eugen Rochko
f81f264b37 New translations strings.xml (Dutch) 2024-02-17 21:28:35 +01:00
Eugen Rochko
5e14270c47 New translations strings.xml (Dutch) 2024-02-17 19:19:32 +01:00
Eugen Rochko
6d2427b336 New translations strings.xml (Dutch) 2024-02-17 18:24:02 +01:00
Eugen Rochko
d89562a4c0 New translations strings.xml (Dutch) 2024-02-17 17:15:00 +01:00
Eugen Rochko
08389b023d New translations strings.xml (Turkish) 2024-02-17 12:50:42 +01:00
Eugen Rochko
797c4d6baa New translations strings.xml (Greek) 2024-02-17 12:50:41 +01:00
Eugen Rochko
9458ddd490 New translations strings.xml (Vietnamese) 2024-02-17 05:24:24 +01:00
Eugen Rochko
8f3a8af35e New translations strings.xml (Vietnamese) 2024-02-17 04:26:46 +01:00
Eugen Rochko
d0f927c8d2 New translations strings.xml (Lithuanian) 2024-02-16 12:54:29 +01:00
Eugen Rochko
2de44c8d7f New translations strings.xml (Slovenian) 2024-02-15 23:54:13 +01:00
Eugen Rochko
9a3ab2f4d2 New translations strings.xml (Slovenian) 2024-02-15 22:29:25 +01:00
Eugen Rochko
7cecd689bb New translations strings.xml (Lithuanian) 2024-02-15 22:29:24 +01:00
Eugen Rochko
b79cf4e087 New translations strings.xml (Lithuanian) 2024-02-15 21:11:35 +01:00
Eugen Rochko
63a9ce6eb6 New translations strings.xml (Hindi) 2024-02-15 13:02:59 +01:00
Eugen Rochko
1f960e8631 New translations strings.xml (Hindi) 2024-02-15 11:34:35 +01:00
Eugen Rochko
6dc059646e New translations strings.xml (Italian) 2024-02-15 00:39:26 +01:00
Eugen Rochko
5b37db0f8e New translations strings.xml (Italian) 2024-02-14 23:35:53 +01:00
Eugen Rochko
2789dd9fd1 New translations strings.xml (Belarusian) 2024-02-14 15:35:06 +01:00
Eugen Rochko
a2a72a4aee New translations strings.xml (Belarusian) 2024-02-14 14:02:19 +01:00
Eugen Rochko
b79fc8132a New translations strings.xml (Japanese) 2024-02-14 06:46:50 +01:00
Eugen Rochko
91b8735a4c New translations strings.xml (Japanese) 2024-02-14 05:28:17 +01:00
Eugen Rochko
313d81ffe1 New translations strings.xml (Japanese) 2024-02-14 01:33:19 +01:00
Eugen Rochko
192b32c1c6 New translations strings.xml (Lithuanian) 2024-02-13 22:53:46 +01:00
Eugen Rochko
2ef8be7c59 New translations strings.xml (Lithuanian) 2024-02-13 21:49:15 +01:00
Eugen Rochko
94e8d5e6d9 New translations strings.xml (Thai) 2024-02-13 20:40:01 +01:00
Eugen Rochko
445600653e New translations strings.xml (Lithuanian) 2024-02-13 20:40:00 +01:00
Eugen Rochko
4349c7a9e7 New translations full_description.txt (Chinese Traditional) 2024-02-13 19:24:53 +01:00
Eugen Rochko
c2b2c39c8a New translations strings.xml (Thai) 2024-02-13 19:24:52 +01:00
Eugen Rochko
9dee6eea24 New translations strings.xml (Chinese Traditional) 2024-02-13 19:24:51 +01:00
Eugen Rochko
53355b31ea New translations strings.xml (Thai) 2024-02-13 17:47:14 +01:00
Eugen Rochko
c2f55675a8 New translations strings.xml (Thai) 2024-02-13 15:07:09 +01:00
Eugen Rochko
42f3c58d02 New translations strings.xml (Swedish) 2024-02-13 09:32:18 +01:00
Eugen Rochko
dac2c413d6 New translations strings.xml (Belarusian) 2024-02-13 09:32:17 +01:00
Grishka
8dffbff97c Domain badges & info sheet & my fanciest animation yet 2024-02-13 07:31:42 +03:00
Eugen Rochko
a539eb3768 New translations strings.xml (Chinese Traditional) 2024-02-13 05:00:33 +01:00
Eugen Rochko
481610cd10 New translations strings.xml (Belarusian) 2024-02-13 02:38:49 +01:00
Grishka
efb8cd565b Update locales 2024-02-12 21:40:28 +03:00
Eugen Rochko
fa78a0f6ca New translations strings.xml (Chinese Traditional) 2024-02-12 19:39:20 +01:00
Eugen Rochko
bcc96ff329 New translations strings.xml (Slovenian) 2024-02-12 19:39:16 +01:00
Eugen Rochko
bb3028fff6 New translations strings.xml (Japanese) 2024-02-12 19:39:12 +01:00
Eugen Rochko
3e66ce8949 New translations strings.xml (Spanish) 2024-02-12 19:39:04 +01:00
Grishka
1f5bdb975b Merge branch 'l10n_master' 2024-02-12 21:38:00 +03:00
Grishka
22dfc33974 Update strings 2024-02-12 21:37:44 +03:00
Eugen Rochko
5f89fb1e49 New translations strings.xml (Spanish) 2024-02-12 10:43:18 +01:00
Eugen Rochko
b5b3c2671a New translations strings.xml (Spanish) 2024-02-12 09:04:43 +01:00
Eugen Rochko
6a8c09c113 New translations strings.xml (Slovenian) 2024-02-11 22:10:41 +01:00
Eugen Rochko
9660a2a019 New translations strings.xml (Japanese) 2024-02-11 13:03:46 +01:00
Eugen Rochko
f667b657f6 New translations strings.xml (Japanese) 2024-02-11 11:07:22 +01:00
Eugen Rochko
9db309634e New translations strings.xml (Chinese Traditional) 2024-02-09 10:00:17 +01:00
Eugen Rochko
8a96762bcc New translations strings.xml (Chinese Traditional) 2024-02-09 08:08:32 +01:00
Grishka
6915d19fb4 fix 2024-02-09 03:38:00 +03:00
Eugen Rochko
d58b24722e New translations strings.xml (Kabyle) 2024-02-09 01:31:56 +01:00
Eugen Rochko
fe8904b7a5 New translations strings.xml (Scottish Gaelic) 2024-02-09 01:31:54 +01:00
Eugen Rochko
6f3404aac9 New translations strings.xml (Bosnian) 2024-02-09 01:31:52 +01:00
Eugen Rochko
9a81f720c2 New translations strings.xml (Filipino) 2024-02-09 01:31:51 +01:00
Eugen Rochko
3605ad4616 New translations strings.xml (Burmese) 2024-02-09 01:31:50 +01:00
Eugen Rochko
490ecfcb43 New translations strings.xml (Croatian) 2024-02-09 01:31:49 +01:00
Eugen Rochko
02b8ac55d5 New translations strings.xml (Thai) 2024-02-09 01:31:48 +01:00
Eugen Rochko
fd71f04ca5 New translations strings.xml (Bengali) 2024-02-09 01:31:47 +01:00
Eugen Rochko
091953ada8 New translations strings.xml (Indonesian) 2024-02-09 01:31:46 +01:00
Eugen Rochko
8707db891a New translations strings.xml (Portuguese, Brazilian) 2024-02-09 01:31:45 +01:00
Eugen Rochko
1c67cb5edb New translations strings.xml (Icelandic) 2024-02-09 01:31:44 +01:00
Eugen Rochko
a96567c329 New translations strings.xml (Galician) 2024-02-09 01:31:43 +01:00
Eugen Rochko
691372119a New translations strings.xml (Vietnamese) 2024-02-09 01:31:42 +01:00
Eugen Rochko
25734af54e New translations strings.xml (Chinese Traditional) 2024-02-09 01:31:41 +01:00
Eugen Rochko
b37f9abeae New translations strings.xml (Chinese Simplified) 2024-02-09 01:31:40 +01:00
Eugen Rochko
f502374533 New translations strings.xml (Ukrainian) 2024-02-09 01:31:39 +01:00
Eugen Rochko
f10da18272 New translations strings.xml (Turkish) 2024-02-09 01:31:38 +01:00
Eugen Rochko
940f2ca73f New translations strings.xml (Swedish) 2024-02-09 01:31:37 +01:00
Eugen Rochko
db3192e75a New translations strings.xml (Slovenian) 2024-02-09 01:31:36 +01:00
Eugen Rochko
1841568e7e New translations strings.xml (Portuguese) 2024-02-09 01:31:35 +01:00
Eugen Rochko
f4ce0e67ac New translations strings.xml (Norwegian) 2024-02-09 01:31:34 +01:00
Eugen Rochko
92b34f085e New translations strings.xml (Lithuanian) 2024-02-09 01:31:33 +01:00
Eugen Rochko
4e4eb05526 New translations strings.xml (Korean) 2024-02-09 01:31:32 +01:00
Eugen Rochko
4dc707871d New translations strings.xml (Georgian) 2024-02-09 01:31:31 +01:00
Eugen Rochko
e9562378b4 New translations strings.xml (Japanese) 2024-02-09 01:31:30 +01:00
Eugen Rochko
c1b9aa7826 New translations strings.xml (Italian) 2024-02-09 01:31:29 +01:00
Eugen Rochko
e979a348be New translations strings.xml (Armenian) 2024-02-09 01:31:28 +01:00
Eugen Rochko
89c5787ad6 New translations strings.xml (Finnish) 2024-02-09 01:31:26 +01:00
Eugen Rochko
f0b2329656 New translations strings.xml (Basque) 2024-02-09 01:31:25 +01:00
Eugen Rochko
6cf9969220 New translations strings.xml (German) 2024-02-09 01:31:24 +01:00
Eugen Rochko
1f37e7605e New translations strings.xml (Danish) 2024-02-09 01:31:23 +01:00
Eugen Rochko
cf6af6f912 New translations strings.xml (Czech) 2024-02-09 01:31:22 +01:00
Eugen Rochko
2c91adb03e New translations strings.xml (Arabic) 2024-02-09 01:31:21 +01:00
Eugen Rochko
911da90854 New translations strings.xml (Spanish) 2024-02-09 01:31:20 +01:00
Eugen Rochko
27f0235055 New translations strings.xml (French) 2024-02-09 01:31:19 +01:00
Eugen Rochko
f3764222d8 New translations strings.xml (Dutch) 2024-02-09 01:31:17 +01:00
Eugen Rochko
64cb8c4a9a New translations strings.xml (Persian) 2024-02-09 01:31:16 +01:00
Eugen Rochko
de78356f5a New translations strings.xml (Catalan) 2024-02-09 01:31:16 +01:00
Eugen Rochko
8a9d39397c New translations strings.xml (Belarusian) 2024-02-09 01:31:15 +01:00
Eugen Rochko
2d89fd0cf0 New translations strings.xml (Hindi) 2024-02-09 01:31:14 +01:00
Eugen Rochko
402620dbe4 New translations strings.xml (Greek) 2024-02-09 01:31:13 +01:00
Eugen Rochko
32776db395 New translations strings.xml (Russian) 2024-02-09 01:31:12 +01:00
Eugen Rochko
4523ab8a67 New translations strings.xml (Polish) 2024-02-09 01:31:11 +01:00
Eugen Rochko
760106bf5b New translations strings.xml (Hungarian) 2024-02-09 01:31:09 +01:00
Grishka
ad2ef39ace AND-122 Mute, block, and domain block confirmation screens 2024-02-09 03:27:05 +03:00
Eugen Rochko
f705afcafc New translations strings.xml (Dutch) 2024-02-08 23:16:32 +01:00
Eugen Rochko
3cff655e6f New translations strings.xml (Persian) 2024-02-08 14:34:47 +01:00
Eugen Rochko
ed86a5a3e8 New translations strings.xml (Russian) 2024-02-07 19:48:30 +01:00
Eugen Rochko
f329435f51 New translations strings.xml (Catalan) 2024-02-06 18:19:58 +01:00
Eugen Rochko
6a6a80bcd7 New translations strings.xml (Belarusian) 2024-02-03 17:27:48 +01:00
Eugen Rochko
62e4983f02 New translations strings.xml (Hindi) 2024-02-03 09:56:54 +01:00
Eugen Rochko
6dfd991e87 New translations strings.xml (Hindi) 2024-02-03 08:52:38 +01:00
Eugen Rochko
e205462bf4 New translations short_description.txt (Hindi) 2024-02-03 07:47:35 +01:00
Eugen Rochko
03f341f6f8 New translations full_description.txt (Hindi) 2024-02-03 07:47:34 +01:00
Eugen Rochko
b9b08c5ea7 New translations strings.xml (Hindi) 2024-02-03 07:47:33 +01:00
Eugen Rochko
2b5498ff5d New translations full_description.txt (Hindi) 2024-02-03 06:46:31 +01:00
Eugen Rochko
84b058873d New translations strings.xml (Hindi) 2024-02-03 06:46:30 +01:00
Eugen Rochko
fcf5c0822e New translations strings.xml (Hindi) 2024-02-03 05:07:26 +01:00
Eugen Rochko
53c3da6a3d New translations strings.xml (Hindi) 2024-02-02 18:19:31 +01:00
Eugen Rochko
68371c9a0f New translations strings.xml (Hindi) 2024-02-02 16:58:56 +01:00
Grishka
e7295aac07 Fix #770 2024-02-02 15:34:06 +01:00
Eugen Rochko
ae7f65954a New translations strings.xml (Hindi) 2024-02-02 15:30:16 +01:00
Grishka
350a73c3eb Fix ripple color on Android 14
closes #767
2024-02-02 15:23:52 +01:00
Eugen Rochko
66d8ba9b5d New translations strings.xml (Hungarian) 2024-01-26 21:49:50 +01:00
Eugen Rochko
f944b12f45 New translations strings.xml (Hungarian) 2024-01-26 20:20:12 +01:00
Eugen Rochko
61928a1cf0 New translations strings.xml (Greek) 2024-01-26 16:15:35 +01:00
Eugen Rochko
f06196802e New translations strings.xml (Greek) 2024-01-26 11:34:11 +01:00
Grishka
e162833ad7 Update fastlane screenshots (closes #633) 2024-01-26 13:28:43 +03:00
Eugen Rochko
936ffdc793 New translations strings.xml (Swedish) 2024-01-25 21:43:29 +01:00
Eugen Rochko
0bbf6abc0c New translations strings.xml (Russian) 2024-01-25 11:03:38 +01:00
Gregory K
5552dc2ac6 Merge pull request #769 from jixiaoyong/master
fix: NullPointerException crash while change post language twice
2024-01-25 11:14:52 +03:00
JI,XIAOYONG
a65d6fbeb3 fix: NullPointerException crash while change post language twice
Closes https://github.com/mastodon/mastodon-android/issues/766
2024-01-25 15:54:15 +08:00
Eugen Rochko
43612ffbc1 New translations strings.xml (Polish) 2024-01-24 20:55:45 +01:00
Eugen Rochko
971881bbd3 New translations strings.xml (Slovenian) 2024-01-21 13:00:18 +01:00
Eugen Rochko
390cc6b65d New translations strings.xml (Slovenian) 2024-01-21 12:01:14 +01:00
Eugen Rochko
ee31288769 New translations strings.xml (Thai) 2024-01-20 22:09:59 +01:00
Eugen Rochko
401986af29 New translations strings.xml (Galician) 2024-01-19 15:25:19 +01:00
Eugen Rochko
e41e89c5cd New translations strings.xml (Hungarian) 2024-01-19 15:25:18 +01:00
Eugen Rochko
53de0cfc63 New translations strings.xml (Polish) 2024-01-19 14:07:59 +01:00
Eugen Rochko
e68481395f New translations strings.xml (Polish) 2024-01-19 13:07:50 +01:00
Eugen Rochko
9a361e0688 New translations strings.xml (Czech) 2024-01-16 15:39:51 +01:00
Eugen Rochko
b8cce74824 New translations strings.xml (Portuguese) 2024-01-16 14:39:36 +01:00
Eugen Rochko
f1ad6fc511 New translations strings.xml (Hungarian) 2024-01-16 14:39:35 +01:00
Eugen Rochko
2aa4cc1a88 New translations strings.xml (Czech) 2024-01-16 14:39:34 +01:00
Eugen Rochko
fb17ba4777 New translations strings.xml (Portuguese, Brazilian) 2024-01-16 12:35:13 +01:00
Eugen Rochko
6e3c464c97 New translations strings.xml (Portuguese) 2024-01-16 12:35:12 +01:00
Eugen Rochko
640e5163a8 New translations strings.xml (Portuguese, Brazilian) 2024-01-16 10:41:19 +01:00
Eugen Rochko
fdd3f2f398 New translations strings.xml (Italian) 2024-01-16 00:28:25 +01:00
Eugen Rochko
dfc55a13b8 New translations strings.xml (Italian) 2024-01-15 23:26:28 +01:00
Eugen Rochko
5a83b79ac2 New translations strings.xml (Polish) 2024-01-15 01:45:23 +01:00
Eugen Rochko
7d954ab3c2 New translations strings.xml (Polish) 2024-01-15 00:43:45 +01:00
Eugen Rochko
ec0b830f4f New translations strings.xml (Polish) 2024-01-14 23:43:15 +01:00
Eugen Rochko
26256b67d3 New translations strings.xml (Polish) 2024-01-14 20:48:10 +01:00
Eugen Rochko
2f9d60b9c0 New translations strings.xml (Polish) 2024-01-14 19:51:03 +01:00
Eugen Rochko
499a325bc8 New translations strings.xml (Polish) 2024-01-14 18:07:35 +01:00
Eugen Rochko
97ca2634a0 New translations strings.xml (Lithuanian) 2024-01-12 16:34:20 +01:00
Eugen Rochko
6630f0f8da New translations strings.xml (Lithuanian) 2024-01-12 14:51:16 +01:00
Eugen Rochko
129b253176 New translations strings.xml (Armenian) 2024-01-11 20:43:52 +01:00
Eugen Rochko
c2382d065e New translations strings.xml (Armenian) 2024-01-11 19:46:15 +01:00
Eugen Rochko
085264755a New translations strings.xml (Chinese Traditional) 2024-01-08 14:54:56 +01:00
Eugen Rochko
baac955e52 New translations strings.xml (Chinese Traditional) 2024-01-08 13:30:18 +01:00
Eugen Rochko
4a0501209a New translations strings.xml (Turkish) 2024-01-07 21:08:49 +01:00
Eugen Rochko
e471b36d39 New translations strings.xml (Japanese) 2024-01-07 09:53:25 +01:00
Eugen Rochko
5c2961cf7c New translations strings.xml (Japanese) 2024-01-07 08:56:52 +01:00
Eugen Rochko
6e980f17c6 New translations strings.xml (Vietnamese) 2024-01-07 06:43:58 +01:00
Eugen Rochko
2860ce8755 New translations strings.xml (Vietnamese) 2024-01-07 05:25:19 +01:00
Eugen Rochko
ca25a868a0 New translations strings.xml (Thai) 2024-01-04 21:01:23 +01:00
Eugen Rochko
74f3bd5905 New translations strings.xml (Thai) 2024-01-04 19:45:26 +01:00
Eugen Rochko
e0a53b4296 New translations strings.xml (Slovenian) 2024-01-04 14:57:53 +01:00
Eugen Rochko
c20f043f38 New translations strings.xml (Icelandic) 2024-01-04 12:50:19 +01:00
Eugen Rochko
daf3005178 New translations strings.xml (Dutch) 2024-01-04 11:32:03 +01:00
Eugen Rochko
d17e24faae New translations strings.xml (Scottish Gaelic) 2024-01-03 22:18:18 +01:00
Eugen Rochko
0cd17accf9 New translations strings.xml (Thai) 2024-01-03 22:18:13 +01:00
Eugen Rochko
65f7b97e60 New translations strings.xml (Persian) 2024-01-03 22:18:11 +01:00
Eugen Rochko
c7324285f3 New translations strings.xml (Indonesian) 2024-01-03 22:18:10 +01:00
Eugen Rochko
6bc795ebea New translations strings.xml (Portuguese, Brazilian) 2024-01-03 22:18:09 +01:00
Eugen Rochko
f2616cdd58 New translations strings.xml (Galician) 2024-01-03 22:18:08 +01:00
Eugen Rochko
d50f65ffd8 New translations strings.xml (Vietnamese) 2024-01-03 22:18:07 +01:00
Eugen Rochko
b39b2d0544 New translations strings.xml (Chinese Traditional) 2024-01-03 22:18:06 +01:00
Eugen Rochko
cdaaa91bcc New translations strings.xml (Ukrainian) 2024-01-03 22:18:05 +01:00
Eugen Rochko
109dca0b8a New translations strings.xml (Turkish) 2024-01-03 22:18:03 +01:00
Eugen Rochko
ee87da564b New translations strings.xml (Swedish) 2024-01-03 22:18:02 +01:00
Eugen Rochko
b143559a0f New translations strings.xml (Russian) 2024-01-03 22:18:01 +01:00
Eugen Rochko
9b89727c80 New translations strings.xml (Polish) 2024-01-03 22:17:59 +01:00
Eugen Rochko
68a252c85c New translations strings.xml (Norwegian) 2024-01-03 22:17:58 +01:00
Eugen Rochko
d99cb91e89 New translations strings.xml (Lithuanian) 2024-01-03 22:17:57 +01:00
Eugen Rochko
38879cd2fe New translations strings.xml (Korean) 2024-01-03 22:17:56 +01:00
Eugen Rochko
af4d98a48b New translations strings.xml (Japanese) 2024-01-03 22:17:54 +01:00
Eugen Rochko
39bb93d650 New translations strings.xml (Italian) 2024-01-03 22:17:53 +01:00
Eugen Rochko
0a3568f424 New translations strings.xml (Hungarian) 2024-01-03 22:17:52 +01:00
Eugen Rochko
e0b45720f0 New translations strings.xml (Finnish) 2024-01-03 22:17:50 +01:00
Eugen Rochko
f5b7024bb5 New translations strings.xml (Basque) 2024-01-03 22:17:49 +01:00
Eugen Rochko
f1bfa1f598 New translations strings.xml (Greek) 2024-01-03 22:17:48 +01:00
Eugen Rochko
653304f9a4 New translations strings.xml (German) 2024-01-03 22:17:47 +01:00
Eugen Rochko
3d416a038a New translations strings.xml (Danish) 2024-01-03 22:17:46 +01:00
Eugen Rochko
e0eeb87182 New translations strings.xml (Czech) 2024-01-03 22:17:45 +01:00
Eugen Rochko
2570a86da9 New translations strings.xml (Belarusian) 2024-01-03 22:17:43 +01:00
Eugen Rochko
7b110f16b3 New translations strings.xml (Arabic) 2024-01-03 22:17:42 +01:00
Eugen Rochko
d170e87325 New translations strings.xml (Spanish) 2024-01-03 22:17:41 +01:00
Eugen Rochko
4a60f0c576 New translations strings.xml (Icelandic) 2024-01-03 22:17:39 +01:00
Eugen Rochko
4b5e9d604c New translations strings.xml (French) 2024-01-03 22:17:38 +01:00
Eugen Rochko
f9562d5087 New translations strings.xml (Chinese Simplified) 2024-01-03 22:17:37 +01:00
Eugen Rochko
786091c0a4 New translations strings.xml (Armenian) 2024-01-03 22:17:36 +01:00
Eugen Rochko
436b8240ef New translations strings.xml (Slovenian) 2024-01-03 22:17:35 +01:00
Eugen Rochko
e7253dcf97 New translations strings.xml (Dutch) 2024-01-03 22:17:34 +01:00
Grishka
48f9aabaf7 Support for invite links (AND-90) 2024-01-03 23:51:35 +03:00
Eugen Rochko
14d353ae27 New translations strings.xml (Icelandic) 2024-01-03 14:57:33 +01:00
Eugen Rochko
9a82846b84 New translations strings.xml (Icelandic) 2024-01-02 15:54:39 +01:00
Eugen Rochko
a4c9bbadc4 New translations strings.xml (Icelandic) 2024-01-02 14:01:52 +01:00
Eugen Rochko
fa70c55084 New translations strings.xml (French) 2023-12-30 16:27:55 +01:00
Eugen Rochko
8d0a89fb06 New translations strings.xml (Chinese Simplified) 2023-12-29 18:47:34 +01:00
Eugen Rochko
3caf6cb94c New translations strings.xml (Chinese Simplified) 2023-12-29 17:46:24 +01:00
Eugen Rochko
f4854061ea New translations strings.xml (Armenian) 2023-12-27 21:54:36 +01:00
Eugen Rochko
bf7607674e New translations strings.xml (Armenian) 2023-12-27 20:06:13 +01:00
Eugen Rochko
137a8ca27b New translations strings.xml (Slovenian) 2023-12-21 22:08:11 +01:00
Eugen Rochko
b9ed4e0ee2 New translations strings.xml (Dutch) 2023-12-21 15:36:39 +01:00
Eugen Rochko
bc04672d32 New translations strings.xml (Dutch) 2023-12-21 12:26:19 +01:00
Eugen Rochko
70c668ecf1 New translations strings.xml (Polish) 2023-12-17 22:13:43 +01:00
Eugen Rochko
64bbe2c438 New translations strings.xml (Polish) 2023-12-17 21:16:34 +01:00
Eugen Rochko
32209e766e New translations strings.xml (Greek) 2023-12-17 11:29:11 +01:00
Eugen Rochko
99349cff0a New translations strings.xml (Persian) 2023-12-17 09:35:27 +01:00
Eugen Rochko
74ca1961e0 New translations strings.xml (Spanish) 2023-12-17 03:38:08 +01:00
Eugen Rochko
ef6ba7fe0c New translations strings.xml (Spanish) 2023-12-17 02:07:50 +01:00
Eugen Rochko
9ace2b71cc New translations strings.xml (Spanish) 2023-12-16 22:08:55 +01:00
Eugen Rochko
0c54654b8b New translations strings.xml (Basque) 2023-12-16 20:25:55 +01:00
Eugen Rochko
bf686309fb New translations strings.xml (Basque) 2023-12-16 18:48:35 +01:00
Eugen Rochko
ce4f46537b New translations strings.xml (Spanish) 2023-12-16 17:44:05 +01:00
Eugen Rochko
4c43207f17 New translations strings.xml (Spanish) 2023-12-16 16:46:39 +01:00
Eugen Rochko
afe5bcd1f3 New translations strings.xml (Portuguese, Brazilian) 2023-12-15 17:01:27 +01:00
Eugen Rochko
3bda81bd43 New translations strings.xml (Portuguese, Brazilian) 2023-12-15 16:02:20 +01:00
Eugen Rochko
7339b2325f New translations strings.xml (Armenian) 2023-12-15 12:30:11 +01:00
Eugen Rochko
ee84a9ee7e New translations strings.xml (Armenian) 2023-12-15 11:09:33 +01:00
Eugen Rochko
fef594150a New translations strings.xml (Turkish) 2023-12-13 17:09:47 +01:00
Eugen Rochko
10371f69cb New translations strings.xml (Turkish) 2023-12-13 15:52:37 +01:00
Eugen Rochko
75cf3d76fb New translations strings.xml (Dutch) 2023-12-12 16:01:19 +01:00
Eugen Rochko
51a7d00c47 New translations strings.xml (Lithuanian) 2023-12-11 15:13:33 +01:00
Eugen Rochko
9ac8261cc4 New translations strings.xml (Lithuanian) 2023-12-11 10:21:28 +01:00
Eugen Rochko
1f4ad80b7d New translations strings.xml (Lithuanian) 2023-12-11 07:10:38 +01:00
Eugen Rochko
4b090f0d68 New translations strings.xml (Lithuanian) 2023-12-10 20:30:16 +01:00
Eugen Rochko
4002bcde26 New translations strings.xml (Lithuanian) 2023-12-10 10:29:09 +01:00
Eugen Rochko
ded3777b40 New translations strings.xml (Lithuanian) 2023-12-10 09:10:25 +01:00
Eugen Rochko
7236066003 New translations strings.xml (Vietnamese) 2023-12-10 03:30:15 +01:00
Eugen Rochko
033f07ea09 New translations strings.xml (Lithuanian) 2023-12-09 22:16:10 +01:00
Eugen Rochko
283c0cba4b New translations strings.xml (Lithuanian) 2023-12-09 21:05:47 +01:00
Eugen Rochko
e3a1fc2fbb New translations strings.xml (Lithuanian) 2023-12-09 19:50:11 +01:00
Eugen Rochko
95de9e2917 New translations strings.xml (Lithuanian) 2023-12-09 16:56:00 +01:00
Eugen Rochko
a82ebeed11 New translations strings.xml (Basque) 2023-12-09 15:50:00 +01:00
Eugen Rochko
3a3aa0be1c New translations strings.xml (Lithuanian) 2023-12-09 07:31:00 +01:00
Eugen Rochko
e72491c2d1 New translations strings.xml (Lithuanian) 2023-12-08 19:29:39 +01:00
Eugen Rochko
36dede1f93 New translations strings.xml (Lithuanian) 2023-12-08 16:59:20 +01:00
Eugen Rochko
ed15daf9e9 New translations strings.xml (Lithuanian) 2023-12-08 15:26:26 +01:00
Eugen Rochko
c6052c841d New translations strings.xml (Lithuanian) 2023-12-08 14:07:22 +01:00
Eugen Rochko
ce39c7ca8f New translations strings.xml (Lithuanian) 2023-12-08 10:16:11 +01:00
Eugen Rochko
b7723dcb98 New translations strings.xml (Lithuanian) 2023-12-08 08:50:52 +01:00
Eugen Rochko
ad0774f8a5 New translations strings.xml (Armenian) 2023-12-07 22:59:36 +01:00
Eugen Rochko
9172feb72b New translations strings.xml (Italian) 2023-12-07 21:21:39 +01:00
Eugen Rochko
a297bd3281 New translations strings.xml (Lithuanian) 2023-12-07 16:52:37 +01:00
Eugen Rochko
e713a9cfc3 New translations strings.xml (Lithuanian) 2023-12-07 15:46:22 +01:00
Eugen Rochko
195395a22d New translations strings.xml (Lithuanian) 2023-12-07 14:14:31 +01:00
Eugen Rochko
7b6a62b047 New translations strings.xml (Lithuanian) 2023-12-07 13:14:37 +01:00
Eugen Rochko
ada1c9ff6d New translations strings.xml (Lithuanian) 2023-12-07 12:05:29 +01:00
Eugen Rochko
5a0a14ed56 New translations strings.xml (Lithuanian) 2023-12-07 09:11:41 +01:00
Eugen Rochko
cad3879646 New translations strings.xml (Galician) 2023-12-07 07:52:19 +01:00
Eugen Rochko
5d961991d4 New translations strings.xml (Lithuanian) 2023-12-06 22:10:19 +01:00
Eugen Rochko
e27536743f New translations strings.xml (Czech) 2023-12-06 22:10:18 +01:00
Eugen Rochko
9f8d4a0f34 New translations strings.xml (Lithuanian) 2023-12-06 20:56:46 +01:00
Eugen Rochko
67b6a89fd9 New translations strings.xml (Swedish) 2023-12-06 20:56:45 +01:00
Eugen Rochko
dabc4058ba New translations strings.xml (Lithuanian) 2023-12-06 18:59:35 +01:00
Eugen Rochko
6c468602c6 New translations full_description.txt (Lithuanian) 2023-12-06 17:20:30 +01:00
Eugen Rochko
9c5d29a860 New translations strings.xml (Lithuanian) 2023-12-06 17:20:29 +01:00
Eugen Rochko
da5e2a6b50 New translations short_description.txt (Lithuanian) 2023-12-06 16:23:00 +01:00
Eugen Rochko
a194569fd4 New translations full_description.txt (Lithuanian) 2023-12-06 16:22:58 +01:00
Eugen Rochko
78a4ace9b2 New translations strings.xml (Lithuanian) 2023-12-06 14:26:32 +01:00
Eugen Rochko
9a664088cd New translations strings.xml (Lithuanian) 2023-12-06 13:02:34 +01:00
Eugen Rochko
1d2e6f880b New translations strings.xml (Lithuanian) 2023-12-06 11:26:57 +01:00
Eugen Rochko
2cd2918d53 New translations strings.xml (Japanese) 2023-12-06 11:26:56 +01:00
Eugen Rochko
9b49db6677 New translations strings.xml (French) 2023-12-06 08:58:00 +01:00
Eugen Rochko
9f6c61e5c0 New translations title.txt (Lithuanian) 2023-12-05 21:30:20 +01:00
Eugen Rochko
b6a2bb7881 New translations short_description.txt (Lithuanian) 2023-12-05 21:30:19 +01:00
Eugen Rochko
62262010b9 New translations full_description.txt (Lithuanian) 2023-12-05 21:30:18 +01:00
Eugen Rochko
72fe9a04a6 New translations strings.xml (Lithuanian) 2023-12-05 21:30:17 +01:00
Eugen Rochko
d8cf55ae21 New translations strings.xml (Greek) 2023-12-05 17:58:41 +01:00
Eugen Rochko
dfb393b934 New translations strings.xml (Chinese Traditional) 2023-12-05 07:46:21 +01:00
Eugen Rochko
cd27716f6a New translations strings.xml (Russian) 2023-12-05 07:46:20 +01:00
Eugen Rochko
469553b34e New translations strings.xml (Thai) 2023-12-05 06:46:35 +01:00
Grishka
5d7c37262e Info sheet in media viewer (AND-109) 2023-12-04 21:33:25 +03:00
Eugen Rochko
3f3867473f New translations strings.xml (Czech) 2023-12-04 11:21:21 +01:00
Grishka
b08cd1eb4b Crash fixes 2023-12-04 06:22:21 +03:00
Grishka
1f9ff8d341 Increase timeout to 60 seconds 2023-12-03 21:38:00 +03:00
Eugen Rochko
528b362f64 New translations strings.xml (Thai) 2023-12-03 09:24:19 +01:00
Eugen Rochko
1db10c5047 New translations strings.xml (Thai) 2023-12-03 08:16:34 +01:00
Eugen Rochko
f295f5f4e7 New translations strings.xml (Japanese) 2023-12-02 17:44:51 +01:00
Eugen Rochko
08924bd9b0 New translations strings.xml (Russian) 2023-12-02 15:31:03 +01:00
Eugen Rochko
5d432435a1 New translations strings.xml (Russian) 2023-12-02 14:26:43 +01:00
Eugen Rochko
8bd76aa833 New translations strings.xml (Italian) 2023-12-02 00:37:04 +01:00
Eugen Rochko
2147cb87ac New translations strings.xml (Slovenian) 2023-12-01 18:50:27 +01:00
Eugen Rochko
00ed0f5402 New translations strings.xml (Galician) 2023-12-01 16:15:43 +01:00
Eugen Rochko
870f79f6cd New translations strings.xml (Galician) 2023-12-01 14:51:25 +01:00
Eugen Rochko
da879213fc New translations strings.xml (Greek) 2023-11-30 23:34:22 +01:00
Eugen Rochko
db66974bd6 New translations strings.xml (Chinese Traditional) 2023-11-30 02:07:51 +01:00
Eugen Rochko
e3d5ae1d65 New translations strings.xml (Thai) 2023-11-29 20:48:42 +01:00
Eugen Rochko
b06bc5b3b7 New translations strings.xml (Russian) 2023-11-29 11:46:16 +01:00
Eugen Rochko
a4c988012d New translations strings.xml (Georgian) 2023-11-29 09:16:14 +01:00
Grishka
a200701e4c Link card improvements (AND-115)
closes #651
2023-11-29 07:51:11 +03:00
Eugen Rochko
e8f604792c New translations strings.xml (Chinese Traditional) 2023-11-29 03:02:35 +01:00
Eugen Rochko
c8b0666ef9 New translations strings.xml (Scottish Gaelic) 2023-11-29 00:35:44 +01:00
Eugen Rochko
13aa72b150 New translations strings.xml (Thai) 2023-11-29 00:35:39 +01:00
Eugen Rochko
6694074b18 New translations strings.xml (Persian) 2023-11-29 00:35:37 +01:00
Eugen Rochko
63aa32c636 New translations strings.xml (Indonesian) 2023-11-29 00:35:36 +01:00
Eugen Rochko
5fbab870c3 New translations strings.xml (Portuguese, Brazilian) 2023-11-29 00:35:35 +01:00
Eugen Rochko
4a34e248e0 New translations strings.xml (Icelandic) 2023-11-29 00:35:34 +01:00
Eugen Rochko
2c45165e53 New translations strings.xml (Galician) 2023-11-29 00:35:33 +01:00
Eugen Rochko
3f029ac45b New translations strings.xml (Chinese Traditional) 2023-11-29 00:35:32 +01:00
Eugen Rochko
a4cf76d5ba New translations strings.xml (Chinese Simplified) 2023-11-29 00:35:31 +01:00
Eugen Rochko
3044000cf8 New translations strings.xml (Ukrainian) 2023-11-29 00:35:30 +01:00
Eugen Rochko
ab1ef5cfd8 New translations strings.xml (Turkish) 2023-11-29 00:35:29 +01:00
Eugen Rochko
16b91a283a New translations strings.xml (Swedish) 2023-11-29 00:35:28 +01:00
Eugen Rochko
e9fbdc21fa New translations strings.xml (Slovenian) 2023-11-29 00:35:27 +01:00
Eugen Rochko
b429e662aa New translations strings.xml (Russian) 2023-11-29 00:35:26 +01:00
Eugen Rochko
834ad1736e New translations strings.xml (Polish) 2023-11-29 00:35:24 +01:00
Eugen Rochko
91021699d2 New translations strings.xml (Norwegian) 2023-11-29 00:35:23 +01:00
Eugen Rochko
0f86aa12ab New translations strings.xml (Japanese) 2023-11-29 00:35:21 +01:00
Eugen Rochko
fb7bf6f308 New translations strings.xml (Italian) 2023-11-29 00:35:20 +01:00
Eugen Rochko
5aa67aaa78 New translations strings.xml (Hungarian) 2023-11-29 00:35:19 +01:00
Eugen Rochko
2e892e7305 New translations strings.xml (Finnish) 2023-11-29 00:35:16 +01:00
Eugen Rochko
6486a1689f New translations strings.xml (Basque) 2023-11-29 00:35:15 +01:00
Eugen Rochko
5966535111 New translations strings.xml (German) 2023-11-29 00:35:14 +01:00
Eugen Rochko
a2cf4bda99 New translations strings.xml (Danish) 2023-11-29 00:35:13 +01:00
Eugen Rochko
7a93c8615d New translations strings.xml (Arabic) 2023-11-29 00:35:12 +01:00
Eugen Rochko
cf29f11cea New translations strings.xml (Spanish) 2023-11-29 00:35:10 +01:00
Eugen Rochko
23188a26d7 New translations strings.xml (Belarusian) 2023-11-29 00:35:08 +01:00
Eugen Rochko
0480dc0140 New translations strings.xml (French) 2023-11-29 00:35:07 +01:00
Eugen Rochko
cb14b29c78 New translations strings.xml (Armenian) 2023-11-29 00:35:06 +01:00
Eugen Rochko
bf68272de3 New translations strings.xml (Greek) 2023-11-29 00:35:05 +01:00
Eugen Rochko
730f5f8cc9 New translations strings.xml (Dutch) 2023-11-29 00:35:03 +01:00
Eugen Rochko
4b6d328e3d New translations strings.xml (Czech) 2023-11-29 00:35:02 +01:00
Eugen Rochko
cfde38be2d New translations strings.xml (Vietnamese) 2023-11-29 00:35:01 +01:00
Grishka
a2ea8e76fb Improve follow recommendations screen (AND-101) 2023-11-29 02:09:59 +03:00
Grishka
e797d8a1c2 Revert "Update icon"
This reverts commit b58c157c87.
2023-11-29 01:26:51 +03:00
Grishka
b58c157c87 Update icon 2023-11-29 01:22:27 +03:00
Eugen Rochko
58f746a285 New translations strings.xml (Georgian) 2023-11-28 10:52:56 +01:00
Eugen Rochko
a6bba42a49 New translations title.txt (Georgian) 2023-11-28 07:16:50 +01:00
Eugen Rochko
519d6868b2 New translations short_description.txt (Georgian) 2023-11-28 07:16:49 +01:00
Eugen Rochko
5322120097 New translations full_description.txt (Georgian) 2023-11-28 07:16:48 +01:00
Eugen Rochko
2c88c86480 New translations strings.xml (Georgian) 2023-11-28 07:16:47 +01:00
Eugen Rochko
55f32fd45b New translations strings.xml (Dutch) 2023-11-28 01:12:21 +01:00
Eugen Rochko
f39f0b03d1 New translations strings.xml (Dutch) 2023-11-27 23:54:03 +01:00
Eugen Rochko
ff2f1a4955 New translations strings.xml (Dutch) 2023-11-27 21:48:52 +01:00
Grishka
b283e216a7 Increase default HTTP timeouts to 30 seconds
fixes #751
2023-11-27 20:30:33 +03:00
Eugen Rochko
4328d568b3 New translations strings.xml (Belarusian) 2023-11-27 15:23:31 +01:00
Eugen Rochko
8edc47703f New translations strings.xml (Dutch) 2023-11-27 14:08:47 +01:00
Eugen Rochko
92ce906163 New translations strings.xml (Dutch) 2023-11-27 05:46:48 +01:00
Eugen Rochko
6e141e360e New translations strings.xml (French) 2023-11-25 22:27:46 +01:00
Grishka
d1aba87e13 Fix #747 2023-11-25 03:38:57 +03:00
Grishka
723853079e Fix "go to account" in search 2023-11-25 03:35:21 +03:00
Eugen Rochko
cd0742c093 New translations strings.xml (Armenian) 2023-11-24 23:26:11 +01:00
Eugen Rochko
52d5de5aec New translations strings.xml (Dutch) 2023-11-24 23:26:10 +01:00
Eugen Rochko
4f8a5ae5db New translations strings.xml (Dutch) 2023-11-24 21:58:52 +01:00
Eugen Rochko
616f2463c7 New translations strings.xml (Dutch) 2023-11-24 20:51:33 +01:00
Eugen Rochko
d3b711a966 New translations strings.xml (Greek) 2023-11-24 19:55:43 +01:00
Eugen Rochko
827fe34709 New translations strings.xml (Dutch) 2023-11-24 19:55:41 +01:00
Eugen Rochko
4b6c0242d5 New translations strings.xml (Dutch) 2023-11-24 18:54:54 +01:00
Eugen Rochko
c3cbc16084 New translations strings.xml (Czech) 2023-11-23 18:37:42 +01:00
Eugen Rochko
36493bfc88 New translations strings.xml (Vietnamese) 2023-11-23 18:37:41 +01:00
Eugen Rochko
66ce93a3ff New translations strings.xml (Vietnamese) 2023-11-23 16:34:34 +01:00
Grishka
957bc76dbb Merge branch 'l10n_master' 2023-11-23 00:27:58 +03:00
Grishka
1f5a28fb33 Add top margin to pre-reply shits 2023-11-23 00:27:24 +03:00
Grishka
045c58ce66 fix color 2023-11-22 18:26:56 +03:00
Grishka
e2dde7239f Render custom emojis in non-mutual pre-reply sheet 2023-11-22 18:06:14 +03:00
Eugen Rochko
512ad93eea New translations strings.xml (Turkish) 2023-11-22 00:00:30 +01:00
Grishka
19759023a4 Bump version 2023-11-21 22:34:51 +03:00
Grishka
714d3399ce Merge branch 'l10n_master' 2023-11-21 22:34:30 +03:00
Grishka
e1850e5282 Validate timezone and locale against what server supports
closes #654, AND-118
2023-11-21 22:31:26 +03:00
Grishka
a05c917b2c Assorted crash fixes 2023-11-21 21:46:41 +03:00
Grishka
8f3a9c265c Fix a rare crash when opening a notification 2023-11-21 21:27:43 +03:00
Grishka
6f1a33b76e Fix #741 2023-11-21 21:17:30 +03:00
Eugen Rochko
8f0451175f New translations strings.xml (Japanese) 2023-11-21 13:38:37 +01:00
Eugen Rochko
37a3a4f1c0 New translations strings.xml (Chinese Simplified) 2023-11-21 05:53:58 +01:00
Eugen Rochko
bd85746726 New translations strings.xml (Chinese Simplified) 2023-11-21 04:58:34 +01:00
249 changed files with 10701 additions and 2246 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 844 KiB

After

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 776 KiB

After

Width:  |  Height:  |  Size: 748 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 790 KiB

After

Width:  |  Height:  |  Size: 722 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -1,16 +1,16 @@
Mastodon is the largest decentralized social network on the internet. Instead of a single website, its a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what youre into, you can meet passionate people posting about it on Mastodon!
Mastodon इंटरनेट का सबसे बड़ा डिसेंट्रलाइज़्ड सोशल नेटवर्क है। एक सिंगल वेबसाइट के जगह, ये हज़ारों आज़ाद ग्रुपों के लाखों यूज़रों का एक नेटवर्क है जो एक दूसरे से आसानी से बात करते है। चाहे आपकी जो भी दिलचस्पी हो, आपको उसके ऊपर पोस्ट करनेवाले लोग Mastodon पे ज़रूर मिलेंगे!
Join a community and create your profile. Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
कोई ग्रुप जॉइन करें और अपना प्रोफाइल बनाएं। दिलचस्प लोगों को ढूंढ़ें और फॉलो करें और उनके पोस्ट पढ़ें बिना किसी ऐड के। Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
More features:
एक्स्ट्रा फीचर:
Dark Mode: Read posts in light, dark, or true black mode
डार्क मोड: पोस्ट लाइट, डार्क, या प्योर ब्लैक मोड में पढ़ें
• Polls: Ask followers for their opinion and tally the votes
• Explore: Trending hashtags and accounts are a tap away
• Notifications: Get notified about new follows, replies, and reblogs
• Sharing: Post directly to Mastodon from any share sheet in any app
Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
क्यूटपन: हमारा मैस्कॉट एक प्यारा हाथी है, और आप उसे समय-समय पे देखेंगे
Mastodon is a registered nonprofit and development is supported directly by your donations. Theres no advertising, no monetization, and no venture capital, and we plan to keep it that way.

View File

@@ -1 +1 @@
Decentralized social network
डिसेंट्रलाइज़्ड सोशल नेटवर्क

View File

@@ -4,12 +4,12 @@
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Զգայուն կամ հրահրող թեմաներով գրառումները կարելի է թաքցնել նախազգուշացումներով։ Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
Ավելին՝
Ավելին.
• Մուգ տարբերակ՝ կարդացեք գրառումներ մուգ, բաց կամ իսկական սև տարբերակներում
• Հարցումներ՝ իմացեք ձեր հետևորդների կարծիքը և հաշվեք ձայները
• Մուգ տարբերակ. կարդացեք գրառումներ մուգ, բաց կամ իսկական սև տարբերակներում
• Հարցումներ. իմացեք ձեր հետևորդների կարծիքը և հաշվեք ձայները
• Explore: Trending hashtags and accounts are a tap away
Notifications: Get notified about new follows, replies, and reblogs
Հիշեցումներ` ստացեք հիշեցումներ նոր հետևումների, մեկնաբանությունների և կիսումների մասին
• Sharing: Post directly to Mastodon from any share sheet in any app
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time

View File

@@ -0,0 +1,16 @@
Mastodon is the largest decentralized social network on the internet. Instead of a single website, its a network of millions of users in independent communities that can all interact with one another, seamlessly. No matter what youre into, you can meet passionate people posting about it on Mastodon!
Join a community and create your profile. Find and follow fascinating folks and read their posts in an ad-free, chronological timeline. Express yourself with custom emoji, images, GIFs, videos, and audio in 500-character posts. Reply to threads and reblog posts from anyone to share great stuff. Find new accounts to follow and trending hashtags to expand your network.
Mastodon is built with a focus on privacy and safety. Decide whether your posts are shared with your followers, just the people you mention, or the whole world. Content warnings let you hide posts containing sensitive or triggering material until you're ready to engage with them. Each community has its own guidelines and moderators to keep its members safe, and robust blocking and reporting tools help prevent abuse.
More features:
• Dark Mode: Read posts in light, dark, or true black mode
• Polls: Ask followers for their opinion and tally the votes
• Explore: Trending hashtags and accounts are a tap away
• Notifications: Get notified about new follows, replies, and reblogs
• Sharing: Post directly to Mastodon from any share sheet in any app
• Cuteness: Our mascot is an adorable elephant, and you'll see them pop up from time to time
Mastodon is a registered nonprofit and development is supported directly by your donations. Theres no advertising, no monetization, and no venture capital, and we plan to keep it that way.

View File

@@ -0,0 +1 @@
Decentralized social network

View File

@@ -0,0 +1 @@
Mastodon

View File

@@ -0,0 +1,16 @@
Mastodon didžiausias decentralizuotas socialinis tinklas internete. Vietoj vienos svetainės tai yra milijonų naudotojų, priklausančių nepriklausomoms bendruomenėms, kurios gali sklandžiai bendrauti tarpusavyje, tinklas. Nesvarbu, kuo domiesi, Mastodon gali sutikti aistringų žmonių, skelbiančių apie tai!
Prisijunk prie bendruomenės ir susikurk savo profilį. Rask ir sek žavius žmones bei skaityk jų įrašus chronologinėje laiko skalėje be reklamų. Išreikšk save su pasirinktais jaustukais, vaizdais, GIF, vaizdo ir garso įrašais 500 simbolių įrašuose. Atsakyk į gijas ir perrašyk bet kurio asmens įrašus, kad galėtum dalytis puikiais dalykais. Ieškok naujų paskyrų sekti ir tendencingų saitažodžių, kad praplėstum savo tinklą.
Mastodon sukurtas daugiausia dėmesio skiriant privatumui ir saugumui. Nuspręsk, ar tavo įrašai bus bendrinami tavo sekėjams, tik tavo paminėtiems žmonėms, ar visam pasauliui. Turinio įspėjimai leidžia paslėpti įrašus, kuriuose yra jautrios ar dirginančios medžiagos, kol būsi pasiruošęs (-usi) su jais bendrauti. Kiekviena bendruomenė turi savo gaires ir prižiūrėtojus, kad jos nariai būtų saugūs, o patikimi blokavimo ir pranešimo įrankiai padeda užkirsti kelią piktnaudžiavimui.
Daugiau funkcijų:
• Tamsusis režimas: skaityk įrašus šviesiu, tamsiu arba tikru juodu režimu
• Apklausos: paklausk sekėjų nuomonės ir suskaičiuok balsus
• Naršyti: tendencingos saitažodžiai ir paskyros vos nuo vieno prisilietimo
• Pranešimai: gauk pranešimus apie naujus sekėjus, atsakymus ir tinklaraščių perrašymus
• Bendrinimas: skelbk tiesiogiai į Mastodon iš bet kurio bendrinimo lapo bet kurioje programėlėje
• Mielumas: mūsų talismanas yra žavus drambliukas, kurį retkarčiais pamatysi
Mastodon yra registruota ne pelno siekianti organizacija, kurios plėtra yra tiesiogiai paremta aukomis. Nėra jokios reklamos, jokių monetizacijos ir rizikos kapitalo, ir mes planuojame, kad taip ir liks.

View File

@@ -0,0 +1 @@
Decentralizuotas socialinis tinklas

View File

@@ -0,0 +1 @@
Mastodon

View File

@@ -2,7 +2,7 @@ Mastodon เป็นเครือข่ายสังคมแบบกร
เข้าร่วมชุมชนและสร้างโปรไฟล์ ค้นหาและติดตามผู้คนที่น่าสนใจและอ่านโพสต์ของเขาในเส้นเวลาที่ไม่มีโฆษณาและเรียงตามลำดับเวลา แสดงความรู้สึกของตัวคุณเองด้วยอีโมจิที่กำหนดเอง รูปภาพ GIF วิดีโอ และเสียงในโพสต์ 500 ตัวอักษร ตอบกลับและดันโพสต์จากคนอื่น ๆ เพื่อแชร์สิ่งดี ๆ และค้นหาบัญชีใหม่ ๆ ที่จะติดตามและแฮชแท็กที่เป็นที่นิยมเพื่อขยายเครือข่ายของคุณ
Mastodon สร้างขึ้นโดยเน้นความเป็นส่วนตัวและความปลอดภัยเป็นสำคัญ คุณสามารถตัดสินใจได้ว่าโพสต์ของคุณจะถูกแชร์กับผู้ติดตามของคุณ คนที่คุณกล่าวถึง หรือคนทั้งโลกก็ได้ ฟังก์ชั่นคำเตือนเนื้อหาช่วยให้คุณซ่อนโพสต์ที่มีเนื้อหาที่ละเอียดอ่อนจนกว่าคุณจะพร้อมเห็นเนื้อหานั้น แต่ละชุมชนจะมีหลักเกณฑ์และผู้ควบคุมเป็นของตนเองเพื่อรักษาความปลอดภัยของสมาชิก รวมถึงเครื่องมือการปิดกั้นและรายงานที่มีประสิทธิภาพเพื่อช่วยป้องกันการกระทำผิด
Mastodon สร้างขึ้นโดยเน้นความเป็นส่วนตัวและความปลอดภัยเป็นสำคัญ คุณสามารถตัดสินใจได้ว่าโพสต์ของคุณจะถูกแชร์กับผู้ติดตามของคุณ คนที่คุณกล่าวถึง หรือคนทั้งโลกก็ได้ ฟังก์ชั่นคำเตือนเนื้อหาช่วยให้คุณซ่อนโพสต์ที่มีเนื้อหาที่ละเอียดอ่อนจนกว่าคุณจะพร้อมเห็นเนื้อหานั้น แต่ละชุมชนจะมีหลักเกณฑ์และผู้กลั่นกรองเป็นของตนเองเพื่อรักษาความปลอดภัยของสมาชิก รวมถึงเครื่องมือการปิดกั้นและรายงานที่มีประสิทธิภาพเพื่อช่วยป้องกันการกระทำผิด
คุณสมบัติอื่น ๆ:

View File

@@ -1,8 +1,8 @@
Mastodon 是網際網路上最大的去中心化社群網路。 它是一個由能無縫互動的獨立社群中,數百萬使用者組成的網路,而非單一網站。 無論您對什麼事情感興趣,您都能 Mastodon 上遇到充滿熱情的人們討論該話題。
Mastodon 是網際網路上最大的去中心化社群網路。 它是一個由能無縫互動的獨立社群中,數百萬使用者組成的網路,而非單一網站。 無論您對什麼事情感興趣,您都能 Mastodon 上遇到充滿熱情的人們討論該話題。
加入社群並建立您的個人檔案。 尋找並跟隨迷人的朋友們,並無廣告、按時間順序排列的時間軸上閱讀他們的嘟文。 500 個字元的嘟文中使用自訂表情符號、GIF、視訊與音訊表達您自己。 回覆任何人的話題與轉發貼文以分享精彩內容。 尋找要跟隨的新帳號與熱門主題標籤拓展您的網路。
加入社群並建立您的個人檔案。 尋找並跟隨迷人的朋友們,並無廣告、按時間順序排列的時間軸上閱讀他們的嘟文。 500 個字元的嘟文中使用自訂表情符號、GIF、視訊與音訊表達您自己。 回覆任何人的話題與轉發貼文以分享精彩內容。 尋找要跟隨的新帳號與熱門主題標籤拓展您的網路。
Mastodon 以隱私與安全為要。 決定您的嘟文要與您的跟隨者分享、只與您提及的人們分享,又或是與全世界分享。 內容警告可讓您隱藏包含敏感或可能觸發強烈情緒反應的嘟文,直到您準備好與它們進行互動。 每個社群都有它們自己的指導方針與管理原來確保其成員安全,強大的封鎖與回報工具有助於防止濫用。
Mastodon 以隱私與安全為要。 決定您的嘟文要與您的跟隨者分享、只與您提及的人們分享,又或是與全世界分享。 內容警告可讓您隱藏包含敏感或可能觸發強烈情緒反應的嘟文,直到您準備好與它們進行互動。 每個社群都有它們自己的指導方針與管理員以確保其成員安全,強大的封鎖與回報工具有助於防止濫用。
更多功能:
@@ -10,7 +10,7 @@ Mastodon 以隱私與安全為要。 決定您的嘟文要與您的跟隨者分
• 投票:詢問跟隨者們的意見並計票
• 探索:僅需輕點一下,即可看到熱門主題標籤與帳號
• 通知:取得關於新跟隨者們、回覆與轉發的通知
• 分享:任何應用程式中的分享表中直接發表嘟文到 Mastodon 中
• 分享:任何應用程式中的分享表中直接發表嘟文到 Mastodon 中
• 可愛:我們的吉祥物是一隻可愛的大象,您會不時看到牠出現
Mastodon 是一家註冊的非營利組織,您的捐款會直接支援開發工作。 沒有廣告、沒有貨幣化、沒有風險投資,我們計畫維持這種狀態。 沒有廣告、沒有貨幣化、沒有風險投資,我們計畫維持這種狀態。

View File

@@ -9,10 +9,10 @@ android {
applicationId "org.joinmastodon.android"
minSdk 23
targetSdk 33
versionCode 78
versionName "2.2.2"
versionCode 86
versionName "2.4.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "da-rDK", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fa-rIR", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "ig-rNG", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "kab", "ko-rKR", "my-rMM", "nl-rNL", "no-rNO", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "ur-rIN", "vi-rVN", "zh-rCN", "zh-rTW"
resConfigs "ar-rSA", "be-rBY", "bn-rBD", "bs-rBA", "ca-rES", "cs-rCZ", "da-rDK", "de-rDE", "el-rGR", "es-rES", "eu-rES", "fa-rIR", "fi-rFI", "fil-rPH", "fr-rFR", "ga-rIE", "gd-rGB", "gl-rES", "hi-rIN", "hr-rHR", "hu-rHU", "hy-rAM", "ig-rNG", "in-rID", "is-rIS", "it-rIT", "iw-rIL", "ja-rJP", "ka-rGE", "kab", "ko-rKR", "lt-rLT", "my-rMM", "nl-rNL", "no-rNO", "oc-rFR", "pl-rPL", "pt-rBR", "pt-rPT", "ro-rRO", "ru-rRU", "si-rLK", "sl-rSI", "sv-rSE", "th-rTH", "tr-rTR", "uk-rUA", "ur-rIN", "vi-rVN", "zh-rCN", "zh-rTW"
}
buildTypes {
@@ -64,6 +64,9 @@ android {
checkReleaseBuilds false
abortOnError false
}
buildFeatures{
aidl true
}
}
dependencies {
@@ -76,11 +79,13 @@ 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.2.15'
implementation 'me.grishka.appkit:appkit:1.2.16'
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'org.jsoup:jsoup:1.14.3'
implementation 'com.squareup:otto:1.3.8'
implementation 'de.psdev:async-otto:1.0.3'
implementation 'com.google.zxing:core:3.5.3'
implementation 'org.microg:safe-parcel:1.5.0'
implementation 'org.parceler:parceler-api:1.1.12'
annotationProcessor 'org.parceler:parceler:1.1.12'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3'

View File

@@ -19,6 +19,10 @@
<intent>
<action android:name="android.intent.action.TRANSLATE" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="http"/>
</intent>
</queries>
<application
@@ -31,6 +35,10 @@
android:theme="@style/Theme.Mastodon.AutoLightDark"
android:largeHeap="true">
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="barcode_ui"/>
<activity android:name=".MainActivity" android:exported="true" android:configChanges="orientation|screenSize" android:windowSoftInputMode="adjustResize" android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
@@ -84,6 +92,14 @@
</intent-filter>
</receiver>
<provider
android:authorities="${applicationId}.fileprovider"
android:name=".TweakedFileProvider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/fileprovider_paths"/>
</provider>
</application>
</manifest>

View File

@@ -0,0 +1,3 @@
package com.google.android.gms.common.api;
parcelable Status;

View File

@@ -0,0 +1,7 @@
package com.google.android.gms.common.api.internal;
import com.google.android.gms.common.api.Status;
interface IStatusCallback {
void onResult(in Status status);
}

View File

@@ -0,0 +1,7 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.common.internal;
parcelable ConnectionInfo;

View File

@@ -0,0 +1,3 @@
package com.google.android.gms.common.internal;
parcelable GetServiceRequest;

View File

@@ -0,0 +1,15 @@
/*
* SPDX-FileCopyrightText: 2020, microG Project Team
* SPDX-License-Identifier: Apache-2.0
*/
package com.google.android.gms.common.internal;
import android.os.Bundle;
import com.google.android.gms.common.internal.ConnectionInfo;
interface IGmsCallbacks {
void onPostInitComplete(int statusCode, IBinder binder, in Bundle params);
void onAccountValidationComplete(int statusCode, in Bundle params);
void onPostInitCompleteWithConnectionInfo(int statusCode, IBinder binder, in ConnectionInfo info);
}

View File

@@ -0,0 +1,10 @@
package com.google.android.gms.common.internal;
import android.os.Bundle;
import com.google.android.gms.common.internal.IGmsCallbacks;
import com.google.android.gms.common.internal.GetServiceRequest;
interface IGmsServiceBroker {
void getService(IGmsCallbacks callback, in GetServiceRequest request) = 45;
}

View File

@@ -0,0 +1,3 @@
package com.google.android.gms.common.moduleinstall;
parcelable ModuleAvailabilityResponse;

View File

@@ -0,0 +1,3 @@
package com.google.android.gms.common.moduleinstall;
parcelable ModuleInstallIntentResponse;

View File

@@ -0,0 +1,3 @@
package com.google.android.gms.common.moduleinstall;
parcelable ModuleInstallResponse;

View File

@@ -0,0 +1,3 @@
package com.google.android.gms.common.moduleinstall;
parcelable ModuleInstallStatusUpdate;

View File

@@ -0,0 +1,3 @@
package com.google.android.gms.common.moduleinstall.internal;
parcelable ApiFeatureRequest;

View File

@@ -0,0 +1,13 @@
package com.google.android.gms.common.moduleinstall.internal;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.common.moduleinstall.ModuleAvailabilityResponse;
import com.google.android.gms.common.moduleinstall.ModuleInstallIntentResponse;
import com.google.android.gms.common.moduleinstall.ModuleInstallResponse;
interface IModuleInstallCallbacks {
void onModuleAvailabilityResponse(in Status status, in ModuleAvailabilityResponse response) = 0;
void onModuleInstallResponse(in Status status, in ModuleInstallResponse response) = 1;
void onModuleInstallIntentResponse(in Status status, in ModuleInstallIntentResponse response) = 2;
void onStatus(in Status status) = 3;
}

View File

@@ -0,0 +1,14 @@
package com.google.android.gms.common.moduleinstall.internal;
import com.google.android.gms.common.api.internal.IStatusCallback;
import com.google.android.gms.common.moduleinstall.internal.ApiFeatureRequest;
import com.google.android.gms.common.moduleinstall.internal.IModuleInstallCallbacks;
import com.google.android.gms.common.moduleinstall.internal.IModuleInstallStatusListener;
interface IModuleInstallService {
void areModulesAvailable(IModuleInstallCallbacks callbacks, in ApiFeatureRequest request) = 0;
void installModules(IModuleInstallCallbacks callbacks, in ApiFeatureRequest request, IModuleInstallStatusListener listener) = 1;
void getInstallModulesIntent(IModuleInstallCallbacks callbacks, in ApiFeatureRequest request) = 2;
void releaseModules(IStatusCallback callback, in ApiFeatureRequest request) = 3;
void unregisterListener(IStatusCallback callback, IModuleInstallStatusListener listener) = 5;
}

View File

@@ -0,0 +1,7 @@
package com.google.android.gms.common.moduleinstall.internal;
import com.google.android.gms.common.moduleinstall.ModuleInstallStatusUpdate;
interface IModuleInstallStatusListener {
void onModuleInstallStatusUpdate(in ModuleInstallStatusUpdate statusUpdate) = 0;
}

View File

@@ -0,0 +1,15 @@
package com.google.android.gms.common;
import org.microg.safeparcel.AutoSafeParcelable;
import org.microg.safeparcel.SafeParceled;
public class Feature extends AutoSafeParcelable{
@SafeParceled(1)
public String name;
@SafeParceled(2)
public int oldVersion;
@SafeParceled(3)
public long version=-1;
public static final Creator<Feature> CREATOR=new AutoCreator<>(Feature.class);
}

View File

@@ -0,0 +1,13 @@
package com.google.android.gms.common.api;
import org.microg.safeparcel.AutoSafeParcelable;
import org.microg.safeparcel.SafeParceled;
public class Scope extends AutoSafeParcelable{
@SafeParceled(1)
public int versionCode=1;
@SafeParceled(2)
public String scopeUri;
public static final Creator<Scope> CREATOR=new AutoCreator<>(Scope.class);
}

View File

@@ -0,0 +1,33 @@
package com.google.android.gms.common.api;
import android.app.PendingIntent;
import org.joinmastodon.android.googleservices.ConnectionResult;
import org.microg.safeparcel.AutoSafeParcelable;
import org.microg.safeparcel.SafeParceled;
public class Status extends AutoSafeParcelable{
@SafeParceled(1000)
public int versionCode;
@SafeParceled(1)
public int statusCode;
@SafeParceled(2)
public String statusMessage;
@SafeParceled(3)
public PendingIntent pendingIntent;
@SafeParceled(4)
public ConnectionResult connectionResult;
public static final Creator<Status> CREATOR=new AutoCreator<>(Status.class);
@Override
public String toString(){
return "Status{"+
"versionCode="+versionCode+
", statusCode="+statusCode+
", statusMessage='"+statusMessage+'\''+
", pendingIntent="+pendingIntent+
", connectionResult="+connectionResult+
'}';
}
}

View File

@@ -0,0 +1,19 @@
package com.google.android.gms.common.internal;
import android.os.Bundle;
import com.google.android.gms.common.Feature;
import org.microg.safeparcel.AutoSafeParcelable;
import org.microg.safeparcel.SafeParceled;
public class ConnectionInfo extends AutoSafeParcelable{
@SafeParceled(1)
public Bundle params;
@SafeParceled(2)
public Feature[] features;
@SafeParceled(3)
public int unknown3;
public static final Creator<ConnectionInfo> CREATOR=new AutoCreator<>(ConnectionInfo.class);
}

View File

@@ -0,0 +1,47 @@
package com.google.android.gms.common.internal;
import android.os.Bundle;
import android.os.IBinder;
import android.accounts.Account;
import com.google.android.gms.common.Feature;
import com.google.android.gms.common.api.Scope;
import org.microg.safeparcel.AutoSafeParcelable;
import org.microg.safeparcel.SafeParceled;
public class GetServiceRequest extends AutoSafeParcelable{
@SafeParceled(1)
int versionCode=6;
@SafeParceled(2)
public int serviceId;
@SafeParceled(3)
public int gmsVersion;
@SafeParceled(4)
public String packageName;
@SafeParceled(5)
public IBinder accountAccessor;
@SafeParceled(6)
public Scope[] scopes;
@SafeParceled(7)
public Bundle extras;
@SafeParceled(8)
public Account account;
@SafeParceled(9)
@Deprecated
long field9;
@SafeParceled(10)
public Feature[] defaultFeatures;
@SafeParceled(11)
public Feature[] apiFeatures;
@SafeParceled(12)
boolean supportsConnectionInfo;
@SafeParceled(13)
int field13;
@SafeParceled(14)
boolean field14;
@SafeParceled(15)
String attributionTag;
public static final Creator<GetServiceRequest> CREATOR=new AutoCreator<>(GetServiceRequest.class);
}

View File

@@ -0,0 +1,13 @@
package com.google.android.gms.common.moduleinstall;
import org.microg.safeparcel.AutoSafeParcelable;
import org.microg.safeparcel.SafeParceled;
public class ModuleAvailabilityResponse extends AutoSafeParcelable{
@SafeParceled(1)
public boolean modulesAvailable;
@SafeParceled(2)
public int availabilityStatus;
public static final Creator<ModuleAvailabilityResponse> CREATOR=new AutoCreator<>(ModuleAvailabilityResponse.class);
}

View File

@@ -0,0 +1,13 @@
package com.google.android.gms.common.moduleinstall;
import android.app.PendingIntent;
import org.microg.safeparcel.AutoSafeParcelable;
import org.microg.safeparcel.SafeParceled;
public class ModuleInstallIntentResponse extends AutoSafeParcelable{
@SafeParceled(1)
public PendingIntent pendingIntent;
public static final Creator<ModuleInstallIntentResponse> CREATOR=new AutoCreator<>(ModuleInstallIntentResponse.class);
}

View File

@@ -0,0 +1,21 @@
package com.google.android.gms.common.moduleinstall;
import org.microg.safeparcel.AutoSafeParcelable;
import org.microg.safeparcel.SafeParceled;
public class ModuleInstallResponse extends AutoSafeParcelable{
@SafeParceled(1)
public int sessionID;
@SafeParceled(2)
public boolean shouldUnregisterListener;
public static final Creator<ModuleInstallResponse> CREATOR=new AutoCreator<>(ModuleInstallResponse.class);
@Override
public String toString(){
return "ModuleInstallResponse{"+
"sessionID="+sessionID+
", shouldUnregisterListener="+shouldUnregisterListener+
'}';
}
}

View File

@@ -0,0 +1,63 @@
package com.google.android.gms.common.moduleinstall;
import org.microg.safeparcel.AutoSafeParcelable;
import org.microg.safeparcel.SafeParceled;
public class ModuleInstallStatusUpdate extends AutoSafeParcelable{
public static final int STATE_UNKNOWN = 0;
/**
* The request is pending and will be processed soon.
*/
public static final int STATE_PENDING = 1;
/**
* The optional module download is in progress.
*/
public static final int STATE_DOWNLOADING = 2;
/**
* The optional module download has been canceled.
*/
public static final int STATE_CANCELED = 3;
/**
* Installation is completed; the optional modules are available to the client app.
*/
public static final int STATE_COMPLETED = 4;
/**
* The optional module download or installation has failed.
*/
public static final int STATE_FAILED = 5;
/**
* The optional modules have been downloaded and the installation is in progress.
*/
public static final int STATE_INSTALLING = 6;
/**
* The optional module download has been paused.
* <p>
* This usually happens when connectivity requirements can't be met during download. Once the connectivity requirements
* are met, the download will be resumed automatically.
*/
public static final int STATE_DOWNLOAD_PAUSED = 7;
@SafeParceled(1)
public int sessionID;
@SafeParceled(2)
public int installState;
@SafeParceled(3)
public Long bytesDownloaded;
@SafeParceled(4)
public Long totalBytesToDownload;
@SafeParceled(5)
public int errorCode;
@Override
public String toString(){
return "ModuleInstallStatusUpdate{"+
"sessionID="+sessionID+
", installState="+installState+
", bytesDownloaded="+bytesDownloaded+
", totalBytesToDownload="+totalBytesToDownload+
", errorCode="+errorCode+
'}';
}
public static final Creator<ModuleInstallStatusUpdate> CREATOR=new AutoCreator<>(ModuleInstallStatusUpdate.class);
}

View File

@@ -0,0 +1,21 @@
package com.google.android.gms.common.moduleinstall.internal;
import com.google.android.gms.common.Feature;
import org.microg.safeparcel.AutoSafeParcelable;
import org.microg.safeparcel.SafeParceled;
import java.util.List;
public class ApiFeatureRequest extends AutoSafeParcelable{
@SafeParceled(value=1, subClass=Feature.class)
public List<Feature> features;
@SafeParceled(2)
public boolean urgent;
@SafeParceled(3)
public String sessionId;
@SafeParceled(4)
public String callingPackage;
public static final Creator<ApiFeatureRequest> CREATOR=new AutoCreator<>(ApiFeatureRequest.class);
}

View File

@@ -0,0 +1,841 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.joinmastodon.android;
import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
import static org.xmlpull.v1.XmlPullParser.START_TAG;
import android.content.ClipData;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.res.XmlResourceParser;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;
import android.text.TextUtils;
import android.webkit.MimeTypeMap;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* FileProvider is a special subclass of {@link ContentProvider} that facilitates secure sharing
* of files associated with an app by creating a <code>content://</code> {@link Uri} for a file
* instead of a <code>file:///</code> {@link Uri}.
* <p>
* A content URI allows you to grant read and write access using
* temporary access permissions. When you create an {@link Intent} containing
* a content URI, in order to send the content URI
* to a client app, you can also call {@link Intent#setFlags(int) Intent.setFlags()} to add
* permissions. These permissions are available to the client app for as long as the stack for
* a receiving {@link android.app.Activity} is active. For an {@link Intent} going to a
* {@link android.app.Service}, the permissions are available as long as the
* {@link android.app.Service} is running.
* <p>
* In comparison, to control access to a <code>file:///</code> {@link Uri} you have to modify the
* file system permissions of the underlying file. The permissions you provide become available to
* <em>any</em> app, and remain in effect until you change them. This level of access is
* fundamentally insecure.
* <p>
* The increased level of file access security offered by a content URI
* makes FileProvider a key part of Android's security infrastructure.
* <p>
* This overview of FileProvider includes the following topics:
* </p>
* <ol>
* <li><a href="#ProviderDefinition">Defining a FileProvider</a></li>
* <li><a href="#SpecifyFiles">Specifying Available Files</a></li>
* <li><a href="#GetUri">Retrieving the Content URI for a File</li>
* <li><a href="#Permissions">Granting Temporary Permissions to a URI</a></li>
* <li><a href="#ServeUri">Serving a Content URI to Another App</a></li>
* </ol>
* <h3 id="ProviderDefinition">Defining a FileProvider</h3>
* <p>
* Since the default functionality of FileProvider includes content URI generation for files, you
* don't need to define a subclass in code. Instead, you can include a FileProvider in your app
* by specifying it entirely in XML. To specify the FileProvider component itself, add a
* <code><a href="{@docRoot}guide/topics/manifest/provider-element.html">&lt;provider&gt;</a></code>
* element to your app manifest. Set the <code>android:name</code> attribute to
* <code>androidx.core.content.FileProvider</code>. Set the <code>android:authorities</code>
* attribute to a URI authority based on a domain you control; for example, if you control the
* domain <code>mydomain.com</code> you should use the authority
* <code>com.mydomain.fileprovider</code>. Set the <code>android:exported</code> attribute to
* <code>false</code>; the FileProvider does not need to be public. Set the
* <a href="{@docRoot}guide/topics/manifest/provider-element.html#gprmsn"
* >android:grantUriPermissions</a> attribute to <code>true</code>, to allow you
* to grant temporary access to files. For example:
* <pre class="prettyprint">
*&lt;manifest&gt;
* ...
* &lt;application&gt;
* ...
* &lt;provider
* android:name="androidx.core.content.FileProvider"
* android:authorities="com.mydomain.fileprovider"
* android:exported="false"
* android:grantUriPermissions="true"&gt;
* ...
* &lt;/provider&gt;
* ...
* &lt;/application&gt;
*&lt;/manifest&gt;</pre>
* <p>
* If you want to override any of the default behavior of FileProvider methods, extend
* the FileProvider class and use the fully-qualified class name in the <code>android:name</code>
* attribute of the <code>&lt;provider&gt;</code> element.
* <h3 id="SpecifyFiles">Specifying Available Files</h3>
* A FileProvider can only generate a content URI for files in directories that you specify
* beforehand. To specify a directory, specify the its storage area and path in XML, using child
* elements of the <code>&lt;paths&gt;</code> element.
* For example, the following <code>paths</code> element tells FileProvider that you intend to
* request content URIs for the <code>images/</code> subdirectory of your private file area.
* <pre class="prettyprint">
*&lt;paths xmlns:android="http://schemas.android.com/apk/res/android"&gt;
* &lt;files-path name="my_images" path="images/"/&gt;
* ...
*&lt;/paths&gt;
*</pre>
* <p>
* The <code>&lt;paths&gt;</code> element must contain one or more of the following child elements:
* </p>
* <dl>
* <dt>
* <pre class="prettyprint">
*&lt;files-path name="<i>name</i>" path="<i>path</i>" /&gt;
*</pre>
* </dt>
* <dd>
* Represents files in the <code>files/</code> subdirectory of your app's internal storage
* area. This subdirectory is the same as the value returned by {@link Context#getFilesDir()
* Context.getFilesDir()}.
* </dd>
* <dt>
* <pre>
*&lt;cache-path name="<i>name</i>" path="<i>path</i>" /&gt;
*</pre>
* <dt>
* <dd>
* Represents files in the cache subdirectory of your app's internal storage area. The root path
* of this subdirectory is the same as the value returned by {@link Context#getCacheDir()
* getCacheDir()}.
* </dd>
* <dt>
* <pre class="prettyprint">
*&lt;external-path name="<i>name</i>" path="<i>path</i>" /&gt;
*</pre>
* </dt>
* <dd>
* Represents files in the root of the external storage area. The root path of this subdirectory
* is the same as the value returned by
* {@link Environment#getExternalStorageDirectory() Environment.getExternalStorageDirectory()}.
* </dd>
* <dt>
* <pre class="prettyprint">
*&lt;external-files-path name="<i>name</i>" path="<i>path</i>" /&gt;
*</pre>
* </dt>
* <dd>
* Represents files in the root of your app's external storage area. The root path of this
* subdirectory is the same as the value returned by
* {@code Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)}.
* </dd>
* <dt>
* <pre class="prettyprint">
*&lt;external-cache-path name="<i>name</i>" path="<i>path</i>" /&gt;
*</pre>
* </dt>
* <dd>
* Represents files in the root of your app's external cache area. The root path of this
* subdirectory is the same as the value returned by
* {@link Context#getExternalCacheDir() Context.getExternalCacheDir()}.
* </dd>
* <dt>
* <pre class="prettyprint">
*&lt;external-media-path name="<i>name</i>" path="<i>path</i>" /&gt;
*</pre>
* </dt>
* <dd>
* Represents files in the root of your app's external media area. The root path of this
* subdirectory is the same as the value returned by the first result of
* {@link Context#getExternalMediaDirs() Context.getExternalMediaDirs()}.
* <p><strong>Note:</strong> this directory is only available on API 21+ devices.</p>
* </dd>
* </dl>
* <p>
* These child elements all use the same attributes:
* </p>
* <dl>
* <dt>
* <code>name="<i>name</i>"</code>
* </dt>
* <dd>
* A URI path segment. To enforce security, this value hides the name of the subdirectory
* you're sharing. The subdirectory name for this value is contained in the
* <code>path</code> attribute.
* </dd>
* <dt>
* <code>path="<i>path</i>"</code>
* </dt>
* <dd>
* The subdirectory you're sharing. While the <code>name</code> attribute is a URI path
* segment, the <code>path</code> value is an actual subdirectory name. Notice that the
* value refers to a <b>subdirectory</b>, not an individual file or files. You can't
* share a single file by its file name, nor can you specify a subset of files using
* wildcards.
* </dd>
* </dl>
* <p>
* You must specify a child element of <code>&lt;paths&gt;</code> for each directory that contains
* files for which you want content URIs. For example, these XML elements specify two directories:
* <pre class="prettyprint">
*&lt;paths xmlns:android="http://schemas.android.com/apk/res/android"&gt;
* &lt;files-path name="my_images" path="images/"/&gt;
* &lt;files-path name="my_docs" path="docs/"/&gt;
*&lt;/paths&gt;
*</pre>
* <p>
* Put the <code>&lt;paths&gt;</code> element and its children in an XML file in your project.
* For example, you can add them to a new file called <code>res/xml/file_paths.xml</code>.
* To link this file to the FileProvider, add a
* <a href="{@docRoot}guide/topics/manifest/meta-data-element.html">&lt;meta-data&gt;</a> element
* as a child of the <code>&lt;provider&gt;</code> element that defines the FileProvider. Set the
* <code>&lt;meta-data&gt;</code> element's "android:name" attribute to
* <code>android.support.FILE_PROVIDER_PATHS</code>. Set the element's "android:resource" attribute
* to <code>&#64;xml/file_paths</code> (notice that you don't specify the <code>.xml</code>
* extension). For example:
* <pre class="prettyprint">
*&lt;provider
* android:name="androidx.core.content.FileProvider"
* android:authorities="com.mydomain.fileprovider"
* android:exported="false"
* android:grantUriPermissions="true"&gt;
* &lt;meta-data
* android:name="android.support.FILE_PROVIDER_PATHS"
* android:resource="&#64;xml/file_paths" /&gt;
*&lt;/provider&gt;
*</pre>
* <h3 id="GetUri">Generating the Content URI for a File</h3>
* <p>
* To share a file with another app using a content URI, your app has to generate the content URI.
* To generate the content URI, create a new {@link File} for the file, then pass the {@link File}
* to {@link #getUriForFile(Context, String, File) getUriForFile()}. You can send the content URI
* returned by {@link #getUriForFile(Context, String, File) getUriForFile()} to another app in an
* {@link Intent}. The client app that receives the content URI can open the file
* and access its contents by calling
* {@link android.content.ContentResolver#openFileDescriptor(Uri, String)
* ContentResolver.openFileDescriptor} to get a {@link ParcelFileDescriptor}.
* <p>
* For example, suppose your app is offering files to other apps with a FileProvider that has the
* authority <code>com.mydomain.fileprovider</code>. To get a content URI for the file
* <code>default_image.jpg</code> in the <code>images/</code> subdirectory of your internal storage
* add the following code:
* <pre class="prettyprint">
*File imagePath = new File(Context.getFilesDir(), "images");
*File newFile = new File(imagePath, "default_image.jpg");
*Uri contentUri = getUriForFile(getContext(), "com.mydomain.fileprovider", newFile);
*</pre>
* As a result of the previous snippet,
* {@link #getUriForFile(Context, String, File) getUriForFile()} returns the content URI
* <code>content://com.mydomain.fileprovider/my_images/default_image.jpg</code>.
* <h3 id="Permissions">Granting Temporary Permissions to a URI</h3>
* To grant an access permission to a content URI returned from
* {@link #getUriForFile(Context, String, File) getUriForFile()}, do one of the following:
* <ul>
* <li>
* Call the method
* {@link Context#grantUriPermission(String, Uri, int)
* Context.grantUriPermission(package, Uri, mode_flags)} for the <code>content://</code>
* {@link Uri}, using the desired mode flags. This grants temporary access permission for the
* content URI to the specified package, according to the value of the
* the <code>mode_flags</code> parameter, which you can set to
* {@link Intent#FLAG_GRANT_READ_URI_PERMISSION}, {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}
* or both. The permission remains in effect until you revoke it by calling
* {@link Context#revokeUriPermission(Uri, int) revokeUriPermission()} or until the device
* reboots.
* </li>
* <li>
* Put the content URI in an {@link Intent} by calling {@link Intent#setData(Uri) setData()}.
* </li>
* <li>
* Next, call the method {@link Intent#setFlags(int) Intent.setFlags()} with either
* {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} or
* {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION} or both.
* </li>
* <li>
* Finally, send the {@link Intent} to
* another app. Most often, you do this by calling
* {@link android.app.Activity#setResult(int, Intent) setResult()}.
* <p>
* Permissions granted in an {@link Intent} remain in effect while the stack of the receiving
* {@link android.app.Activity} is active. When the stack finishes, the permissions are
* automatically removed. Permissions granted to one {@link android.app.Activity} in a client
* app are automatically extended to other components of that app.
* </p>
* </li>
* </ul>
* <h3 id="ServeUri">Serving a Content URI to Another App</h3>
* <p>
* There are a variety of ways to serve the content URI for a file to a client app. One common way
* is for the client app to start your app by calling
* {@link android.app.Activity#startActivityForResult(Intent, int, Bundle) startActivityResult()},
* which sends an {@link Intent} to your app to start an {@link android.app.Activity} in your app.
* In response, your app can immediately return a content URI to the client app or present a user
* interface that allows the user to pick a file. In the latter case, once the user picks the file
* your app can return its content URI. In both cases, your app returns the content URI in an
* {@link Intent} sent via {@link android.app.Activity#setResult(int, Intent) setResult()}.
* </p>
* <p>
* You can also put the content URI in a {@link android.content.ClipData} object and then add the
* object to an {@link Intent} you send to a client app. To do this, call
* {@link Intent#setClipData(ClipData) Intent.setClipData()}. When you use this approach, you can
* add multiple {@link android.content.ClipData} objects to the {@link Intent}, each with its own
* content URI. When you call {@link Intent#setFlags(int) Intent.setFlags()} on the {@link Intent}
* to set temporary access permissions, the same permissions are applied to all of the content
* URIs.
* </p>
* <p class="note">
* <strong>Note:</strong> The {@link Intent#setClipData(ClipData) Intent.setClipData()} method is
* only available in platform version 16 (Android 4.1) and later. If you want to maintain
* compatibility with previous versions, you should send one content URI at a time in the
* {@link Intent}. Set the action to {@link Intent#ACTION_SEND} and put the URI in data by calling
* {@link Intent#setData setData()}.
* </p>
* <h3 id="">More Information</h3>
* <p>
* To learn more about FileProvider, see the Android training class
* <a href="{@docRoot}training/secure-file-sharing/index.html">Sharing Files Securely with URIs</a>.
* </p>
*/
public class FileProvider extends ContentProvider {
private static final String[] COLUMNS = {
OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE };
private static final String
META_DATA_FILE_PROVIDER_PATHS = "android.support.FILE_PROVIDER_PATHS";
private static final String TAG_ROOT_PATH = "root-path";
private static final String TAG_FILES_PATH = "files-path";
private static final String TAG_CACHE_PATH = "cache-path";
private static final String TAG_EXTERNAL = "external-path";
private static final String TAG_EXTERNAL_FILES = "external-files-path";
private static final String TAG_EXTERNAL_CACHE = "external-cache-path";
private static final String TAG_EXTERNAL_MEDIA = "external-media-path";
private static final String ATTR_NAME = "name";
private static final String ATTR_PATH = "path";
private static final File DEVICE_ROOT = new File("/");
@GuardedBy("sCache")
private static HashMap<String, PathStrategy> sCache = new HashMap<String, PathStrategy>();
private PathStrategy mStrategy;
/**
* The default FileProvider implementation does not need to be initialized. If you want to
* override this method, you must provide your own subclass of FileProvider.
*/
@Override
public boolean onCreate() {
return true;
}
/**
* After the FileProvider is instantiated, this method is called to provide the system with
* information about the provider.
*
* @param context A {@link Context} for the current component.
* @param info A {@link ProviderInfo} for the new provider.
*/
@Override
public void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) {
super.attachInfo(context, info);
// Sanity check our security
if (info.exported) {
throw new SecurityException("Provider must not be exported");
}
if (!info.grantUriPermissions) {
throw new SecurityException("Provider must grant uri permissions");
}
mStrategy = getPathStrategy(context, info.authority);
}
/**
* Return a content URI for a given {@link File}. Specific temporary
* permissions for the content URI can be set with
* {@link Context#grantUriPermission(String, Uri, int)}, or added
* to an {@link Intent} by calling {@link Intent#setData(Uri) setData()} and then
* {@link Intent#setFlags(int) setFlags()}; in both cases, the applicable flags are
* {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} and
* {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}. A FileProvider can only return a
* <code>content</code> {@link Uri} for file paths defined in their <code>&lt;paths&gt;</code>
* meta-data element. See the Class Overview for more information.
*
* @param context A {@link Context} for the current component.
* @param authority The authority of a {@link FileProvider} defined in a
* {@code <provider>} element in your app's manifest.
* @param file A {@link File} pointing to the filename for which you want a
* <code>content</code> {@link Uri}.
* @return A content URI for the file.
* @throws IllegalArgumentException When the given {@link File} is outside
* the paths supported by the provider.
*/
public static Uri getUriForFile(@NonNull Context context, @NonNull String authority,
@NonNull File file) {
final PathStrategy strategy = getPathStrategy(context, authority);
return strategy.getUriForFile(file);
}
/**
* Use a content URI returned by
* {@link #getUriForFile(Context, String, File) getUriForFile()} to get information about a file
* managed by the FileProvider.
* FileProvider reports the column names defined in {@link OpenableColumns}:
* <ul>
* <li>{@link OpenableColumns#DISPLAY_NAME}</li>
* <li>{@link OpenableColumns#SIZE}</li>
* </ul>
* For more information, see
* {@link ContentProvider#query(Uri, String[], String, String[], String)
* ContentProvider.query()}.
*
* @param uri A content URI returned by {@link #getUriForFile}.
* @param projection The list of columns to put into the {@link Cursor}. If null all columns are
* included.
* @param selection Selection criteria to apply. If null then all data that matches the content
* URI is returned.
* @param selectionArgs An array of {@link String}, containing arguments to bind to
* the <i>selection</i> parameter. The <i>query</i> method scans <i>selection</i> from left to
* right and iterates through <i>selectionArgs</i>, replacing the current "?" character in
* <i>selection</i> with the value at the current position in <i>selectionArgs</i>. The
* values are bound to <i>selection</i> as {@link String} values.
* @param sortOrder A {@link String} containing the column name(s) on which to sort
* the resulting {@link Cursor}.
* @return A {@link Cursor} containing the results of the query.
*
*/
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
@Nullable String[] selectionArgs,
@Nullable String sortOrder) {
// ContentProvider has already checked granted permissions
final File file = mStrategy.getFileForUri(uri);
if (projection == null) {
projection = COLUMNS;
}
String[] cols = new String[projection.length];
Object[] values = new Object[projection.length];
int i = 0;
for (String col : projection) {
if (OpenableColumns.DISPLAY_NAME.equals(col)) {
cols[i] = OpenableColumns.DISPLAY_NAME;
values[i++] = file.getName();
} else if (OpenableColumns.SIZE.equals(col)) {
cols[i] = OpenableColumns.SIZE;
values[i++] = file.length();
}
}
cols = copyOf(cols, i);
values = copyOf(values, i);
final MatrixCursor cursor = new MatrixCursor(cols, 1);
cursor.addRow(values);
return cursor;
}
/**
* Returns the MIME type of a content URI returned by
* {@link #getUriForFile(Context, String, File) getUriForFile()}.
*
* @param uri A content URI returned by
* {@link #getUriForFile(Context, String, File) getUriForFile()}.
* @return If the associated file has an extension, the MIME type associated with that
* extension; otherwise <code>application/octet-stream</code>.
*/
@Override
public String getType(@NonNull Uri uri) {
// ContentProvider has already checked granted permissions
final File file = mStrategy.getFileForUri(uri);
final int lastDot = file.getName().lastIndexOf('.');
if (lastDot >= 0) {
final String extension = file.getName().substring(lastDot + 1);
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mime != null) {
return mime;
}
}
return "application/octet-stream";
}
/**
* By default, this method throws an {@link UnsupportedOperationException}. You must
* subclass FileProvider if you want to provide different functionality.
*/
@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {
throw new UnsupportedOperationException("No external inserts");
}
/**
* By default, this method throws an {@link UnsupportedOperationException}. You must
* subclass FileProvider if you want to provide different functionality.
*/
@Override
public int update(@NonNull Uri uri, ContentValues values, @Nullable String selection,
@Nullable String[] selectionArgs) {
throw new UnsupportedOperationException("No external updates");
}
/**
* Deletes the file associated with the specified content URI, as
* returned by {@link #getUriForFile(Context, String, File) getUriForFile()}. Notice that this
* method does <b>not</b> throw an {@link IOException}; you must check its return value.
*
* @param uri A content URI for a file, as returned by
* {@link #getUriForFile(Context, String, File) getUriForFile()}.
* @param selection Ignored. Set to {@code null}.
* @param selectionArgs Ignored. Set to {@code null}.
* @return 1 if the delete succeeds; otherwise, 0.
*/
@Override
public int delete(@NonNull Uri uri, @Nullable String selection,
@Nullable String[] selectionArgs) {
// ContentProvider has already checked granted permissions
final File file = mStrategy.getFileForUri(uri);
return file.delete() ? 1 : 0;
}
/**
* By default, FileProvider automatically returns the
* {@link ParcelFileDescriptor} for a file associated with a <code>content://</code>
* {@link Uri}. To get the {@link ParcelFileDescriptor}, call
* {@link android.content.ContentResolver#openFileDescriptor(Uri, String)
* ContentResolver.openFileDescriptor}.
*
* To override this method, you must provide your own subclass of FileProvider.
*
* @param uri A content URI associated with a file, as returned by
* {@link #getUriForFile(Context, String, File) getUriForFile()}.
* @param mode Access mode for the file. May be "r" for read-only access, "rw" for read and
* write access, or "rwt" for read and write access that truncates any existing file.
* @return A new {@link ParcelFileDescriptor} with which you can access the file.
*/
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode)
throws FileNotFoundException {
// ContentProvider has already checked granted permissions
final File file = mStrategy.getFileForUri(uri);
final int fileMode = modeToMode(mode);
return ParcelFileDescriptor.open(file, fileMode);
}
/**
* Return {@link PathStrategy} for given authority, either by parsing or
* returning from cache.
*/
private static PathStrategy getPathStrategy(Context context, String authority) {
PathStrategy strat;
synchronized (sCache) {
strat = sCache.get(authority);
if (strat == null) {
try {
strat = parsePathStrategy(context, authority);
} catch (IOException e) {
throw new IllegalArgumentException(
"Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
} catch (XmlPullParserException e) {
throw new IllegalArgumentException(
"Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
}
sCache.put(authority, strat);
}
}
return strat;
}
/**
* Parse and return {@link PathStrategy} for given authority as defined in
* {@link #META_DATA_FILE_PROVIDER_PATHS} {@code <meta-data>}.
*
* @see #getPathStrategy(Context, String)
*/
private static PathStrategy parsePathStrategy(Context context, String authority)
throws IOException, XmlPullParserException {
final SimplePathStrategy strat = new SimplePathStrategy(authority);
final ProviderInfo info = context.getPackageManager()
.resolveContentProvider(authority, PackageManager.GET_META_DATA);
if (info == null) {
throw new IllegalArgumentException(
"Couldn't find meta-data for provider with authority " + authority);
}
final XmlResourceParser in = info.loadXmlMetaData(
context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS);
if (in == null) {
throw new IllegalArgumentException(
"Missing " + META_DATA_FILE_PROVIDER_PATHS + " meta-data");
}
int type;
while ((type = in.next()) != END_DOCUMENT) {
if (type == START_TAG) {
final String tag = in.getName();
final String name = in.getAttributeValue(null, ATTR_NAME);
String path = in.getAttributeValue(null, ATTR_PATH);
File target = null;
if (TAG_ROOT_PATH.equals(tag)) {
target = DEVICE_ROOT;
} else if (TAG_FILES_PATH.equals(tag)) {
target = context.getFilesDir();
} else if (TAG_CACHE_PATH.equals(tag)) {
target = context.getCacheDir();
} else if (TAG_EXTERNAL.equals(tag)) {
target = Environment.getExternalStorageDirectory();
} else if (TAG_EXTERNAL_FILES.equals(tag)) {
File[] externalFilesDirs = context.getExternalFilesDirs(null);
if (externalFilesDirs.length > 0) {
target = externalFilesDirs[0];
}
} else if (TAG_EXTERNAL_CACHE.equals(tag)) {
File[] externalCacheDirs = context.getExternalCacheDirs();
if (externalCacheDirs.length > 0) {
target = externalCacheDirs[0];
}
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& TAG_EXTERNAL_MEDIA.equals(tag)) {
File[] externalMediaDirs = context.getExternalMediaDirs();
if (externalMediaDirs.length > 0) {
target = externalMediaDirs[0];
}
}
if (target != null) {
strat.addRoot(name, buildPath(target, path));
}
}
}
return strat;
}
/**
* Strategy for mapping between {@link File} and {@link Uri}.
* <p>
* Strategies must be symmetric so that mapping a {@link File} to a
* {@link Uri} and then back to a {@link File} points at the original
* target.
* <p>
* Strategies must remain consistent across app launches, and not rely on
* dynamic state. This ensures that any generated {@link Uri} can still be
* resolved if your process is killed and later restarted.
*
* @see SimplePathStrategy
*/
interface PathStrategy {
/**
* Return a {@link Uri} that represents the given {@link File}.
*/
Uri getUriForFile(File file);
/**
* Return a {@link File} that represents the given {@link Uri}.
*/
File getFileForUri(Uri uri);
}
/**
* Strategy that provides access to files living under a narrow whitelist of
* filesystem roots. It will throw {@link SecurityException} if callers try
* accessing files outside the configured roots.
* <p>
* For example, if configured with
* {@code addRoot("myfiles", context.getFilesDir())}, then
* {@code context.getFileStreamPath("foo.txt")} would map to
* {@code content://myauthority/myfiles/foo.txt}.
*/
static class SimplePathStrategy implements PathStrategy {
private final String mAuthority;
private final HashMap<String, File> mRoots = new HashMap<String, File>();
SimplePathStrategy(String authority) {
mAuthority = authority;
}
/**
* Add a mapping from a name to a filesystem root. The provider only offers
* access to files that live under configured roots.
*/
void addRoot(String name, File root) {
if (TextUtils.isEmpty(name)) {
throw new IllegalArgumentException("Name must not be empty");
}
try {
// Resolve to canonical path to keep path checking fast
root = root.getCanonicalFile();
} catch (IOException e) {
throw new IllegalArgumentException(
"Failed to resolve canonical path for " + root, e);
}
mRoots.put(name, root);
}
@Override
public Uri getUriForFile(File file) {
String path;
try {
path = file.getCanonicalPath();
} catch (IOException e) {
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
}
// Find the most-specific root path
Map.Entry<String, File> mostSpecific = null;
for (Map.Entry<String, File> root : mRoots.entrySet()) {
final String rootPath = root.getValue().getPath();
if (path.startsWith(rootPath) && (mostSpecific == null
|| rootPath.length() > mostSpecific.getValue().getPath().length())) {
mostSpecific = root;
}
}
if (mostSpecific == null) {
throw new IllegalArgumentException(
"Failed to find configured root that contains " + path);
}
// Start at first char of path under root
final String rootPath = mostSpecific.getValue().getPath();
if (rootPath.endsWith("/")) {
path = path.substring(rootPath.length());
} else {
path = path.substring(rootPath.length() + 1);
}
// Encode the tag and path separately
path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/");
return new Uri.Builder().scheme("content")
.authority(mAuthority).encodedPath(path).build();
}
@Override
public File getFileForUri(Uri uri) {
String path = uri.getEncodedPath();
final int splitIndex = path.indexOf('/', 1);
final String tag = Uri.decode(path.substring(1, splitIndex));
path = Uri.decode(path.substring(splitIndex + 1));
final File root = mRoots.get(tag);
if (root == null) {
throw new IllegalArgumentException("Unable to find configured root for " + uri);
}
File file = new File(root, path);
try {
file = file.getCanonicalFile();
} catch (IOException e) {
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
}
if (!file.getPath().startsWith(root.getPath())) {
throw new SecurityException("Resolved path jumped beyond configured root");
}
return file;
}
}
/**
* Copied from ContentResolver.java
*/
private static int modeToMode(String mode) {
int modeBits;
if ("r".equals(mode)) {
modeBits = ParcelFileDescriptor.MODE_READ_ONLY;
} else if ("w".equals(mode) || "wt".equals(mode)) {
modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
| ParcelFileDescriptor.MODE_CREATE
| ParcelFileDescriptor.MODE_TRUNCATE;
} else if ("wa".equals(mode)) {
modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
| ParcelFileDescriptor.MODE_CREATE
| ParcelFileDescriptor.MODE_APPEND;
} else if ("rw".equals(mode)) {
modeBits = ParcelFileDescriptor.MODE_READ_WRITE
| ParcelFileDescriptor.MODE_CREATE;
} else if ("rwt".equals(mode)) {
modeBits = ParcelFileDescriptor.MODE_READ_WRITE
| ParcelFileDescriptor.MODE_CREATE
| ParcelFileDescriptor.MODE_TRUNCATE;
} else {
throw new IllegalArgumentException("Invalid mode: " + mode);
}
return modeBits;
}
private static File buildPath(File base, String... segments) {
File cur = base;
for (String segment : segments) {
if (segment != null) {
cur = new File(cur, segment);
}
}
return cur;
}
private static String[] copyOf(String[] original, int newLength) {
final String[] result = new String[newLength];
System.arraycopy(original, 0, result, 0, newLength);
return result;
}
private static Object[] copyOf(Object[] original, int newLength) {
final Object[] result = new Object[newLength];
System.arraycopy(original, 0, result, 0, newLength);
return result;
}
}

View File

@@ -6,6 +6,7 @@ import android.app.Fragment;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.BadParcelableException;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
@@ -36,6 +37,8 @@ import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
public class MainActivity extends FragmentStackActivity{
private static final String TAG="MainActivity";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState){
UiUtils.setUserPreferredTheme(this);
@@ -99,11 +102,11 @@ public class MainActivity extends FragmentStackActivity{
session=AccountSessionManager.get(accountID);
if(session==null || !session.activated)
return;
openSearchQuery(uri.toString(), session.getID(), R.string.opening_link, false);
openSearchQuery(uri.toString(), session.getID(), R.string.opening_link, false, null);
}
public void openSearchQuery(String q, String accountID, int progressText, boolean fromSearch){
new GetSearchResults(q, null, true, null, 0, 0)
public void openSearchQuery(String q, String accountID, int progressText, boolean fromSearch, GetSearchResults.Type type){
new GetSearchResults(q, type, true, null, 0, 0)
.setCallback(new Callback<>(){
@Override
public void onSuccess(SearchResults result){
@@ -193,8 +196,14 @@ public class MainActivity extends FragmentStackActivity{
fragment.setArguments(args);
showFragmentClearingBackStack(fragment);
if(intent.getBooleanExtra("fromNotification", false) && intent.hasExtra("notification")){
Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
showFragmentForNotification(notification, session.getID());
// Parcelables might not be compatible across app versions so this protects against possible crashes
// when a notification was received, then the app was updated, and then the user opened the notification
try{
Notification notification=Parcels.unwrap(intent.getParcelableExtra("notification"));
showFragmentForNotification(notification, session.getID());
}catch(BadParcelableException x){
Log.w(TAG, x);
}
}else if(intent.getBooleanExtra("compose", false)){
showCompose();
}else if(Intent.ACTION_VIEW.equals(intent.getAction())){

View File

@@ -0,0 +1,38 @@
package org.joinmastodon.android;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import java.io.FileNotFoundException;
import java.util.Arrays;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class TweakedFileProvider extends FileProvider{
private static final String TAG="TweakedFileProvider";
@Override
public String getType(@NonNull Uri uri){
Log.d(TAG, "getType() called with: uri = ["+uri+"]");
if(uri.getPathSegments().get(0).equals("image_cache")){
Log.i(TAG, "getType: HERE!");
return "image/jpeg"; // might as well be a png but image decoding APIs don't care, needs to be image/* though
}
return super.getType(uri);
}
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder){
Log.d(TAG, "query() called with: uri = ["+uri+"], projection = ["+Arrays.toString(projection)+"], selection = ["+selection+"], selectionArgs = ["+Arrays.toString(selectionArgs)+"], sortOrder = ["+sortOrder+"]");
return super.query(uri, projection, selection, selectionArgs, sortOrder);
}
@Override
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException{
Log.d(TAG, "openFile() called with: uri = ["+uri+"], mode = ["+mode+"]");
return super.openFile(uri, mode);
}
}

View File

@@ -45,7 +45,11 @@ public class MastodonAPIController{
.registerTypeAdapter(LocalDate.class, new IsoLocalDateTypeAdapter())
.create();
private static WorkerThread thread=new WorkerThread("MastodonAPIController");
private static OkHttpClient httpClient=new OkHttpClient.Builder().build();
private static OkHttpClient httpClient=new OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.build();
private AccountSession session;

View File

@@ -0,0 +1,22 @@
package org.joinmastodon.android.api.requests.accounts;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.RequiredField;
import org.joinmastodon.android.model.BaseModel;
public class CheckInviteLink extends MastodonAPIRequest<CheckInviteLink.Response>{
public CheckInviteLink(String path){
super(HttpMethod.GET, path, Response.class);
addHeader("Accept", "application/json");
}
@Override
protected String getPathPrefix(){
return "";
}
public static class Response extends BaseModel{
@RequiredField
public String inviteCode;
}
}

View File

@@ -4,22 +4,23 @@ import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Token;
public class RegisterAccount extends MastodonAPIRequest<Token>{
public RegisterAccount(String username, String email, String password, String locale, String reason, String timezone){
public RegisterAccount(String username, String email, String password, String locale, String reason, String timezone, String inviteCode){
super(HttpMethod.POST, "/accounts", Token.class);
setRequestBody(new Body(username, email, password, locale, reason, timezone));
setRequestBody(new Body(username, email, password, locale, reason, timezone, inviteCode));
}
private static class Body{
public String username, email, password, locale, reason, timeZone;
public String username, email, password, locale, reason, timeZone, inviteCode;
public boolean agreement=true;
public Body(String username, String email, String password, String locale, String reason, String timeZone){
public Body(String username, String email, String password, String locale, String reason, String timeZone, String inviteCode){
this.username=username;
this.email=email;
this.password=password;
this.locale=locale;
this.reason=reason;
this.timeZone=timeZone;
this.inviteCode=inviteCode;
}
}
}

View File

@@ -29,10 +29,9 @@ import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.Translation;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.NonMutualPreReplySheet;
import org.joinmastodon.android.ui.OldPostPreReplySheet;
import org.joinmastodon.android.ui.sheets.NonMutualPreReplySheet;
import org.joinmastodon.android.ui.sheets.OldPostPreReplySheet;
import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
@@ -182,7 +181,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, new PhotoViewer.Listener(){
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, status, accountID, new PhotoViewer.Listener(){
private MediaAttachmentViewController transitioningHolder;
@Override
@@ -248,6 +247,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@Override
public void photoViewerDismissed(){
currentPhotoViewer=null;
gridHolder.itemView.setHasTransientState(false);
}
@Override
@@ -259,6 +259,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
return gridHolder.getViewController(index);
}
});
gridHolder.itemView.setHasTransientState(true);
}
@Override
@@ -659,7 +660,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
new NonMutualPreReplySheet(getActivity(), notAgain->{
GlobalUserPreferences.optOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.NON_MUTUAL, notAgain ? null : status.account, accountID);
proceed.run();
}, status.account).show();
}, status.account, accountID).show();
}else if(!GlobalUserPreferences.isOptedOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.OLD_POST, null, null) &&
status.createdAt.isBefore(Instant.now().minus(90, ChronoUnit.DAYS))){
new OldPostPreReplySheet(getActivity(), notAgain->{

View File

@@ -5,6 +5,7 @@ import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ClipData;
import android.content.Intent;
import android.content.res.Configuration;
@@ -1077,6 +1078,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
Editable e=mainEditText.getText();
int start=e.getSpanStart(currentAutocompleteSpan);
int end=e.getSpanEnd(currentAutocompleteSpan);
if(start==-1 || end==-1)
return;
e.replace(start, end, text+" ");
finishAutocomplete();
InputConnection conn=mainEditText.getCurrentInputConnection();
@@ -1118,12 +1121,15 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private void showLanguageAlert(){
Preferences prefs=AccountSessionManager.getInstance().getAccount(accountID).preferences;
ComposeLanguageAlertViewController vc=new ComposeLanguageAlertViewController(getActivity(), prefs!=null ? prefs.postingDefaultLanguage : null, postLang, mainEditText.getText().toString());
new M3AlertDialogBuilder(getActivity())
final AlertDialog dlg=new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.language)
.setView(vc.getView())
.setPositiveButton(R.string.ok, (dialog, which)->setPostLanguage(vc.getSelectedOption()))
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.cancel, null)
.show();
vc.setSelectionListener(opt->{
setPostLanguage(opt);
dlg.dismiss();
});
}
private void setPostLanguage(ComposeLanguageAlertViewController.SelectedOption language){

View File

@@ -7,10 +7,7 @@ import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.SpannableStringBuilder;
import android.text.style.BulletSpan;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
@@ -131,20 +128,9 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp
@Override
public boolean onOptionsItemSelected(MenuItem item){
if(item.getItemId()==R.id.help){
SpannableStringBuilder msg=new SpannableStringBuilder(getText(R.string.alt_text_help));
BulletSpan[] spans=msg.getSpans(0, msg.length(), BulletSpan.class);
for(BulletSpan span:spans){
BulletSpan betterSpan;
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.Q)
betterSpan=new BulletSpan(V.dp(10), UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnSurface));
else
betterSpan=new BulletSpan(V.dp(10), UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnSurface), V.dp(1.5f));
msg.setSpan(betterSpan, msg.getSpanStart(span), msg.getSpanEnd(span), msg.getSpanFlags(span));
msg.removeSpan(span);
}
new M3AlertDialogBuilder(themeWrapper)
.setTitle(R.string.what_is_alt_text)
.setMessage(msg)
.setMessage(UiUtils.fixBulletListInString(themeWrapper, R.string.alt_text_help))
.setPositiveButton(R.string.ok, null)
.show();
}
@@ -181,7 +167,7 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp
fakeAttachment.meta.width=width;
fakeAttachment.meta.height=height;
photoViewer=new PhotoViewer(getActivity(), Collections.singletonList(fakeAttachment), 0, new PhotoViewer.Listener(){
photoViewer=new PhotoViewer(getActivity(), Collections.singletonList(fakeAttachment), 0, null, accountID, new PhotoViewer.Listener(){
@Override
public void setPhotoViewVisibility(int index, boolean visible){
image.setAlpha(visible ? 1f : 0f);

View File

@@ -30,7 +30,7 @@ import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestions
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.TabBar;

View File

@@ -178,6 +178,7 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
if(refreshing)
list.scrollToPosition(0);
maxID=result.isEmpty() ? null : result.get(result.size()-1).id;
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.PUBLIC);
onDataLoaded(result, !result.isEmpty());
}
})
@@ -191,6 +192,7 @@ public class HomeTimelineFragment extends StatusListFragment implements ToolbarD
if(refreshing)
list.scrollToPosition(0);
maxID=result.isEmpty() ? null : result.get(result.size()-1).id;
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME);
onDataLoaded(result, !result.isEmpty());
}
})

View File

@@ -1,12 +1,17 @@
package org.joinmastodon.android.fragments;
import android.os.Build;
import android.os.Bundle;
import android.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.inputmethod.InputMethodManager;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import com.squareup.otto.Subscribe;
@@ -19,7 +24,7 @@ import org.joinmastodon.android.api.requests.lists.GetListAccounts;
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
import org.joinmastodon.android.events.AccountAddedToListEvent;
import org.joinmastodon.android.events.AccountRemovedFromListEvent;
import org.joinmastodon.android.fragments.account_list.AddListMembersFragment;
import org.joinmastodon.android.fragments.account_list.AddNewListMembersFragment;
import org.joinmastodon.android.fragments.account_list.PaginatedAccountListFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.FollowList;
@@ -33,24 +38,31 @@ import org.parceler.Parcels;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class ListMembersFragment extends PaginatedAccountListFragment{
private static final int ADD_MEMBER_RESULT=600;
public class ListMembersFragment extends PaginatedAccountListFragment implements AddNewListMembersFragment.Listener, OnBackPressedListener{
private ImageButton fab;
private FollowList followList;
private boolean inSelectionMode;
private Set<String> selectedAccounts=new HashSet<>();
private ActionMode actionMode;
private MenuItem deleteItem;
private FrameLayout searchFragmentContainer;
private FrameLayout fragmentContentWrap;
private AddNewListMembersFragment searchFragment;
private FragmentRootLinearLayout rootView;
private WindowInsets lastInsets;
private HashSet<String> accountIDsInList=new HashSet<>();
private boolean dismissingSearchFragment;
public ListMembersFragment(){
setListLayoutId(R.layout.recycler_fragment_with_fab);
@@ -76,6 +88,26 @@ public class ListMembersFragment extends PaginatedAccountListFragment{
return new GetListAccounts(followList.id, maxID, count);
}
@Override
protected void onDataLoaded(List<AccountViewModel> d, boolean more){
if(refreshing)
accountIDsInList.clear();
for(AccountViewModel a:d){
accountIDsInList.add(a.account.id);
}
super.onDataLoaded(d, more);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
View view=super.onCreateView(inflater, container, savedInstanceState);
FrameLayout wrapper=new FrameLayout(getActivity());
wrapper.addView(view);
rootView=(FragmentRootLinearLayout) view;
fragmentContentWrap=wrapper;
return wrapper;
}
@Override
protected void onConfigureViewHolder(AccountViewHolder holder){
super.onConfigureViewHolder(holder);
@@ -132,16 +164,19 @@ public class ListMembersFragment extends PaginatedAccountListFragment{
@Override
public void onApplyWindowInsets(WindowInsets insets){
super.onApplyWindowInsets(insets);
lastInsets=insets;
if(searchFragment!=null)
searchFragment.onApplyWindowInsets(insets);
UiUtils.applyBottomInsetToFAB(fab, insets);
}
@Override
public void onFragmentResult(int reqCode, boolean success, Bundle result){
if(reqCode==ADD_MEMBER_RESULT && success){
Account acc=Objects.requireNonNull(Parcels.unwrap(result.getParcelable("selectedAccount")));
addAccounts(List.of(acc));
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
list.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
emptyView.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
progress.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom());
}else{
list.setPadding(0, 0, 0, 0);
}
rootView.onApplyWindowInsets(insets);
}
@Subscribe
@@ -160,9 +195,25 @@ public class ListMembersFragment extends PaginatedAccountListFragment{
}
private void onFabClick(){
searchFragmentContainer=new FrameLayout(getActivity());
searchFragmentContainer.setId(R.id.search_fragment);
fragmentContentWrap.addView(searchFragmentContainer);
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.goForResult(getActivity(), AddListMembersFragment.class, args, ADD_MEMBER_RESULT, this);
args.putParcelable("list", Parcels.wrap(followList));
args.putBoolean("_can_go_back", true);
searchFragment=new AddNewListMembersFragment(this);
searchFragment.setArguments(args);
getChildFragmentManager().beginTransaction().add(R.id.search_fragment, searchFragment).commit();
getChildFragmentManager().executePendingTransactions();
if(lastInsets!=null)
searchFragment.onApplyWindowInsets(lastInsets);
searchFragmentContainer.setTranslationX(V.dp(100));
searchFragmentContainer.setAlpha(0f);
searchFragmentContainer.animate().translationX(0).alpha(1).setDuration(300).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{
rootView.setVisibility(View.GONE);
}).start();
}
private void onItemClick(AccountViewHolder holder){
@@ -198,7 +249,7 @@ public class ListMembersFragment extends PaginatedAccountListFragment{
if(id==R.id.remove_from_list){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.confirm_remove_list_member)
.setPositiveButton(R.string.remove, (dlg, which)->removeAccounts(Set.of(holder.getItem().account.id)))
.setPositiveButton(R.string.remove, (dlg, which)->removeAccounts(Set.of(holder.getItem().account.id), null))
.setNegativeButton(R.string.cancel, null)
.show();
}
@@ -229,7 +280,7 @@ public class ListMembersFragment extends PaginatedAccountListFragment{
public boolean onActionItemClicked(ActionMode mode, MenuItem item){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.confirm_remove_list_members)
.setPositiveButton(R.string.remove, (dlg, which)->removeAccounts(new HashSet<>(selectedAccounts)))
.setPositiveButton(R.string.remove, (dlg, which)->removeAccounts(new HashSet<>(selectedAccounts), null))
.setNegativeButton(R.string.cancel, null)
.show();
return true;
@@ -251,13 +302,16 @@ public class ListMembersFragment extends PaginatedAccountListFragment{
actionMode.setTitle(getResources().getQuantityString(R.plurals.x_items_selected, selectedAccounts.size(), selectedAccounts.size()));
}
private void removeAccounts(Set<String> ids){
private void removeAccounts(Set<String> ids, Runnable onDone){
new RemoveAccountsFromList(followList.id, ids)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Void result){
if(onDone!=null)
onDone.run();
if(inSelectionMode)
actionMode.finish();
accountIDsInList.removeAll(ids);
removeAccountRows(ids);
}
@@ -270,12 +324,15 @@ public class ListMembersFragment extends PaginatedAccountListFragment{
.exec(accountID);
}
private void addAccounts(Collection<Account> accounts){
private void addAccounts(Collection<Account> accounts, Runnable onDone){
new AddAccountsToList(followList.id, accounts.stream().map(a->a.id).collect(Collectors.toSet()))
.setCallback(new Callback<>(){
@Override
public void onSuccess(Void result){
if(onDone!=null)
onDone.run();
for(Account acc:accounts){
accountIDsInList.add(acc.id);
data.add(new AccountViewModel(acc, accountID));
}
list.getAdapter().notifyItemRangeInserted(data.size()-accounts.size(), accounts.size());
@@ -298,4 +355,54 @@ public class ListMembersFragment extends PaginatedAccountListFragment{
}
}
}
@Override
public boolean isAccountInList(AccountViewModel account){
return accountIDsInList.contains(account.account.id);
}
@Override
public void addAccountToList(AccountViewModel account, Runnable onDone){
addAccounts(Set.of(account.account), onDone);
}
@Override
public void removeAccountAccountFromList(AccountViewModel account, Runnable onDone){
removeAccounts(Set.of(account.account.id), onDone);
}
@Override
public boolean onBackPressed(){
if(searchFragment!=null){
dismissSearchFragment();
return true;
}
return false;
}
private void dismissSearchFragment(){
if(searchFragment==null || dismissingSearchFragment)
return;
dismissingSearchFragment=true;
rootView.setVisibility(View.VISIBLE);
searchFragmentContainer.animate().translationX(V.dp(100)).alpha(0).setDuration(200).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{
getChildFragmentManager().beginTransaction().remove(searchFragment).commit();
getChildFragmentManager().executePendingTransactions();
fragmentContentWrap.removeView(searchFragmentContainer);
searchFragmentContainer=null;
searchFragment=null;
dismissingSearchFragment=false;
}).start();
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
}
@Override
protected void setStatusBarColor(int color){
rootView.setStatusBarColor(color);
}
@Override
protected void setNavigationBarColor(int color){
rootView.setNavigationBarColor(color);
}
}

View File

@@ -314,6 +314,8 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
}
private void markAsRead(){
if(data.isEmpty())
return;
String id=data.get(0).id;
if(ObjectIdComparator.INSTANCE.compare(id, realUnreadMarker)>0){
new SaveMarkers(null, id).exec(accountID);

View File

@@ -18,13 +18,18 @@ import android.graphics.drawable.LayerDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.SpannedString;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.text.style.ImageSpan;
import android.transition.ChangeBounds;
import android.transition.Fade;
import android.transition.Transition;
import android.transition.TransitionManager;
import android.transition.TransitionSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -36,11 +41,11 @@ import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.Toolbar;
import org.joinmastodon.android.GlobalUserPreferences;
@@ -63,10 +68,12 @@ import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.SingleImagePhotoViewerListener;
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
import org.joinmastodon.android.ui.sheets.DecentralizationExplainerSheet;
import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.tabs.TabLayoutMediator;
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.text.ImageSpanThatDoesNotBreakShitForNoGoodReason;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.CoverImageView;
@@ -108,7 +115,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private ImageView avatar;
private CoverImageView cover;
private View avatarBorder;
private TextView name, username, bio, followersCount, followersLabel, followingCount, followingLabel;
private TextView name, username, usernameDomain, bio, followersCount, followersLabel, followingCount, followingLabel;
private ProgressBarButton actionButton;
private ViewPager2 pager;
private NestedRecyclerScrollView scrollView;
@@ -128,6 +135,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private View tabsDivider;
private View actionButtonWrap;
private CustomDrawingOrderLinearLayout scrollableContent;
private ImageButton qrCodeButton;
private Account account;
private String accountID;
@@ -186,6 +194,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
avatarBorder=content.findViewById(R.id.avatar_border);
name=content.findViewById(R.id.name);
username=content.findViewById(R.id.username);
usernameDomain=content.findViewById(R.id.username_domain);
bio=content.findViewById(R.id.bio);
followersCount=content.findViewById(R.id.followers_count);
followersLabel=content.findViewById(R.id.followers_label);
@@ -209,6 +218,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
tabsDivider=content.findViewById(R.id.tabs_divider);
actionButtonWrap=content.findViewById(R.id.profile_action_btn_wrap);
scrollableContent=content.findViewById(R.id.scrollable_content);
qrCodeButton=content.findViewById(R.id.qr_code);
avatar.setOutlineProvider(OutlineProviders.roundedRect(24));
avatar.setClipToOutline(true);
@@ -321,6 +331,16 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
nameEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
bioEdit.addTextChangedListener(new SimpleTextWatcher(e->editDirty=true));
usernameDomain.setOnClickListener(v->new DecentralizationExplainerSheet(getActivity(), accountID, account).show());
qrCodeButton.setOnClickListener(v->{
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("targetAccount", Parcels.wrap(account));
ProfileQrCodeFragment qf=new ProfileQrCodeFragment();
qf.setArguments(args);
qf.show(getChildFragmentManager(), "qrDialog");
});
return sizeWrapper;
}
@@ -500,22 +520,21 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
boolean isSelf=AccountSessionManager.getInstance().isSelf(accountID, account);
if(account.locked){
ssb=new SpannableStringBuilder("@");
ssb.append(account.acct);
if(isSelf){
ssb.append('@');
ssb.append(AccountSessionManager.getInstance().getAccount(accountID).domain);
}
ssb=new SpannableStringBuilder(account.username);
ssb.append(" ");
Drawable lock=username.getResources().getDrawable(R.drawable.ic_lock_fill1_20px, getActivity().getTheme()).mutate();
lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight());
lock.setTint(username.getCurrentTextColor());
ssb.append(getString(R.string.manually_approves_followers), new ImageSpan(lock, ImageSpan.ALIGN_BOTTOM), 0);
ssb.append(getString(R.string.manually_approves_followers), new ImageSpanThatDoesNotBreakShitForNoGoodReason(lock, ImageSpan.ALIGN_BOTTOM), 0);
username.setText(ssb);
}else{
// noinspection SetTextI18n
username.setText('@'+account.acct+(isSelf ? ('@'+AccountSessionManager.getInstance().getAccount(accountID).domain) : ""));
username.setText(account.username);
}
String domain=account.getDomain();
if(TextUtils.isEmpty(domain))
domain=AccountSessionManager.get(accountID).domain;
usernameDomain.setText(domain);
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
if(TextUtils.isEmpty(parsedBio)){
bio.setVisibility(View.GONE);
@@ -580,6 +599,13 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
}
private CharSequence makeRedString(CharSequence s){
int color=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Error);
SpannableString ss=new SpannableString(s);
ss.setSpan(new ForegroundColorSpan(color), 0, ss.length(), 0);
return ss;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
if(isOwnProfile && isInEditMode){
@@ -596,28 +622,29 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(isOwnProfile)
return;
menu.findItem(R.id.mute).setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.displayName));
menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.displayName));
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.displayName));
menu.findItem(R.id.mute).setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getDisplayUsername()));
menu.findItem(R.id.block).setTitle(makeRedString(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getDisplayUsername())));
menu.findItem(R.id.report).setTitle(makeRedString(getString(R.string.report_user, account.getDisplayUsername())));
if(relationship.following)
menu.findItem(R.id.hide_boosts).setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.displayName));
menu.findItem(R.id.hide_boosts).setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user));
else
menu.findItem(R.id.hide_boosts).setVisible(false);
if(!account.isLocal())
menu.findItem(R.id.block_domain).setTitle(getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain()));
menu.findItem(R.id.block_domain).setTitle(makeRedString(getString(relationship.domainBlocking ? R.string.unblock_domain : R.string.block_domain, account.getDomain())));
else
menu.findItem(R.id.block_domain).setVisible(false);
menu.findItem(R.id.add_to_list).setVisible(relationship.following);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P){
menu.setGroupDividerEnabled(true);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
int id=item.getItemId();
if(id==R.id.share){
Intent intent=new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, account.url);
startActivity(Intent.createChooser(intent, item.getTitle()));
UiUtils.openSystemShareSheet(getActivity(), account);
}else if(id==R.id.mute){
confirmToggleMuted();
}else if(id==R.id.block){
@@ -631,10 +658,10 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}else if(id==R.id.open_in_browser){
UiUtils.launchWebBrowser(getActivity(), account.url);
}else if(id==R.id.block_domain){
UiUtils.confirmToggleBlockDomain(getActivity(), accountID, account.getDomain(), relationship.domainBlocking, ()->{
UiUtils.confirmToggleBlockDomain(getActivity(), accountID, account, relationship.domainBlocking, ()->{
relationship.domainBlocking=!relationship.domainBlocking;
updateRelationship();
});
}, this::updateRelationship);
}else if(id==R.id.hide_boosts){
new SetAccountFollowed(account.id, true, !relationship.showingReblogs)
.setCallback(new Callback<>(){
@@ -833,17 +860,48 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
toolbar.setNavigationContentDescription(R.string.discard);
ViewGroup parent=contentView.findViewById(R.id.scrollable_content);
Runnable updater=new Runnable(){
@Override
public void run(){
// setPadding() calls nullLayouts() internally, forcing the text layout to update
actionButton.setPadding(actionButton.getPaddingLeft(), 1, actionButton.getPaddingRight(), 0);
actionButton.setPadding(actionButton.getPaddingLeft(), 0, actionButton.getPaddingRight(), 0);
actionButton.measure(actionButton.getWidth()|View.MeasureSpec.EXACTLY, actionButton.getHeight()|View.MeasureSpec.EXACTLY);
actionButton.postOnAnimation(this);
}
};
actionButton.postOnAnimation(updater);
TransitionManager.beginDelayedTransition(parent, new TransitionSet()
.addTransition(new Fade(Fade.IN | Fade.OUT))
.addTransition(new ChangeBounds())
.setDuration(250)
.setInterpolator(CubicBezierInterpolator.DEFAULT)
.addListener(new Transition.TransitionListener(){
@Override
public void onTransitionStart(Transition transition){}
@Override
public void onTransitionEnd(Transition transition){
actionButton.removeCallbacks(updater);
}
@Override
public void onTransitionCancel(Transition transition){}
@Override
public void onTransitionPause(Transition transition){}
@Override
public void onTransitionResume(Transition transition){}
})
);
name.setVisibility(View.GONE);
username.setVisibility(View.GONE);
name.setVisibility(View.INVISIBLE);
username.setVisibility(View.INVISIBLE);
bio.setVisibility(View.GONE);
countersLayout.setVisibility(View.GONE);
qrCodeButton.setVisibility(View.GONE);
usernameDomain.setVisibility(View.INVISIBLE);
nameEditWrap.setVisibility(View.VISIBLE);
nameEdit.setText(account.displayName);
@@ -882,11 +940,40 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
editSaveMenuItem=null;
ViewGroup parent=contentView.findViewById(R.id.scrollable_content);
Runnable updater=new Runnable(){
@Override
public void run(){
// setPadding() calls nullLayouts() internally, forcing the text layout to update
actionButton.setPadding(actionButton.getPaddingLeft(), 1, actionButton.getPaddingRight(), 0);
actionButton.setPadding(actionButton.getPaddingLeft(), 0, actionButton.getPaddingRight(), 0);
actionButton.measure(actionButton.getWidth()|View.MeasureSpec.EXACTLY, actionButton.getHeight()|View.MeasureSpec.EXACTLY);
actionButton.postOnAnimation(this);
}
};
actionButton.postOnAnimation(updater);
TransitionManager.beginDelayedTransition(parent, new TransitionSet()
.addTransition(new Fade(Fade.IN | Fade.OUT))
.addTransition(new ChangeBounds())
.setDuration(250)
.setInterpolator(CubicBezierInterpolator.DEFAULT)
.addListener(new Transition.TransitionListener(){
@Override
public void onTransitionStart(Transition transition){}
@Override
public void onTransitionEnd(Transition transition){
actionButton.removeCallbacks(updater);
}
@Override
public void onTransitionCancel(Transition transition){}
@Override
public void onTransitionPause(Transition transition){}
@Override
public void onTransitionResume(Transition transition){}
})
);
nameEditWrap.setVisibility(View.GONE);
bioEditWrap.setVisibility(View.GONE);
@@ -895,6 +982,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
bio.setVisibility(View.VISIBLE);
countersLayout.setVisibility(View.VISIBLE);
refreshLayout.setEnabled(true);
usernameDomain.setVisibility(View.VISIBLE);
qrCodeButton.setVisibility(View.VISIBLE);
bindHeaderView();
V.setVisibilityAnimated(fab, View.VISIBLE);
@@ -977,7 +1066,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return;
int radius=V.dp(25);
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(account.avatar, ava), 0,
new SingleImagePhotoViewerListener(avatar, avatarBorder, new int[]{radius, radius, radius, radius}, this, ()->currentPhotoViewer=null, ()->ava, null, null));
null, accountID, new SingleImagePhotoViewerListener(avatar, avatarBorder, new int[]{radius, radius, radius, radius}, this, ()->currentPhotoViewer=null, ()->ava, null, null));
}
}
@@ -989,7 +1078,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(drawable==null || drawable instanceof ColorDrawable)
return;
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(account.header, drawable), 0,
new SingleImagePhotoViewerListener(cover, cover, null, this, ()->currentPhotoViewer=null, ()->drawable, ()->avatarBorder.setTranslationZ(2), ()->avatarBorder.setTranslationZ(0)));
null, accountID, new SingleImagePhotoViewerListener(cover, cover, null, this, ()->currentPhotoViewer=null, ()->drawable, ()->avatarBorder.setTranslationZ(2), ()->avatarBorder.setTranslationZ(0)));
}
}

View File

@@ -0,0 +1,679 @@
package org.joinmastodon.android.fragments;
import android.Manifest;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Dialog;
import android.app.DownloadManager;
import android.app.ProgressDialog;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.RemoteException;
import android.os.SystemClock;
import android.provider.MediaStore;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.google.android.gms.common.Feature;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.common.moduleinstall.ModuleAvailabilityResponse;
import com.google.android.gms.common.moduleinstall.ModuleInstallIntentResponse;
import com.google.android.gms.common.moduleinstall.ModuleInstallResponse;
import com.google.android.gms.common.moduleinstall.ModuleInstallStatusUpdate;
import com.google.android.gms.common.moduleinstall.internal.ApiFeatureRequest;
import com.google.android.gms.common.moduleinstall.internal.IModuleInstallCallbacks;
import com.google.android.gms.common.moduleinstall.internal.IModuleInstallService;
import com.google.android.gms.common.moduleinstall.internal.IModuleInstallStatusListener;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.googleservices.GmsClient;
import org.joinmastodon.android.googleservices.barcodescanner.Barcode;
import org.joinmastodon.android.googleservices.barcodescanner.BarcodeScanner;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.Snackbar;
import org.joinmastodon.android.ui.drawables.FancyQrCodeDrawable;
import org.joinmastodon.android.ui.drawables.RadialParticleSystemDrawable;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.FixedAspectRatioFrameLayout;
import org.parceler.Parcels;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.CustomViewHelper;
import me.grishka.appkit.utils.V;
public class ProfileQrCodeFragment extends AppKitFragment{
private static final String TAG="ProfileQrCodeFragment";
private static final int PERMISSION_RESULT=388;
private static final int SCAN_RESULT=439;
private Context themeWrapper;
private GradientDrawable scrim=new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, new int[]{0xE6000000, 0xD9000000});
private RadialParticleSystemDrawable particles;
private View codeContainer;
private View particleAnimContainer;
private Animator currentTransition;
private View saveBtn;
private TextView saveBtnText;
private String accountID;
private Account account;
private String accountDomain;
private Intent scannerIntent;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setStyle(STYLE_NO_FRAME, 0);
setHasOptionsMenu(true);
accountID=getArguments().getString("account");
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
setCancelable(false);
scannerIntent=BarcodeScanner.createIntent(Barcode.FORMAT_QR_CODE, false, true);
}
@Override
public void onStart(){
super.onStart();
Dialog dlg=getDialog();
dlg.getWindow().setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT);
dlg.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR);
dlg.getWindow().setNavigationBarColor(0);
dlg.getWindow().setStatusBarColor(0);
WindowManager.LayoutParams lp=dlg.getWindow().getAttributes();
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.P){
lp.layoutInDisplayCutoutMode=WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
}
dlg.getWindow().setAttributes(lp);
if(!isTablet){
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
dlg.setOnKeyListener((dialog, keyCode, event)->{
if(keyCode==KeyEvent.KEYCODE_BACK && event.getAction()==KeyEvent.ACTION_DOWN){
dismiss();
}
return true;
});
}
@Override
public void onDismiss(DialogInterface dialog){
super.onDismiss(dialog);
Activity activity=getActivity();
if(activity!=null)
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
themeWrapper=new ContextThemeWrapper(activity, R.style.Theme_Mastodon_Dark);
}
@SuppressLint("ClickableViewAccessibility")
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){
View content=View.inflate(themeWrapper, R.layout.fragment_profile_qr, container);
View decor=getDialog().getWindow().getDecorView();
decor.setOnApplyWindowInsetsListener((v, insets)->{
content.setPadding(insets.getStableInsetLeft(), insets.getStableInsetTop(), insets.getStableInsetRight(), insets.getStableInsetBottom());
return insets.consumeStableInsets();
});
int flags=decor.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
flags&=~(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
decor.setSystemUiVisibility(flags);
content.setBackground(scrim);
String url=account.url;
QRCodeWriter writer=new QRCodeWriter();
BitMatrix code;
try{
code=writer.encode(url, BarcodeFormat.QR_CODE, 0, 0, Map.of(EncodeHintType.MARGIN, 0, EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H));
}catch(WriterException e){
throw new RuntimeException(e);
}
View codeView=content.findViewById(R.id.code);
ImageView avatar=content.findViewById(R.id.avatar);
TextView username=content.findViewById(R.id.username);
TextView domain=content.findViewById(R.id.domain);
View share=content.findViewById(R.id.share_btn);
saveBtn=content.findViewById(R.id.save_btn);
saveBtnText=content.findViewById(R.id.save_text);
View cornerAnimContainer=content.findViewById(R.id.corner_animation_container);
particleAnimContainer=content.findViewById(R.id.particle_animation_container);
codeContainer=content.findViewById(R.id.code_container);
if(!TextUtils.isEmpty(account.avatar)){
ViewImageLoader.loadWithoutAnimation(avatar, getResources().getDrawable(R.drawable.image_placeholder, getActivity().getTheme()), new UrlImageLoaderRequest(Bitmap.Config.ARGB_8888, V.dp(24), V.dp(24), List.of(), Uri.parse(account.avatarStatic)));
}
username.setText(account.username);
String accDomain=account.getDomain();
domain.setText(accountDomain=TextUtils.isEmpty(accDomain) ? AccountSessionManager.get(accountID).domain : accDomain);
Drawable logo=getResources().getDrawable(R.drawable.ic_ntf_logo, themeWrapper.getTheme()).mutate();
logo.setTint(UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnPrimary));
codeView.setBackground(new FancyQrCodeDrawable(code, UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnPrimary), logo));
share.setOnClickListener(v->{
UiUtils.openSystemShareSheet(getActivity(), account);
});
saveBtn.setOnClickListener(v->saveCodeAsFile());
cornerAnimContainer.setBackground(new AnimatedCornersDrawable(themeWrapper));
int particleColor=UiUtils.getThemeColor(themeWrapper, R.attr.colorM3Primary);
particles=new RadialParticleSystemDrawable(5000, 200, (particleColor & 0xFFFFFF) | 0x80000000, particleColor & 0xFFFFFF, V.dp(65), V.dp(50), getResources().getDisplayMetrics().density);
particleAnimContainer.setBackground(particles);
content.setOnTouchListener(new TouchDismissListener());
int buttonExtraWidth=saveBtn.getPaddingLeft()+saveBtn.getPaddingRight()+saveBtnText.getCompoundDrawablesRelative()[0].getIntrinsicWidth()+saveBtnText.getCompoundDrawablePadding();
saveBtn.getLayoutParams().width=(int)Math.max(saveBtnText.getPaint().measureText(getString(R.string.save)), saveBtnText.getPaint().measureText(getString(R.string.saved)))+buttonExtraWidth;
return content;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
if(savedInstanceState==null){
AnimatorSet set=new AnimatorSet();
set.playTogether(
ObjectAnimator.ofInt(scrim, "alpha", 0, 255),
ObjectAnimator.ofFloat(particleAnimContainer, View.TRANSLATION_Y, V.dp(50), 0),
ObjectAnimator.ofFloat(particleAnimContainer, View.ALPHA, 0, 1),
ObjectAnimator.ofFloat(getToolbar(), View.ALPHA, 0, 1)
);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.setDuration(350);
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
currentTransition=null;
}
});
currentTransition=set;
set.start();
}
}
@Override
public void dismiss(){
dismissWithAnimation(super::dismiss, true);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
if(GmsClient.isGooglePlayServicesAvailable(getActivity())){
MenuItem item=menu.add(0, 0, 0, R.string.scan_qr_code);
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
item.setIcon(R.drawable.ic_qr_code_scanner_24px);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
if(scannerIntent.resolveActivity(getActivity().getPackageManager())!=null){
startActivityForResult(scannerIntent, SCAN_RESULT);
}else{
ProgressDialog progress=new ProgressDialog(getActivity());
progress.setMessage(getString(R.string.loading));
progress.setCancelable(false);
progress.show();
GmsClient.getModuleInstallerService(getActivity(), new GmsClient.ServiceConnectionCallback<>(){
@Override
public void onSuccess(IModuleInstallService service, int connectionID){
ApiFeatureRequest req=new ApiFeatureRequest();
req.callingPackage=getActivity().getPackageName();
Feature feature=new Feature();
feature.name="mlkit.barcode.ui";
feature.version=1;
feature.oldVersion=-1;
req.features=List.of(feature);
req.urgent=true;
try{
service.installModules(new IModuleInstallCallbacks.Stub(){
@Override
public void onModuleAvailabilityResponse(Status status, ModuleAvailabilityResponse response) throws RemoteException{}
@Override
public void onModuleInstallResponse(Status status, ModuleInstallResponse response) throws RemoteException{}
@Override
public void onModuleInstallIntentResponse(Status status, ModuleInstallIntentResponse response) throws RemoteException{}
@Override
public void onStatus(Status status) throws RemoteException{}
}, req, new IModuleInstallStatusListener.Stub(){
@Override
public void onModuleInstallStatusUpdate(ModuleInstallStatusUpdate statusUpdate) throws RemoteException{
if(statusUpdate.installState==ModuleInstallStatusUpdate.STATE_COMPLETED){
Runnable r=new Runnable(){
@Override
public void run(){
if(scannerIntent.resolveActivity(getActivity().getPackageManager())!=null){
progress.dismiss();
startActivityForResult(scannerIntent, SCAN_RESULT);
}else{
codeContainer.postDelayed(this, 100);
}
}
};
getActivity().runOnUiThread(r);
GmsClient.disconnectFromService(getActivity(), connectionID);
}else if(statusUpdate.installState==ModuleInstallStatusUpdate.STATE_FAILED || statusUpdate.installState==ModuleInstallStatusUpdate.STATE_CANCELED){
getActivity().runOnUiThread(()->{
progress.dismiss();
Toast.makeText(themeWrapper, R.string.error, Toast.LENGTH_SHORT).show();
});
GmsClient.disconnectFromService(getActivity(), connectionID);
}
}
});
}catch(RemoteException e){
Log.e(TAG, "onSuccess: ", e);
getActivity().runOnUiThread(()->{
progress.dismiss();
Toast.makeText(themeWrapper, R.string.error, Toast.LENGTH_SHORT).show();
});
GmsClient.disconnectFromService(getActivity(), connectionID);
}
}
@Override
public void onError(Exception error){
Log.e(TAG, "onError() called with: error = ["+error+"]");
Toast.makeText(themeWrapper, R.string.error, Toast.LENGTH_SHORT).show();
progress.dismiss();
}
});
}
return true;
}
@Override
protected boolean canGoBack(){
return true;
}
@Override
public void onToolbarNavigationClick(){
dismiss();
}
@Override
public boolean wantsCustomNavigationIcon(){
return true;
}
@Override
protected int getNavigationIconDrawableResource(){
return R.drawable.ic_baseline_close_24;
}
@Override
protected LayoutInflater getToolbarLayoutInflater(){
return LayoutInflater.from(themeWrapper);
}
@Override
protected int getToolbarResource(){
return R.layout.profile_qr_toolbar;
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults){
if(requestCode==PERMISSION_RESULT){
if(grantResults[0]==PackageManager.PERMISSION_GRANTED){
doSaveCodeAsFile();
}else if(!getActivity().shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.permission_required)
.setMessage(R.string.storage_permission_to_download)
.setPositiveButton(R.string.open_settings, (dialog, which)->getActivity().startActivity(new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", getActivity().getPackageName(), null))))
.setNegativeButton(R.string.cancel, null)
.show();
}
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data){
if(requestCode==SCAN_RESULT && resultCode==Activity.RESULT_OK && BarcodeScanner.isValidResult(data)){
Barcode code=BarcodeScanner.getResult(data);
if(code!=null){
if(code.rawValue.startsWith("https:") || code.rawValue.startsWith("http:")){
((MainActivity)getActivity()).handleURL(Uri.parse(code.rawValue), accountID);
dismiss();
}else{
Toast.makeText(themeWrapper, R.string.link_not_supported, Toast.LENGTH_SHORT).show();
}
}
}
}
private void dismissWithAnimation(Runnable onDone, boolean animateTranslationDown){
if(currentTransition!=null)
currentTransition.cancel();
AnimatorSet set=new AnimatorSet();
set.playTogether(
ObjectAnimator.ofInt(scrim, "alpha", 0),
ObjectAnimator.ofFloat(particleAnimContainer, View.TRANSLATION_Y, particleAnimContainer.getTranslationY()+V.dp(animateTranslationDown ? 50 : -50)),
ObjectAnimator.ofFloat(particleAnimContainer, View.ALPHA, 0),
ObjectAnimator.ofFloat(getToolbar(), View.ALPHA, 0)
);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.setDuration(200);
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
onDone.run();
}
});
currentTransition=set;
set.start();
}
private void saveCodeAsFile(){
if(Build.VERSION.SDK_INT>=29){
doSaveCodeAsFile();
}else{
if(getActivity().checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)!=PackageManager.PERMISSION_GRANTED){
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_RESULT);
}else{
doSaveCodeAsFile();
}
}
}
private void doSaveCodeAsFile(){
Bitmap bmp=Bitmap.createBitmap(1080, 1080, Bitmap.Config.ARGB_8888);
Canvas c=new Canvas(bmp);
float factor=1080f/codeContainer.getWidth();
c.scale(factor, factor);
codeContainer.draw(c);
Activity activity=getActivity();
MastodonAPIController.runInBackground(()->{
String fileName=account.username+"_"+accountDomain+".png";
try(OutputStream os=destinationStreamForFile(fileName)){
bmp.compress(Bitmap.CompressFormat.PNG, 100, os);
activity.runOnUiThread(()->{
saveBtn.setEnabled(false);
saveBtnText.setText(R.string.saved);
saveBtnText.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check_20px, 0, 0, 0);
new Snackbar.Builder(activity)
.setText(R.string.image_saved)
.setAction(R.string.view_file, ()->startActivity(new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)))
.show();
});
if(Build.VERSION.SDK_INT<29){
File dstFile=new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName);
MediaScannerConnection.scanFile(activity, new String[]{dstFile.getAbsolutePath()}, new String[]{"image/png"}, null);
}
}catch(IOException x){
activity.runOnUiThread(()->{
new Snackbar.Builder(activity)
.setText(R.string.error_saving_file)
.show();
});
}
});
}
private OutputStream destinationStreamForFile(String fileName) throws IOException{
if(Build.VERSION.SDK_INT>=29){
ContentValues values=new ContentValues();
values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);
values.put(MediaStore.MediaColumns.MIME_TYPE, "image/png");
ContentResolver cr=getActivity().getContentResolver();
Uri itemUri=cr.insert(MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY), values);
return cr.openOutputStream(itemUri);
}else{
return new FileOutputStream(new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName));
}
}
@Override
public void onConfigurationChanged(Configuration newConfig){
super.onConfigurationChanged(newConfig);
codeContainer.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
codeContainer.getViewTreeObserver().removeOnPreDrawListener(this);
updateParticleEmitter();
return true;
}
});
}
private void updateParticleEmitter(){
int[] loc={0, 0};
particleAnimContainer.getLocationInWindow(loc);
int x=loc[0], y=loc[1];
codeContainer.getLocationInWindow(loc);
int cx=loc[0]-x+codeContainer.getWidth()/2;
int cy=loc[1]-y+codeContainer.getHeight()/2;
int r=codeContainer.getWidth()/2-V.dp(10);
particles.setEmitterPosition(cx, cy);
particles.setClipOutBounds(cx-r, cy-r, cx+r, cy+r);
}
public static class CustomizedLinearLayout extends LinearLayout implements CustomViewHelper{
public CustomizedLinearLayout(Context context){
this(context, null);
}
public CustomizedLinearLayout(Context context, AttributeSet attrs){
this(context, attrs, 0);
}
public CustomizedLinearLayout(Context context, AttributeSet attrs, int defStyle){
super(context, attrs, defStyle);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
int maxW=dp(400);
FixedAspectRatioFrameLayout aspectLayout=(FixedAspectRatioFrameLayout) getChildAt(0);
if(MeasureSpec.getSize(widthMeasureSpec)>maxW){
widthMeasureSpec=MeasureSpec.getMode(widthMeasureSpec) | maxW;
aspectLayout.setUseHeight(MeasureSpec.getSize(heightMeasureSpec)<dp(464));
}else{
aspectLayout.setUseHeight(false);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
private class AnimatedCornersDrawable extends Drawable{
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
private RectF tmpRect=new RectF();
public AnimatedCornersDrawable(Context context){
paint.setColor(UiUtils.getThemeColor(context, R.attr.colorM3Primary));
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeWidth(V.dp(4));
}
@Override
public void draw(@NonNull Canvas canvas){
float inset=V.dp(24);
float radius=V.dp(40);
float animProgress=((float)Math.sin(Math.toRadians(SystemClock.uptimeMillis()/16.6%360.0))+1f)/2f;
tmpRect.set(getBounds());
tmpRect.inset(inset, inset);
canvas.save();
float factor=1f+0.025f*animProgress;
paint.setStrokeWidth(V.dp(4)/factor);
canvas.scale(factor, factor, tmpRect.centerX(), tmpRect.centerY());
canvas.drawRoundRect(tmpRect, radius, radius, paint);
canvas.restore();
invalidateSelf();
}
@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){
super.onBoundsChange(bounds);
float inset=V.dp(24);
float radius=V.dp(40);
float additionalLength=V.dp(40);
tmpRect.set(getBounds());
tmpRect.inset(inset, inset);
float[] intervals=new float[]{3.1415f*radius*0.5f+additionalLength*2f, tmpRect.width()-radius*2f-additionalLength*2f};
paint.setPathEffect(new DashPathEffect(intervals, intervals[0]-additionalLength));
updateParticleEmitter();
}
}
private class TouchDismissListener implements View.OnTouchListener{
private Rect tmpRect=new Rect();
private int[] xy={0, 0};
private boolean dragging;
private float dragDownY;
private VelocityTracker velocityTracker;
private SpringAnimation springBackAnim;
@Override
public boolean onTouch(View v, MotionEvent ev){
if(ev.getAction()==MotionEvent.ACTION_DOWN){
codeContainer.getLocationInWindow(xy);
tmpRect.set(xy[0], xy[1], xy[0]+codeContainer.getWidth(), xy[1]+codeContainer.getHeight());
if(springBackAnim!=null){
springBackAnim.skipToEnd();
}
if(tmpRect.contains((int)ev.getX(), (int)ev.getY())){
dragging=true;
dragDownY=ev.getY();
velocityTracker=VelocityTracker.obtain();
velocityTracker.addMovement(ev);
}else{
dismiss();
}
}else if(dragging){
if(ev.getAction()==MotionEvent.ACTION_MOVE){
float transY=ev.getY()-dragDownY;
particleAnimContainer.setTranslationY(transY);
float alpha=1f-Math.abs(transY)/particleAnimContainer.getHeight();
scrim.setAlpha(Math.round(alpha*255));
getToolbar().setAlpha(alpha);
velocityTracker.addMovement(ev);
}else if(ev.getAction()==MotionEvent.ACTION_UP){
dragging=false;
velocityTracker.addMovement(ev);
velocityTracker.computeCurrentVelocity(1000);
float velocity=velocityTracker.getYVelocity();
if(Math.abs(velocity)>=V.dp(1000) || Math.abs(particleAnimContainer.getTranslationY())>particleAnimContainer.getHeight()/4f){
dismissWithAnimation(ProfileQrCodeFragment.super::dismiss, velocity>0);
}else{
springBack(velocity);
}
velocityTracker.recycle();
velocityTracker=null;
}else if(ev.getAction()==MotionEvent.ACTION_CANCEL){
dragging=false;
springBack(velocityTracker.getYVelocity());
velocityTracker.recycle();
velocityTracker=null;
}
}
return true;
}
private void springBack(float velocityY){
SpringAnimation anim=new SpringAnimation(particleAnimContainer, DynamicAnimation.TRANSLATION_Y, 0);
anim.getSpring().setStiffness(SpringForce.STIFFNESS_LOW).setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY);
anim.setStartVelocity(velocityY);
anim.addEndListener((animation, canceled, value, velocity)->springBackAnim=null);
anim.addUpdateListener((animation, value, velocity)->{
float alpha=1f-Math.abs(particleAnimContainer.getTranslationY())/particleAnimContainer.getHeight();
scrim.setAlpha(Math.round(alpha*255));
getToolbar().setAlpha(alpha);
});
springBackAnim=anim;
anim.start();
}
}
}

View File

@@ -1,7 +1,12 @@
package org.joinmastodon.android.fragments;
import android.app.ProgressDialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -11,6 +16,8 @@ import android.widget.ProgressBar;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.requests.accounts.CheckInviteLink;
import org.joinmastodon.android.api.requests.catalog.GetCatalogDefaultInstances;
import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.fragments.onboarding.InstanceCatalogSignupFragment;
@@ -20,6 +27,7 @@ import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.catalog.CatalogDefaultInstance;
import org.joinmastodon.android.ui.InterpolatingMotionEffect;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.joinmastodon.android.ui.views.SizeListenerFrameLayout;
@@ -48,6 +56,9 @@ public class SplashFragment extends AppKitFragment{
private ProgressBar defaultServerProgress;
private String chosenDefaultServer=DEFAULT_SERVER;
private boolean loadingDefaultServer, loadedDefaultServer;
private Uri currentInviteLink;
private ProgressDialog instanceLoadingProgress;
private String inviteCode;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -110,19 +121,65 @@ public class SplashFragment extends AppKitFragment{
Bundle extras=new Bundle();
boolean isSignup=v.getId()==R.id.btn_get_started;
extras.putBoolean("signup", isSignup);
extras.putString("defaultServer", chosenDefaultServer);
Nav.go(getActivity(), isSignup ? InstanceCatalogSignupFragment.class : InstanceChooserLoginFragment.class, extras);
}
private void onJoinDefaultServerClick(View v){
if(loadingDefaultServer)
return;
instanceLoadingProgress=new ProgressDialog(getActivity());
instanceLoadingProgress.setCancelable(false);
instanceLoadingProgress.setMessage(getString(R.string.loading_instance));
instanceLoadingProgress.show();
if(currentInviteLink!=null){
new CheckInviteLink(currentInviteLink.getPath())
.setCallback(new Callback<>(){
@Override
public void onSuccess(CheckInviteLink.Response result){
inviteCode=result.inviteCode;
proceedWithServerDomain(currentInviteLink.getHost());
}
@Override
public void onError(ErrorResponse error){
if(getActivity()==null)
return;
instanceLoadingProgress.dismiss();
instanceLoadingProgress=null;
if(error instanceof MastodonErrorResponse mer){
switch(mer.httpStatus){
case 401 -> new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.expired_invite_link)
.setMessage(getString(R.string.expired_clipboard_invite_link_alert, currentInviteLink.getHost(), chosenDefaultServer))
.setPositiveButton(R.string.ok, null)
.show();
case 404 -> new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.invalid_invite_link)
.setMessage(getString(R.string.invalid_clipboard_invite_link_alert, currentInviteLink.getHost(), chosenDefaultServer))
.setPositiveButton(R.string.ok, null)
.show();
default -> error.showToast(getActivity());
}
}
}
})
.execNoAuth(currentInviteLink.getHost());
return;
}
proceedWithServerDomain(chosenDefaultServer);
}
private void proceedWithServerDomain(String domain){
new GetInstance()
.setCallback(new Callback<>(){
@Override
public void onSuccess(Instance result){
if(getActivity()==null)
return;
if(!result.registrations){
instanceLoadingProgress.dismiss();
instanceLoadingProgress=null;
if(!result.registrations && TextUtils.isEmpty(inviteCode)){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(R.string.instance_signup_closed)
@@ -132,6 +189,8 @@ public class SplashFragment extends AppKitFragment{
}
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(result));
if(inviteCode!=null)
args.putString("inviteCode", inviteCode);
Nav.go(getActivity(), InstanceRulesFragment.class, args);
}
@@ -139,11 +198,12 @@ public class SplashFragment extends AppKitFragment{
public void onError(ErrorResponse error){
if(getActivity()==null)
return;
instanceLoadingProgress.dismiss();
instanceLoadingProgress=null;
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading_instance, true)
.execNoAuth(chosenDefaultServer);
.execNoAuth(domain);
}
private void onLearnMoreClick(View v){
@@ -198,9 +258,18 @@ public class SplashFragment extends AppKitFragment{
}
private void loadAndChooseDefaultServer(){
loadingDefaultServer=true;
defaultServerButton.setTextVisible(false);
defaultServerProgress.setVisibility(View.VISIBLE);
ClipData clipData=getActivity().getSystemService(ClipboardManager.class).getPrimaryClip();
if(clipData!=null && clipData.getItemCount()>0){
CharSequence clipText=clipData.getItemAt(0).coerceToText(getActivity());
if(HtmlParser.INVITE_LINK_PATTERN.matcher(clipText).find()){
currentInviteLink=Uri.parse(clipText.toString());
defaultServerButton.setText(getString(R.string.join_server_x_with_invite, currentInviteLink.getHost()));
}
}else{
loadingDefaultServer=true;
defaultServerButton.setTextVisible(false);
defaultServerProgress.setVisibility(View.VISIBLE);
}
new GetCatalogDefaultInstances()
.setCallback(new Callback<>(){
@Override
@@ -243,7 +312,7 @@ public class SplashFragment extends AppKitFragment{
chosenDefaultServer=domain;
loadingDefaultServer=false;
loadedDefaultServer=true;
if(defaultServerButton!=null && getActivity()!=null){
if(defaultServerButton!=null && getActivity()!=null && currentInviteLink==null){
defaultServerButton.setTextVisible(true);
defaultServerProgress.setVisibility(View.GONE);
defaultServerButton.setText(getString(R.string.join_default_server, chosenDefaultServer));

View File

@@ -151,7 +151,7 @@ public class ThreadFragment extends StatusListFragment{
replyButton.setOnClickListener(v->openReply());
Account self=AccountSessionManager.get(accountID).self;
if(!TextUtils.isEmpty(self.avatar)){
ViewImageLoader.loadWithoutAnimation(replyButtonAva, getResources().getDrawable(R.drawable.image_placeholder), new UrlImageLoaderRequest(self.avatar, V.dp(24), V.dp(24)));
ViewImageLoader.loadWithoutAnimation(replyButtonAva, getResources().getDrawable(R.drawable.image_placeholder, getActivity().getTheme()), new UrlImageLoaderRequest(self.avatar, V.dp(24), V.dp(24)));
}
UiUtils.loadCustomEmojiInTextView(toolbarTitleView);
showContent();

View File

@@ -1,37 +0,0 @@
package org.joinmastodon.android.fragments.account_list;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.SearchAccounts;
import org.joinmastodon.android.model.Account;
import java.util.List;
import me.grishka.appkit.api.SimpleCallback;
public class AddListMembersFragment extends AccountSearchFragment{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
dataLoaded();
}
@Override
protected void doLoadData(int offset, int count){
refreshing=true;
currentRequest=new SearchAccounts(currentQuery, 0, 0, false, true)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Account> result){
AddListMembersFragment.this.onSuccess(result);
}
})
.exec(accountID);
}
@Override
protected String getSearchViewPlaceholder(){
return getString(R.string.search_among_people_you_follow);
}
}

View File

@@ -38,6 +38,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
protected HashMap<String, Relationship> relationships=new HashMap<>();
protected String accountID;
protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>();
protected int itemLayoutRes=R.layout.item_account_list;
public BaseAccountListFragment(){
super(40);
@@ -151,7 +152,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
@NonNull
@Override
public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
AccountViewHolder holder=new AccountViewHolder(BaseAccountListFragment.this, parent, relationships);
AccountViewHolder holder=new AccountViewHolder(BaseAccountListFragment.this, parent, relationships, itemLayoutRes);
onConfigureViewHolder(holder);
return holder;
}

View File

@@ -408,7 +408,7 @@ public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultVi
if(q.lastIndexOf('@')==0){
q+="@"+AccountSessionManager.get(accountID).domain;
}
((MainActivity)getActivity()).openSearchQuery(q, accountID, R.string.loading, true);
((MainActivity)getActivity()).openSearchQuery(q, accountID, R.string.loading, true, GetSearchResults.Type.ACCOUNTS);
}
private void onGoToStatusSearchClick(ListItem<?> item_){

View File

@@ -24,7 +24,7 @@ import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.File;

View File

@@ -137,6 +137,9 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
protected void onButtonClick(){
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
if(getArguments().containsKey("inviteCode")){
args.putString("inviteCode", getArguments().getString("inviteCode"));
}
Nav.goForResult(getActivity(), SignupFragment.class, args, SIGNUP_REQUEST, this);
}

View File

@@ -3,7 +3,6 @@ package org.joinmastodon.android.fragments.onboarding;
import android.app.Activity;
import android.app.ProgressDialog;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.KeyEvent;
@@ -37,6 +36,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import javax.xml.parsers.DocumentBuilderFactory;
@@ -48,7 +48,6 @@ import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
import okhttp3.Call;
import okhttp3.Request;
import okhttp3.Response;
@@ -61,6 +60,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
protected EditText searchEdit;
protected Runnable searchDebouncer=this::onSearchChangedDebounced;
protected String currentSearchQuery;
protected String currentSearchQueryButWithCasePreserved;
protected String loadingInstanceDomain;
protected HashMap<String, Instance> instancesCache=new HashMap<>();
protected View buttonBar;
@@ -91,6 +91,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN)
return true;
currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
currentSearchQueryButWithCasePreserved=searchEdit.getText().toString().trim();
updateFilteredList();
searchEdit.removeCallbacks(searchDebouncer);
Instance instance=instancesCache.get(normalizeInstanceDomain(currentSearchQuery));
@@ -105,6 +106,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
protected void onSearchChangedDebounced(){
currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
currentSearchQueryButWithCasePreserved=searchEdit.getText().toString().trim();
updateFilteredList();
loadInstanceInfo(currentSearchQuery, false);
}
@@ -149,6 +151,10 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
}
protected void loadInstanceInfo(String _domain, boolean isFromRedirect){
loadInstanceInfo(_domain, isFromRedirect, null);
}
protected void loadInstanceInfo(String _domain, boolean isFromRedirect, Consumer<Object> onError){
if(TextUtils.isEmpty(_domain))
return;
String domain=normalizeInstanceDomain(_domain);
@@ -173,7 +179,10 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
try{
new URI("https://"+domain+"/api/v1/instance"); // Validate the host by trying to parse the URI
}catch(URISyntaxException x){
showInstanceInfoLoadError(domain, x);
if(onError!=null)
onError.accept(x);
else
showInstanceInfoLoadError(domain, x);
if(fakeInstance!=null){
fakeInstance.description=getString(R.string.error);
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
@@ -193,10 +202,11 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
loadingInstanceDomain=null;
result.uri=domain; // needed for instances that use domain redirection
instancesCache.put(domain, result);
if(instanceProgressDialog!=null || onError!=null)
proceedWithAuthOrSignup(result);
if(instanceProgressDialog!=null){
instanceProgressDialog.dismiss();
instanceProgressDialog=null;
proceedWithAuthOrSignup(result);
}
if(Objects.equals(domain, currentSearchQuery) || Objects.equals(currentSearchQuery, redirects.get(domain)) || Objects.equals(currentSearchQuery, redirectsInverse.get(domain))){
boolean found=false;
@@ -223,11 +233,14 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
public void onError(ErrorResponse error){
loadingInstanceRequest=null;
if(!isFromRedirect && error instanceof MastodonErrorResponse me && me.httpStatus==404){
fetchDomainFromHostMetaAndMaybeRetry(domain, error);
fetchDomainFromHostMetaAndMaybeRetry(domain, error, onError);
return;
}
loadingInstanceDomain=null;
showInstanceInfoLoadError(domain, error);
if(onError!=null)
onError.accept(error);
else
showInstanceInfoLoadError(domain, error);
if(fakeInstance!=null && getActivity()!=null){
fakeInstance.description=getString(R.string.error);
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
@@ -276,7 +289,7 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
}
}
private void fetchDomainFromHostMetaAndMaybeRetry(String domain, Object origError){
private void fetchDomainFromHostMetaAndMaybeRetry(String domain, Object origError, Consumer<Object> onError){
String url="https://"+domain+"/.well-known/host-meta";
Request req=new Request.Builder()
.url(url)
@@ -290,7 +303,12 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
Activity a=getActivity();
if(a==null)
return;
a.runOnUiThread(()->showInstanceInfoLoadError(domain, e));
a.runOnUiThread(()->{
if(onError!=null)
onError.accept(e);
else
showInstanceInfoLoadError(domain, e);
});
}
@Override
@@ -302,7 +320,13 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
return;
try(response){
if(!response.isSuccessful()){
a.runOnUiThread(()->showInstanceInfoLoadError(domain, response.code()+" "+response.message()));
a.runOnUiThread(()->{
String err=response.code()+" "+response.message();
if(onError!=null)
onError.accept(err);
else
showInstanceInfoLoadError(domain, err);
});
return;
}
InputSource source=new InputSource(response.body().charStream());
@@ -321,9 +345,19 @@ abstract class InstanceCatalogFragment extends BaseRecyclerFragment<CatalogInsta
}
}
}
a.runOnUiThread(()->showInstanceInfoLoadError(domain, origError));
a.runOnUiThread(()->{
if(onError!=null)
onError.accept(origError);
else
showInstanceInfoLoadError(domain, origError);
});
}catch(Exception x){
a.runOnUiThread(()->showInstanceInfoLoadError(domain, x));
a.runOnUiThread(()->{
if(onError!=null)
onError.accept(x);
else
showInstanceInfoLoadError(domain, x);
});
}
}
});

View File

@@ -1,8 +1,13 @@
package org.joinmastodon.android.fragments.onboarding;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.ColorStateList;
import android.net.Uri;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
@@ -12,6 +17,8 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.HorizontalScrollView;
import android.widget.ImageButton;
import android.widget.LinearLayout;
@@ -19,9 +26,12 @@ import android.widget.PopupMenu;
import android.widget.RadioButton;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.requests.accounts.CheckInviteLink;
import org.joinmastodon.android.api.requests.catalog.GetCatalogCategories;
import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances;
import org.joinmastodon.android.model.Instance;
@@ -29,6 +39,8 @@ import org.joinmastodon.android.model.catalog.CatalogCategory;
import org.joinmastodon.android.model.catalog.CatalogInstance;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.FilterChipView;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
@@ -40,7 +52,9 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Random;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
@@ -77,6 +91,9 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
private CatalogInstance.Region chosenRegion;
private CategoryChoice categoryChoice=CategoryChoice.GENERAL;
private String inviteCode, inviteCodeHost;
private AlertDialog currentInviteLinkAlert;
public InstanceCatalogSignupFragment(){
super(R.layout.fragment_onboarding_common, 10);
}
@@ -317,7 +334,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
focusThing=view.findViewById(R.id.focus_thing);
focusThing.requestFocus();
view.findViewById(R.id.btn_random_instance).setOnClickListener(this::onPickRandomInstanceClick);
view.findViewById(R.id.btn_use_invite).setOnClickListener(this::onUseInviteClick);
nextButton.setEnabled(chosenInstance!=null);
}
@@ -351,34 +368,191 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
@Override
protected void proceedWithAuthOrSignup(Instance instance){
if(currentInviteLinkAlert!=null){
currentInviteLinkAlert.dismiss();
}else if(!TextUtils.isEmpty(currentSearchQuery) && HtmlParser.INVITE_LINK_PATTERN.matcher(currentSearchQueryButWithCasePreserved).find()){
if(TextUtils.isEmpty(inviteCode) || !Objects.equals(instance.uri, inviteCodeHost)){
Uri inviteLink=Uri.parse(currentSearchQueryButWithCasePreserved);
new CheckInviteLink(inviteLink.getPath())
.setCallback(new Callback<>(){
@Override
public void onSuccess(CheckInviteLink.Response result){
inviteCodeHost=inviteLink.getHost();
inviteCode=result.inviteCode;
proceedWithAuthOrSignup(instance);
}
@Override
public void onError(ErrorResponse error){
if(getActivity()==null)
return;
if(error instanceof MastodonErrorResponse mer){
switch(mer.httpStatus){
case 401 -> new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.expired_invite_link)
.setMessage(getString(R.string.expired_clipboard_invite_link_alert, inviteLink.getHost(), getArguments().getString("defaultServer")))
.setPositiveButton(R.string.ok, null)
.show();
case 404 -> new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.invalid_invite_link)
.setMessage(getString(R.string.invalid_clipboard_invite_link_alert, inviteLink.getHost(), getArguments().getString("defaultServer")))
.setPositiveButton(R.string.ok, null)
.show();
default -> error.showToast(getActivity());
}
}
}
})
.wrapProgress(getActivity(), R.string.loading_instance, true)
.execNoAuth(inviteLink.getHost());
return;
}
}
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
if(!instance.registrations){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(R.string.instance_signup_closed)
.setPositiveButton(R.string.ok, null)
.show();
if(!instance.registrations && (TextUtils.isEmpty(inviteCode) || !Objects.equals(instance.uri, inviteCodeHost))){
if(instance.invitesEnabled){
showInviteLinkAlert(instance.uri);
}else{
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(R.string.instance_signup_closed)
.setPositiveButton(R.string.ok, null)
.show();
}
return;
}
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
if(!TextUtils.isEmpty(inviteCode) && Objects.equals(instance.uri, inviteCodeHost))
args.putString("inviteCode", inviteCode);
Nav.go(getActivity(), InstanceRulesFragment.class, args);
}
private void onPickRandomInstanceClick(View v){
String lang=Locale.getDefault().getLanguage();
List<CatalogInstance> instances=data.stream().filter(ci->!ci.approvalRequired && ("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general"))) && (lang.equals(ci.language) || (ci.languages!=null && ci.languages.contains(lang)))).collect(Collectors.toList());
if(instances.isEmpty()){
instances=data.stream().filter(ci->!ci.approvalRequired && ("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general")))).collect(Collectors.toList());
private void onUseInviteClick(View v){
showInviteLinkAlert(null);
}
private void showInviteLinkAlert(String domain){
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
.setView(R.layout.alert_invite_link)
.setPositiveButton(R.string.next, null)
.setNegativeButton(R.string.cancel, null)
.create();
Button next=alert.getButton(AlertDialog.BUTTON_POSITIVE);
EditText edit=alert.findViewById(R.id.edit);
TextView supportingText=alert.findViewById(R.id.supporting_text);
TextView label=alert.findViewById(R.id.label);
TextView subtitle=alert.findViewById(R.id.subtitle);
ImageButton clear=alert.findViewById(R.id.clear);
clear.setVisibility(View.GONE);
if(TextUtils.isEmpty(domain)){
subtitle.setVisibility(View.GONE);
}else{
subtitle.setText(getString(R.string.need_invite_to_join_server, domain));
}
if(instances.isEmpty()){
instances=data.stream().filter(ci->("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general")))).collect(Collectors.toList());
Consumer<String> errorSetter=err->{
supportingText.setText(err);
int errorColor=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Error);
supportingText.setTextColor(errorColor);
label.setTextColor(errorColor);
edit.setBackgroundResource(R.drawable.bg_m3_filled_text_field_error);
};
next.setOnClickListener(_v->{
Uri inviteLink=Uri.parse(edit.getText().toString());
if(TextUtils.isEmpty(inviteLink.getHost()) || TextUtils.isEmpty(inviteLink.getPath())){
errorSetter.accept(getString(R.string.this_invite_is_invalid));
return;
}
UiUtils.showProgressForAlertButton(next, true);
new CheckInviteLink(inviteLink.getPath())
.setCallback(new Callback<>(){
@Override
public void onSuccess(CheckInviteLink.Response result){
if(getActivity()==null || !alert.isShowing())
return;
String host=inviteLink.getHost();
inviteCode=result.inviteCode;
inviteCodeHost=host;
Instance instance=instancesCache.get(normalizeInstanceDomain(host));
if(instance==null){
loadInstanceInfo(host, false, err->{
String errorStr;
if(err instanceof String str){
errorStr=str;
}else if(err instanceof Throwable x){
errorStr=x.getMessage();
}else if(err instanceof MastodonErrorResponse mer){
errorStr=mer.error;
}else{
errorStr=getString(R.string.error);
}
errorSetter.accept(errorStr);
UiUtils.showProgressForAlertButton(next, false);
});
}else{
proceedWithAuthOrSignup(instance);
}
}
@Override
public void onError(ErrorResponse error){
if(getActivity()==null || !alert.isShowing())
return;
UiUtils.showProgressForAlertButton(next, false);
if(error instanceof MastodonErrorResponse mer){
errorSetter.accept(switch(mer.httpStatus){
case 404 -> getString(R.string.this_invite_is_invalid);
case 401 -> getString(R.string.this_invite_has_expired);
default -> mer.error;
});
}
}
})
.execNoAuth(inviteLink.getHost());
});
next.setEnabled(false);
edit.addTextChangedListener(new SimpleTextWatcher(e->{
boolean wasEmpty=!next.isEnabled();
next.setEnabled(e.length()>0);
if(supportingText.length()>0){
supportingText.setText("");
int regularColor=UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant);
supportingText.setTextColor(regularColor);
label.setTextColor(regularColor);
edit.setBackgroundResource(R.drawable.bg_m3_filled_text_field);
}
if(wasEmpty!=(e.length()==0)){
int padEnd;
if(e.length()==0){
clear.setVisibility(View.GONE);
padEnd=V.dp(16);
}else{
clear.setVisibility(View.VISIBLE);
padEnd=V.dp(48);
}
edit.setPaddingRelative(edit.getPaddingStart(), edit.getPaddingTop(), padEnd, edit.getPaddingBottom());
}
}));
clear.setOnClickListener(_v->edit.setText(""));
ClipData clipData=getActivity().getSystemService(ClipboardManager.class).getPrimaryClip();
if(clipData!=null && clipData.getItemCount()>0){
CharSequence clipText=clipData.getItemAt(0).coerceToText(getActivity());
if(HtmlParser.INVITE_LINK_PATTERN.matcher(clipText).find()){
edit.setText(clipText);
supportingText.setText(R.string.invite_link_pasted);
}
}
if(instances.isEmpty()){
return;
}
chosenInstance=instances.get(new Random().nextInt(instances.size()));
onNextClick(v);
currentInviteLinkAlert=alert;
alert.setOnDismissListener(dialog->currentInviteLinkAlert=null);
alert.show();
}
@Override
@@ -387,8 +561,14 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
filteredData.clear();
if(searchQueryMode){
if(!TextUtils.isEmpty(currentSearchQuery)){
String actualQuery;
if(currentSearchQuery.startsWith("https:")){
actualQuery=Uri.parse(currentSearchQuery).getHost();
}else{
actualQuery=currentSearchQuery;
}
for(CatalogInstance instance:data){
if(instance.domain.contains(currentSearchQuery)){
if(instance.domain.contains(actualQuery)){
filteredData.add(instance);
}
}

View File

@@ -91,6 +91,9 @@ public class InstanceRulesFragment extends ToolbarFragment{
protected void onButtonClick(){
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(instance));
if(getArguments().containsKey("inviteCode")){
args.putString("inviteCode", getArguments().getString("inviteCode"));
}
Nav.goForResult(getActivity(), GoogleMadeMeAddThisFragment.class, args, RULES_REQUEST, this);
}

View File

@@ -4,6 +4,7 @@ import android.app.ProgressDialog;
import android.os.Bundle;
import android.view.View;
import android.view.WindowInsets;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetFollowSuggestions;
@@ -12,35 +13,38 @@ import org.joinmastodon.android.fragments.account_list.BaseAccountListFragment;
import org.joinmastodon.android.model.FollowSuggestion;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
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.api.SimpleCallback;
import me.grishka.appkit.views.FragmentRootLinearLayout;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment{
private String accountID;
private View buttonBar;
private ElevationOnScrollListener onScrollListener;
private int numRunningFollowRequests=0;
public OnboardingFollowSuggestionsFragment(){
super(R.layout.fragment_onboarding_follow_suggestions, 40);
itemLayoutRes=R.layout.item_account_list_onboarding;
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setRetainInstance(true);
setTitle(R.string.popular_on_mastodon);
setTitle(R.string.onboarding_recommendations_title);
accountID=getArguments().getString("account");
loadData();
}
@@ -49,7 +53,6 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
buttonBar=view.findViewById(R.id.button_bar);
list.addOnScrollListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
view.findViewById(R.id.btn_next).setOnClickListener(UiUtils.rateLimitedClickListener(this::onFollowAllClick));
view.findViewById(R.id.btn_skip).setOnClickListener(UiUtils.rateLimitedClickListener(v->proceed()));
@@ -58,9 +61,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
if(onScrollListener!=null){
onScrollListener.setViews(buttonBar, getToolbar());
}
getToolbar().setContentInsetsRelative(V.dp(56), 0);
}
@Override
@@ -69,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)).collect(Collectors.toList()), false);
onDataLoaded(result.stream().map(fs->new AccountViewModel(fs.account, accountID).stripLinksFromBio()).collect(Collectors.toList()), false);
}
})
.exec(accountID);
@@ -80,6 +81,19 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
@Override
protected RecyclerView.Adapter<?> getAdapter(){
TextView introText=new TextView(getActivity());
introText.setTextAppearance(R.style.m3_body_large);
introText.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurface));
introText.setPaddingRelative(V.dp(56), 0, V.dp(24), V.dp(8));
introText.setText(R.string.onboarding_recommendations_intro);
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(introText));
mergeAdapter.addAdapter(super.getAdapter());
return mergeAdapter;
}
private void onFollowAllClick(View v){
if(!loaded || relationships.isEmpty())
return;
@@ -155,5 +169,6 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
protected void onConfigureViewHolder(AccountViewHolder holder){
super.onConfigureViewHolder(holder);
holder.setStyle(AccountViewHolder.AccessoryType.BUTTON, true);
holder.avatar.setOutlineProvider(OutlineProviders.roundedRect(8));
}
}

View File

@@ -1,18 +1,15 @@
package org.joinmastodon.android.fragments.onboarding;
import android.app.ProgressDialog;
import android.graphics.Typeface;
import android.os.Build;
import android.os.Bundle;
import android.os.LocaleList;
import android.text.Editable;
import android.text.Html;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.style.TypefaceSpan;
import android.text.style.URLSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -46,10 +43,13 @@ import org.jsoup.select.NodeVisitor;
import org.parceler.Parcels;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import androidx.annotation.Nullable;
@@ -58,7 +58,6 @@ import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class SignupFragment extends ToolbarFragment{
@@ -79,6 +78,7 @@ public class SignupFragment extends ToolbarFragment{
private ProgressDialog progressDialog;
private HashSet<EditText> errorFields=new HashSet<>();
private ElevationOnScrollListener onScrollListener;
private Set<String> serverSupportedTimezones, serverSupportedLocales;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -87,6 +87,8 @@ public class SignupFragment extends ToolbarFragment{
instance=Parcels.unwrap(getArguments().getParcelable("instance"));
createAppAndGetToken();
setTitle(R.string.signup_title);
serverSupportedTimezones=Arrays.stream(getResources().getStringArray(R.array.server_supported_timezones)).collect(Collectors.toSet());
serverSupportedLocales=Arrays.stream(getResources().getStringArray(R.array.server_supported_locales)).collect(Collectors.toSet());
}
@Nullable
@@ -190,7 +192,36 @@ public class SignupFragment extends ToolbarFragment{
edit.setError(null);
}
errorFields.clear();
new RegisterAccount(username, email, password.getText().toString(), getResources().getConfiguration().locale.getLanguage(), reason.getText().toString(), ZoneId.systemDefault().getId())
String locale=null;
String timezone=ZoneId.systemDefault().getId();
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
LocaleList localeList=getResources().getConfiguration().getLocales();
for(int i=0;i<localeList.size();i++){
Locale l=localeList.get(i);
if(serverSupportedLocales.contains(l.toLanguageTag())){
locale=l.toLanguageTag();
break;
}else if(serverSupportedLocales.contains(l.getLanguage())){
locale=l.getLanguage();
break;
}
}
}else{
Locale l=getResources().getConfiguration().locale;
if(serverSupportedLocales.contains(l.toLanguageTag())){
locale=l.toLanguageTag();
}else if(serverSupportedLocales.contains(l.getLanguage())){
locale=l.getLanguage();
}
}
if(!serverSupportedTimezones.contains(timezone))
timezone=null;
String inviteCode=getArguments().getString("inviteCode");
new RegisterAccount(username, email, password.getText().toString(), locale, reason.getText().toString(), timezone, inviteCode)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Token result){

View File

@@ -125,41 +125,40 @@ public class EditFilterFragment extends BaseSettingsFragment<Void> implements On
ArrayList<String> options=Arrays.stream(durationOptions).mapToObj(d->UiUtils.formatDuration(getActivity(), d)).collect(Collectors.toCollection(ArrayList<String>::new));
options.add(0, getString(R.string.filter_duration_forever));
options.add(getString(R.string.filter_duration_custom));
Instant[] newEnd={null};
boolean[] isCustom={false};
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.settings_filter_duration_title)
.setSupportingText(endsAt==null ? null : getString(R.string.settings_filter_ends, UiUtils.formatRelativeTimestampAsMinutesAgo(getActivity(), endsAt, false)))
.setSingleChoiceItems(options.toArray(new String[0]), -1, (dlg, item)->{
AlertDialog a=(AlertDialog) dlg;
if(item==options.size()-1){ // custom
showCustomDurationAlert(isCustom[0] ? newEnd[0] : null, date->{
showCustomDurationAlert(null, date->{
if(date==null){
a.getListView().setItemChecked(item, false);
}else{
isCustom[0]=true;
newEnd[0]=date;
a.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
Instant newEnd=date;
if(!Objects.equals(endsAt, newEnd)){
endsAt=newEnd;
updateDurationItem();
dirty=true;
}
a.dismiss();
}
});
}else{
isCustom[0]=false;
Instant newEnd;
if(item==0){
newEnd[0]=null;
newEnd=null;
}else{
newEnd[0]=Instant.now().plusSeconds(durationOptions[item-1]);
newEnd=Instant.now().plusSeconds(durationOptions[item-1]);
}
a.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
if(!Objects.equals(endsAt, newEnd)){
endsAt=newEnd;
updateDurationItem();
dirty=true;
}
a.dismiss();
}
})
.setPositiveButton(R.string.ok, (dlg, item)->{
if(!Objects.equals(endsAt, newEnd[0])){
endsAt=newEnd[0];
updateDurationItem();
dirty=true;
}
})
.setNegativeButton(R.string.cancel, null)
.show();
alert.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
}

View File

@@ -37,14 +37,14 @@ import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.V;
public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> implements OnBackPressedListener{
private ImageButton fab;
private Button fab;
private ActionMode actionMode;
private ArrayList<ListItem<FilterKeyword>> selectedItems=new ArrayList<>();
private ArrayList<String> deletedItemIDs=new ArrayList<>();
private MenuItem deleteItem;
public FilterWordsFragment(){
setListLayoutId(R.layout.recycler_fragment_with_fab);
setListLayoutId(R.layout.recycler_fragment_with_text_fab);
}
@Override
@@ -92,18 +92,14 @@ public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> imp
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
fab.setImageResource(R.drawable.ic_add_24px);
fab.setContentDescription(getString(R.string.add_muted_word));
fab.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_add_24px, 0, 0, 0);
fab.setText(R.string.add_muted_word_short);
fab.setOnClickListener(v->onFabClick());
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
int fabInset=0;
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
fabInset=insets.getSystemWindowInsetBottom();
}
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(16)+fabInset;
UiUtils.applyBottomInsetToFAB(fab, insets);
super.onApplyWindowInsets(insets);
}

View File

@@ -1,6 +1,15 @@
package org.joinmastodon.android.fragments.settings;
import android.app.AlertDialog;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
@@ -15,9 +24,12 @@ import org.joinmastodon.android.ui.viewcontrollers.ComposeLanguageAlertViewContr
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;
private CheckableListItem<Void> altTextItem, playGifsItem, customTabsItem, confirmUnfollowItem, confirmBoostItem, confirmDeleteItem;
private ListItem<Void> languageItem, customTabsItem;
private CheckableListItem<Void> altTextItem, playGifsItem, confirmUnfollowItem, confirmBoostItem, confirmDeleteItem;
private Locale postLanguage;
private ComposeLanguageAlertViewController.SelectedOption newPostLanguage;
@@ -33,9 +45,9 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void>{
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),
customTabsItem=new CheckableListItem<>(R.string.settings_custom_tabs, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.useCustomTabs, R.drawable.ic_open_in_browser_24px, this::toggleCheckableItem),
confirmUnfollowItem=new CheckableListItem<>(R.string.settings_confirm_unfollow, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmUnfollow, R.drawable.ic_person_remove_24px, this::toggleCheckableItem),
confirmBoostItem=new CheckableListItem<>(R.string.settings_confirm_boost, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmBoost, R.drawable.ic_repeat_24px, this::toggleCheckableItem),
confirmDeleteItem=new CheckableListItem<>(R.string.settings_confirm_delete_post, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmDeletePost, R.drawable.ic_delete_24px, this::toggleCheckableItem)
@@ -46,19 +58,61 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void>{
protected void doLoadData(int offset, int count){}
private void onDefaultLanguageClick(ListItem<?> item){
ComposeLanguageAlertViewController vc=new ComposeLanguageAlertViewController(getActivity(), null, new ComposeLanguageAlertViewController.SelectedOption(-1, postLanguage), null);
new M3AlertDialogBuilder(getActivity())
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.ok, (dlg, which)->{
ComposeLanguageAlertViewController.SelectedOption opt=vc.getSelectedOption();
if(!opt.locale.equals(postLanguage)){
newPostLanguage=opt;
languageItem.subtitle=newPostLanguage.locale.getDisplayLanguage(Locale.getDefault());
rebindItem(languageItem);
}
.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;
if(info==null){
browserName="??";
}else{
browserName=info.loadLabel(getActivity().getPackageManager()).toString();
}
ArrayAdapter<CharSequence> adapter=new ArrayAdapter<>(getActivity(), R.layout.item_alert_single_choice_2lines_but_different, R.id.text,
new String[]{getString(R.string.in_app_browser), getString(R.string.system_browser)}){
@Override
public boolean hasStableIds(){
return true;
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent){
View view=super.getView(position, convertView, parent);
TextView subtitle=view.findViewById(R.id.subtitle);
if(position==0){
subtitle.setVisibility(View.GONE);
}else{
subtitle.setVisibility(View.VISIBLE);
subtitle.setText(browserName);
}
return view;
}
};
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.settings_custom_tabs)
.setSingleChoiceItems(adapter, GlobalUserPreferences.useCustomTabs ? 0 : 1, (dlg, which)->{
GlobalUserPreferences.useCustomTabs=which==0;
customTabsItem.subtitleRes=GlobalUserPreferences.useCustomTabs ? R.string.in_app_browser : R.string.system_browser;
rebindItem(customTabsItem);
dlg.dismiss();
})
.setNegativeButton(R.string.cancel, null)
.show();
}
@@ -66,9 +120,8 @@ public class SettingsBehaviorFragment extends BaseSettingsFragment<Void>{
protected void onHidden(){
super.onHidden();
GlobalUserPreferences.playGifs=playGifsItem.checked;
GlobalUserPreferences.useCustomTabs=customTabsItem.checked;
GlobalUserPreferences.altTextReminders=altTextItem.checked;
GlobalUserPreferences.confirmUnfollow=customTabsItem.checked;
GlobalUserPreferences.confirmUnfollow=confirmUnfollowItem.checked;
GlobalUserPreferences.confirmBoost=confirmBoostItem.checked;
GlobalUserPreferences.confirmDeletePost=confirmDeleteItem.checked;
GlobalUserPreferences.save();

View File

@@ -86,28 +86,26 @@ public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
case DARK -> 1;
case AUTO -> 2;
};
int[] newSelected={selected};
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.settings_theme)
.setSingleChoiceItems((String[])IntStream.of(R.string.theme_light, R.string.theme_dark, R.string.theme_auto).mapToObj(this::getString).toArray(String[]::new),
selected, (dlg, item)->newSelected[0]=item)
.setPositiveButton(R.string.ok, (dlg, item)->{
GlobalUserPreferences.ThemePreference pref=switch(newSelected[0]){
case 0 -> GlobalUserPreferences.ThemePreference.LIGHT;
case 1 -> GlobalUserPreferences.ThemePreference.DARK;
case 2 -> GlobalUserPreferences.ThemePreference.AUTO;
default -> throw new IllegalStateException("Unexpected value: "+newSelected[0]);
};
if(pref!=GlobalUserPreferences.theme){
GlobalUserPreferences.ThemePreference prev=GlobalUserPreferences.theme;
GlobalUserPreferences.theme=pref;
GlobalUserPreferences.save();
themeItem.subtitleRes=getAppearanceValue();
rebindItem(themeItem);
maybeApplyNewThemeRightNow(prev);
}
})
.setNegativeButton(R.string.cancel, null)
selected, (dlg, item)->{
GlobalUserPreferences.ThemePreference pref=switch(item){
case 0 -> GlobalUserPreferences.ThemePreference.LIGHT;
case 1 -> GlobalUserPreferences.ThemePreference.DARK;
case 2 -> GlobalUserPreferences.ThemePreference.AUTO;
default -> throw new IllegalStateException("Unexpected value: "+item);
};
if(pref!=GlobalUserPreferences.theme){
GlobalUserPreferences.ThemePreference prev=GlobalUserPreferences.theme;
GlobalUserPreferences.theme=pref;
GlobalUserPreferences.save();
themeItem.subtitleRes=getAppearanceValue();
rebindItem(themeItem);
maybeApplyNewThemeRightNow(prev);
}
dlg.dismiss();
})
.show();
}

View File

@@ -1,6 +1,9 @@
package org.joinmastodon.android.fragments.settings;
import android.os.Bundle;
import android.view.View;
import android.view.WindowInsets;
import android.widget.Button;
import com.squareup.otto.Subscribe;
@@ -12,6 +15,7 @@ import org.joinmastodon.android.events.SettingsFilterDeletedEvent;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.Collections;
@@ -24,6 +28,12 @@ import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
public class SettingsFiltersFragment extends BaseSettingsFragment<Filter>{
private Button fab;
public SettingsFiltersFragment(){
setListLayoutId(R.layout.recycler_fragment_with_text_fab);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@@ -44,6 +54,8 @@ public class SettingsFiltersFragment extends BaseSettingsFragment<Filter>{
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Filter> result){
if(getActivity()==null)
return;
onDataLoaded(result.stream().map(f->makeListItem(f)).collect(Collectors.toList()));
}
})
@@ -51,13 +63,12 @@ public class SettingsFiltersFragment extends BaseSettingsFragment<Filter>{
}
@Override
protected RecyclerView.Adapter<?> getAdapter(){
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
adapter.addAdapter(super.getAdapter());
adapter.addAdapter(new GenericListItemsAdapter<>(Collections.singletonList(
new ListItem<Void>(R.string.settings_add_filter, 0, R.drawable.ic_add_24px, this::onAddFilterClick)
)));
return adapter;
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
fab.setText(R.string.settings_add_filter);
fab.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_add_24px, 0, 0, 0);
fab.setOnClickListener(v->onAddFilterClick());
}
private void onFilterClick(ListItem<Filter> filter){
@@ -67,7 +78,7 @@ public class SettingsFiltersFragment extends BaseSettingsFragment<Filter>{
Nav.go(getActivity(), EditFilterFragment.class, args);
}
private void onAddFilterClick(ListItem<?> item){
private void onAddFilterClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), EditFilterFragment.class, args);
@@ -107,4 +118,10 @@ public class SettingsFiltersFragment extends BaseSettingsFragment<Filter>{
data.add(makeListItem(ev.filter));
itemsAdapter.notifyItemInserted(data.size()-1);
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
UiUtils.applyBottomInsetToFAB(fab, insets);
super.onApplyWindowInsets(insets);
}
}

View File

@@ -1,6 +1,5 @@
package org.joinmastodon.android.fragments.settings;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
@@ -17,7 +16,7 @@ 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.viewmodel.ListItem;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
import org.joinmastodon.android.ui.utils.UiUtils;
@@ -153,7 +152,7 @@ public class SettingsMainFragment extends BaseSettingsFragment<Void>{
}
private void onManageAccountsClick(ListItem<?> item){
new AccountSwitcherSheet(getActivity(), null).show();
new AccountSwitcherSheet(getActivity(), null).setOnLoggedOutCallback(()->loggedOut=true).show();
}
private void onLogOutClick(ListItem<?> item_){

View File

@@ -192,18 +192,13 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
3*24*3600,
7*24*3600
};
int[] selectedOption={0};
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.pause_all_notifications_title)
.setSupportingText(time>System.currentTimeMillis() ? getString(R.string.pause_notifications_ends, UiUtils.formatRelativeTimestampAsMinutesAgo(getActivity(), Instant.ofEpochMilli(time), false)) : null)
.setSingleChoiceItems((String[])Arrays.stream(durationOptions).mapToObj(d->UiUtils.formatDuration(getActivity(), d)).toArray(String[]::new), -1, (dlg, item)->{
if(selectedOption[0]==0){
((AlertDialog)dlg).getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
}
selectedOption[0]=durationOptions[item];
AccountSessionManager.get(accountID).getLocalPreferences().setNotificationsPauseEndTime(System.currentTimeMillis()+durationOptions[item]*1000L);
dlg.dismiss();
})
.setPositiveButton(R.string.ok, (dlg, item)->AccountSessionManager.get(accountID).getLocalPreferences().setNotificationsPauseEndTime(System.currentTimeMillis()+selectedOption[0]*1000L))
.setNegativeButton(R.string.cancel, null)
.show();
alert.setOnDismissListener(dialog->updatePauseItem());
alert.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
@@ -216,20 +211,18 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
R.string.notifications_policy_follower,
R.string.notifications_policy_no_one
).map(this::getString).toArray(String[]::new);
int[] selectedItem={getPushSubscription().policy.ordinal()};
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.settings_notifications_policy)
.setSingleChoiceItems(items, selectedItem[0], (dlg, which)->selectedItem[0]=which)
.setPositiveButton(R.string.ok, (dlg, which)->{
.setSingleChoiceItems(items, getPushSubscription().policy.ordinal(), (dlg, which)->{
dlg.dismiss();
PushSubscription.Policy prevValue=getPushSubscription().policy;
PushSubscription.Policy newValue=PushSubscription.Policy.values()[selectedItem[0]];
PushSubscription.Policy newValue=PushSubscription.Policy.values()[which];
if(prevValue==newValue)
return;
getPushSubscription().policy=newValue;
updatePolicyItem(prevValue);
needUpdateNotificationSettings=true;
})
.setNegativeButton(R.string.cancel, null)
.show();
}

View File

@@ -0,0 +1,47 @@
package org.joinmastodon.android.googleservices;
import android.app.PendingIntent;
import org.microg.safeparcel.AutoSafeParcelable;
import org.microg.safeparcel.SafeParceled;
public class ConnectionResult extends AutoSafeParcelable{
public static final int UNKNOWN = -1;
public static final int SUCCESS = 0;
public static final int SERVICE_MISSING = 1;
public static final int SERVICE_VERSION_UPDATE_REQUIRED = 2;
public static final int SERVICE_DISABLED = 3;
public static final int SIGN_IN_REQUIRED = 4;
public static final int INVALID_ACCOUNT = 5;
public static final int RESOLUTION_REQUIRED = 6;
public static final int NETWORK_ERROR = 7;
public static final int INTERNAL_ERROR = 8;
public static final int SERVICE_INVALID = 9;
public static final int DEVELOPER_ERROR = 10;
public static final int LICENSE_CHECK_FAILED = 11;
public static final int CANCELED = 13;
public static final int TIMEOUT = 14;
public static final int INTERRUPTED = 15;
public static final int API_UNAVAILABLE = 16;
public static final int SIGN_IN_FAILED = 17;
public static final int SERVICE_UPDATING = 18;
public static final int SERVICE_MISSING_PERMISSION = 19;
public static final int RESTRICTED_PROFILE = 20;
public static final int RESOLUTION_ACTIVITY_NOT_FOUND = 22;
public static final int API_DISABLED = 23;
public static final int API_DISABLED_FOR_CONNECTION = 24;
@Deprecated
public static final int DRIVE_EXTERNAL_STORAGE_REQUIRED = 1500;
@SafeParceled(1)
public int versionCode;
@SafeParceled(2)
public int errorCode;
@SafeParceled(3)
public PendingIntent resolution;
@SafeParceled(4)
public String errorMessage;
public static final Creator<ConnectionResult> CREATOR=new AutoCreator<>(ConnectionResult.class);
}

View File

@@ -0,0 +1,116 @@
package org.joinmastodon.android.googleservices;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.IInterface;
import android.os.RemoteException;
import android.util.Log;
import android.util.SparseArray;
import com.google.android.gms.common.internal.ConnectionInfo;
import com.google.android.gms.common.internal.GetServiceRequest;
import com.google.android.gms.common.internal.IGmsCallbacks;
import com.google.android.gms.common.internal.IGmsServiceBroker;
import com.google.android.gms.common.moduleinstall.internal.IModuleInstallService;
import java.util.function.Function;
public class GmsClient{
private static final String TAG="GmsClient";
private static final SparseArray<ServiceConnection> currentConnections=new SparseArray<>();
private static int nextConnectionID=0;
public static <I extends IInterface> void connectToService(Context context, String action, int id, boolean useDynamicLookup, ServiceConnectionCallback<I> callback, Function<IBinder, I> asInterface){
Intent intent;
if(useDynamicLookup){
try{
Bundle args=new Bundle();
args.putString("serviceActionBundleKey", action);
Bundle result=context.getContentResolver().call(new Uri.Builder().scheme("content").authority("com.google.android.gms.chimera").build(), "serviceIntentCall", null, args);
if(result==null)
throw new IllegalStateException("Dynamic lookup failed");
intent=result.getParcelable("serviceResponseIntentKey");
if(intent==null)
throw new IllegalStateException("Dynamic lookup returned null");
}catch(Exception x){
callback.onError(x);
return;
}
}else{
intent=new Intent(action);
}
intent.setPackage("com.google.android.gms");
ServiceConnection conn=new ServiceConnection(){
@Override
public void onServiceConnected(ComponentName name, IBinder service){
IGmsServiceBroker broker=IGmsServiceBroker.Stub.asInterface(service);
GetServiceRequest req=new GetServiceRequest();
req.serviceId=id;
req.packageName=context.getPackageName();
ServiceConnection serviceConnectionThis=this;
try{
broker.getService(new IGmsCallbacks.Stub(){
@Override
public void onPostInitComplete(int statusCode, IBinder binder, Bundle params) throws RemoteException{
int connectionID=nextConnectionID++;
currentConnections.put(connectionID, serviceConnectionThis);
callback.onSuccess(asInterface.apply(binder), connectionID);
}
@Override
public void onAccountValidationComplete(int statusCode, Bundle params) throws RemoteException{}
@Override
public void onPostInitCompleteWithConnectionInfo(int statusCode, IBinder binder, ConnectionInfo info) throws RemoteException{
onPostInitComplete(statusCode, binder, info!=null ? info.params : null);
}
}, req);
}catch(Exception x){
callback.onError(x);
context.unbindService(this);
}
}
@Override
public void onServiceDisconnected(ComponentName name){}
};
boolean res=context.bindService(intent, conn, Context.BIND_AUTO_CREATE | Context.BIND_DEBUG_UNBIND | Context.BIND_ADJUST_WITH_ACTIVITY);
if(!res){
context.unbindService(conn);
callback.onError(new IllegalStateException("Service connection failed"));
}
}
public static void disconnectFromService(Context context, int connectionID){
ServiceConnection conn=currentConnections.get(connectionID);
if(conn!=null){
currentConnections.remove(connectionID);
context.unbindService(conn);
}
}
public static boolean isGooglePlayServicesAvailable(Context context){
PackageManager pm=context.getPackageManager();
try{
pm.getPackageInfo("com.google.android.gms", 0);
return true;
}catch(PackageManager.NameNotFoundException e){
return false;
}
}
public static void getModuleInstallerService(Context context, ServiceConnectionCallback<IModuleInstallService> callback){
connectToService(context, "com.google.android.gms.chimera.container.moduleinstall.ModuleInstallService.START", 308, true, callback, IModuleInstallService.Stub::asInterface);
}
public interface ServiceConnectionCallback<I extends IInterface>{
void onSuccess(I service, int connectionID);
void onError(Exception error);
}
}

View File

@@ -0,0 +1,253 @@
package org.joinmastodon.android.googleservices.barcodescanner;
import android.graphics.Point;
import org.microg.safeparcel.AutoSafeParcelable;
import org.microg.safeparcel.SafeParceled;
public class Barcode extends AutoSafeParcelable{
public static final int FORMAT_UNKNOWN = -1;
public static final int FORMAT_ALL_FORMATS = 0;
public static final int FORMAT_CODE_128 = 1;
public static final int FORMAT_CODE_39 = 2;
public static final int FORMAT_CODE_93 = 4;
public static final int FORMAT_CODABAR = 8;
public static final int FORMAT_DATA_MATRIX = 16;
public static final int FORMAT_EAN_13 = 32;
public static final int FORMAT_EAN_8 = 64;
public static final int FORMAT_ITF = 128;
public static final int FORMAT_QR_CODE = 256;
public static final int FORMAT_UPC_A = 512;
public static final int FORMAT_UPC_E = 1024;
public static final int FORMAT_PDF417 = 2048;
public static final int FORMAT_AZTEC = 4096;
public static final int TYPE_UNKNOWN = 0;
public static final int TYPE_CONTACT_INFO = 1;
public static final int TYPE_EMAIL = 2;
public static final int TYPE_ISBN = 3;
public static final int TYPE_PHONE = 4;
public static final int TYPE_PRODUCT = 5;
public static final int TYPE_SMS = 6;
public static final int TYPE_TEXT = 7;
public static final int TYPE_URL = 8;
public static final int TYPE_WIFI = 9;
public static final int TYPE_GEO = 10;
public static final int TYPE_CALENDAR_EVENT = 11;
public static final int TYPE_DRIVER_LICENSE = 12;
@SafeParceled(1)
public int format;
@SafeParceled(2)
public String displayValue;
@SafeParceled(3)
public String rawValue;
@SafeParceled(4)
public byte[] rawBytes;
@SafeParceled(5)
public Point[] cornerPoints;
@SafeParceled(6)
public int valueType;
@SafeParceled(7)
public Email emailValue;
@SafeParceled(8)
public Phone phoneValue;
@SafeParceled(9)
public SMS smsValue;
@SafeParceled(10)
public WiFi wifiValue;
@SafeParceled(11)
public UrlBookmark urlBookmarkValue;
@SafeParceled(12)
public GeoPoint geoPointValue;
@SafeParceled(13)
public CalendarEvent calendarEventValue;
@SafeParceled(14)
public ContactInfo contactInfoValue;
@SafeParceled(15)
public DriverLicense driverLicenseValue;
public static final Creator<Barcode> CREATOR=new AutoCreator<>(Barcode.class);
// None of the following is needed or used in the Mastodon app and its use cases for QR code scanning,
// but I'm putting it out there in case someone else is crazy enough to want to use Google Services without their libraries
public static class Email extends AutoSafeParcelable{
@SafeParceled(1)
public int type;
@SafeParceled(2)
public String address;
@SafeParceled(3)
public String subject;
@SafeParceled(4)
public String body;
public static final Creator<Email> CREATOR=new AutoCreator<>(Email.class);
}
public static class Phone extends AutoSafeParcelable{
@SafeParceled(1)
public int type;
@SafeParceled(2)
public String number;
public static final Creator<Phone> CREATOR=new AutoCreator<>(Phone.class);
}
public static class SMS extends AutoSafeParcelable{
@SafeParceled(1)
public String message;
@SafeParceled(2)
public String phoneNumber;
public static final Creator<SMS> CREATOR=new AutoCreator<>(SMS.class);
}
public static class WiFi extends AutoSafeParcelable{
@SafeParceled(1)
public String ssid;
@SafeParceled(2)
public String password;
@SafeParceled(3)
public int encryptionType;
public static final Creator<WiFi> CREATOR=new AutoCreator<>(WiFi.class);
}
public static class UrlBookmark extends AutoSafeParcelable{
@SafeParceled(1)
public String title;
@SafeParceled(2)
public String url;
public static final Creator<UrlBookmark> CREATOR=new AutoCreator<>(UrlBookmark.class);
}
public static class GeoPoint extends AutoSafeParcelable{
@SafeParceled(1)
public double lat;
@SafeParceled(2)
public double lng;
public static final Creator<GeoPoint> CREATOR=new AutoCreator<>(GeoPoint.class);
}
public static class EventDateTime extends AutoSafeParcelable{
@SafeParceled(1)
public int year;
@SafeParceled(2)
public int month;
@SafeParceled(3)
public int day;
@SafeParceled(4)
public int hours;
@SafeParceled(5)
public int minutes;
@SafeParceled(6)
public int seconds;
@SafeParceled(7)
public boolean isUtc;
@SafeParceled(8)
public String rawValue;
public static final Creator<EventDateTime> CREATOR=new AutoCreator<>(EventDateTime.class);
}
public static class CalendarEvent extends AutoSafeParcelable{
@SafeParceled(1)
public String summary;
@SafeParceled(2)
public String description;
@SafeParceled(3)
public String location;
@SafeParceled(4)
public String organizer;
@SafeParceled(5)
public String status;
@SafeParceled(6)
public EventDateTime start;
@SafeParceled(7)
public EventDateTime end;
public static final Creator<CalendarEvent> CREATOR=new AutoCreator<>(CalendarEvent.class);
}
public static class Address extends AutoSafeParcelable{
@SafeParceled(1)
public int type;
@SafeParceled(2)
public String[] addressLines;
public static final Creator<Address> CREATOR=new AutoCreator<>(Address.class);
}
public static class PersonName extends AutoSafeParcelable{
@SafeParceled(1)
public String formattedName;
@SafeParceled(2)
public String pronunciation;
@SafeParceled(3)
public String prefix;
@SafeParceled(4)
public String first;
@SafeParceled(5)
public String middle;
@SafeParceled(6)
public String last;
@SafeParceled(7)
public String suffix;
public static final Creator<PersonName> CREATOR=new AutoCreator<>(PersonName.class);
}
public static class ContactInfo extends AutoSafeParcelable{
@SafeParceled(1)
public PersonName name;
@SafeParceled(2)
public String organization;
@SafeParceled(3)
public String title;
@SafeParceled(4)
public Phone[] phones;
@SafeParceled(5)
public Email[] emails;
@SafeParceled(6)
public String[] urls;
@SafeParceled(7)
public Address[] addresses;
public static final Creator<ContactInfo> CREATOR=new AutoCreator<>(ContactInfo.class);
}
public static class DriverLicense extends AutoSafeParcelable{
@SafeParceled(1)
public String documentType;
@SafeParceled(2)
public String firstName;
@SafeParceled(3)
public String middleName;
@SafeParceled(4)
public String lastName;
@SafeParceled(5)
public String gender;
@SafeParceled(6)
public String addressStreet;
@SafeParceled(7)
public String addressCity;
@SafeParceled(8)
public String addressState;
@SafeParceled(9)
public String addressZip;
@SafeParceled(10)
public String licenseNumber;
@SafeParceled(11)
public String issueDate;
@SafeParceled(12)
public String expiryDate;
@SafeParceled(13)
public String birthDate;
@SafeParceled(14)
public String issuingCountry;
public static final Creator<DriverLicense> CREATOR=new AutoCreator<>(DriverLicense.class);
}
}

View File

@@ -0,0 +1,38 @@
package org.joinmastodon.android.googleservices.barcodescanner;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.os.Parcel;
import org.joinmastodon.android.MastodonApp;
public class BarcodeScanner{
public static Intent createIntent(int formats, boolean allowManualInout, boolean enableAutoZoom){
Intent intent=new Intent().setPackage("com.google.android.gms").setAction("com.google.android.gms.mlkit.ACTION_SCAN_BARCODE");
String appName;
ApplicationInfo appInfo=MastodonApp.context.getApplicationInfo();
if(appInfo.labelRes!=0)
appName=MastodonApp.context.getString(appInfo.labelRes);
else
appName=MastodonApp.context.getPackageManager().getApplicationLabel(appInfo).toString();
intent.putExtra("extra_calling_app_name", appName);
intent.putExtra("extra_supported_formats", formats);
intent.putExtra("extra_allow_manual_input", allowManualInout);
intent.putExtra("extra_enable_auto_zoom", enableAutoZoom);
return intent;
}
public static boolean isValidResult(Intent intent){
return intent!=null && intent.hasExtra("extra_barcode_result");
}
public static Barcode getResult(Intent intent){
byte[] serialized=intent.getByteArrayExtra("extra_barcode_result");
Parcel parcel=Parcel.obtain();
parcel.unmarshall(serialized, 0, serialized.length);
parcel.setDataPosition(0);
Barcode barcode=Barcode.CREATOR.createFromParcel(parcel);
parcel.recycle();
return barcode;
}
}

View File

@@ -11,6 +11,7 @@ import org.joinmastodon.android.ui.utils.BlurHashDecoder;
import org.joinmastodon.android.ui.utils.BlurHashDrawable;
import org.parceler.Parcel;
import java.time.Instant;
import java.util.List;
@Parcel
@@ -34,11 +35,14 @@ public class Card extends BaseModel{
public String embedUrl;
public String blurhash;
public List<History> history;
public Instant publishedAt;
public transient Drawable blurhashPlaceholder;
@Override
public void postprocess() throws ObjectValidationException{
if(type==null)
type=Type.LINK;
super.postprocess();
if(blurhash!=null){
Bitmap placeholder=BlurHashDecoder.decode(blurhash, 16, 16);
@@ -64,6 +68,7 @@ public class Card extends BaseModel{
", embedUrl='"+embedUrl+'\''+
", blurhash='"+blurhash+'\''+
", history="+history+
", publishedAt="+publishedAt+
'}';
}

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.model.viewmodel;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import org.joinmastodon.android.GlobalUserPreferences;
@@ -7,6 +8,7 @@ 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;
import org.joinmastodon.android.ui.text.LinkSpan;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import java.util.Collections;
@@ -43,4 +45,13 @@ public class AccountViewModel{
}
this.verifiedLink=verifiedLink;
}
public AccountViewModel stripLinksFromBio(){
if(parsedBio instanceof Spannable spannable){
for(LinkSpan span:spannable.getSpans(0, spannable.length(), LinkSpan.class)){
spannable.removeSpan(span);
}
}
return this;
}
}

View File

@@ -6,6 +6,7 @@ import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;
import org.joinmastodon.android.R;
@@ -59,6 +60,12 @@ public class M3AlertDialogBuilder extends AlertDialog.Builder{
buttonBar.setPadding(V.dp(16), V.dp(16), V.dp(16), V.dp(16));
((View)buttonBar.getParent()).setPadding(0, 0, 0, 0);
}
if(btn==null || btn.getVisibility()==View.GONE){
ListView list=alert.getListView();
if(list!=null){
list.setPadding(list.getPaddingLeft(), list.getPaddingTop(), list.getPaddingRight(), V.dp(24));
}
}
// hacc
int titleID=getContext().getResources().getIdentifier("title_template", "id", "android");
if(titleID!=0){

View File

@@ -90,7 +90,7 @@ public class Snackbar{
if(current!=null)
current.dismiss();
current=this;
WindowManager.LayoutParams lp=new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION_PANEL, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, PixelFormat.TRANSLUCENT);
WindowManager.LayoutParams lp=new WindowManager.LayoutParams(WindowManager.LayoutParams.LAST_APPLICATION_WINDOW, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, PixelFormat.TRANSLUCENT);
lp.width=ViewGroup.LayoutParams.MATCH_PARENT;
lp.height=ViewGroup.LayoutParams.WRAP_CONTENT;
lp.gravity=Gravity.BOTTOM;

View File

@@ -0,0 +1,37 @@
package org.joinmastodon.android.ui;
import android.graphics.Typeface;
import android.os.Build;
import android.util.Property;
import android.widget.TextView;
import androidx.annotation.RequiresApi;
public class ViewProperties{
public static final Property<TextView, Integer> FONT_WEIGHT=new Property<>(Integer.class, "fontWeight"){
@RequiresApi(api = Build.VERSION_CODES.P)
@Override
public Integer get(TextView object){
return object.getTypeface().getWeight();
}
@RequiresApi(api = Build.VERSION_CODES.P)
@Override
public void set(TextView object, Integer value){
// typeface objects are cached internally, I looked at AOSP sources to confirm that
object.setTypeface(Typeface.create(null, value, false));
}
};
public static final Property<TextView, Integer> TEXT_COLOR=new Property<>(Integer.class, "textColor"){
@Override
public Integer get(TextView object){
return object.getCurrentTextColor();
}
@Override
public void set(TextView object, Integer value){
object.setTextColor(value);
}
};
}

View File

@@ -169,7 +169,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
private void onShareClick(View v){
UiUtils.openSystemShareSheet(v.getContext(), item.status.url);
UiUtils.openSystemShareSheet(v.getContext(), item.status);
}
private int descriptionForId(int id){

View File

@@ -197,7 +197,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
}else if(id==R.id.bookmark){
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setBookmarked(item.status, !item.status.bookmarked);
}else if(id==R.id.share){
UiUtils.openSystemShareSheet(activity, item.status.url);
UiUtils.openSystemShareSheet(activity, item.status);
}else if(id==R.id.translate){
item.parentFragment.togglePostTranslation(item.status, item.parentID);
}else if(id==R.id.add_to_list){

View File

@@ -1,6 +1,8 @@
package org.joinmastodon.android.ui.displayitems;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.text.TextUtils;
@@ -13,6 +15,7 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.Card;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
import org.joinmastodon.android.ui.utils.UiUtils;
@@ -35,7 +38,7 @@ public class LinkCardStatusDisplayItem extends StatusDisplayItem{
@Override
public Type getType(){
return Type.CARD;
return status.card.type==Card.Type.VIDEO || (status.card.image!=null && status.card.width>status.card.height) ? Type.CARD_LARGE : Type.CARD_COMPACT;
}
@Override
@@ -49,36 +52,65 @@ public class LinkCardStatusDisplayItem extends StatusDisplayItem{
}
public static class Holder extends StatusDisplayItem.Holder<LinkCardStatusDisplayItem> implements ImageLoaderViewHolder{
private final TextView title, description, domain;
private final TextView title, description, domain, timestamp;
private final ImageView photo;
private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable();
private boolean didClear;
private final View inner;
private final boolean isLarge;
public Holder(Context context, ViewGroup parent){
super(context, R.layout.display_item_link_card, parent);
public Holder(Context context, ViewGroup parent, boolean isLarge){
super(context, isLarge ? R.layout.display_item_link_card : R.layout.display_item_link_card_compact, parent);
this.isLarge=isLarge;
title=findViewById(R.id.title);
description=findViewById(R.id.description);
domain=findViewById(R.id.domain);
timestamp=findViewById(R.id.timestamp);
photo=findViewById(R.id.photo);
findViewById(R.id.inner).setOnClickListener(this::onClick);
inner=findViewById(R.id.inner);
inner.setOnClickListener(this::onClick);
inner.setOutlineProvider(OutlineProviders.roundedRect(12));
inner.setClipToOutline(true);
}
@SuppressLint("SetTextI18n")
@Override
public void onBind(LinkCardStatusDisplayItem item){
Card card=item.status.card;
title.setText(card.title);
description.setText(card.description);
description.setVisibility(TextUtils.isEmpty(card.description) ? View.GONE : View.VISIBLE);
domain.setText(Uri.parse(card.url).getHost());
if(description!=null){
description.setText(card.description);
description.setVisibility(TextUtils.isEmpty(card.description) ? View.GONE : View.VISIBLE);
}
String cardDomain=Uri.parse(card.url).getHost();
if(isLarge && !TextUtils.isEmpty(card.authorName)){
domain.setText(itemView.getContext().getString(R.string.article_by_author, card.authorName)+" · "+cardDomain);
}else{
domain.setText(cardDomain);
}
if(card.publishedAt!=null){
timestamp.setVisibility(View.VISIBLE);
timestamp.setText(" · "+UiUtils.formatRelativeTimestamp(itemView.getContext(), card.publishedAt));
}else{
timestamp.setVisibility(View.GONE);
}
photo.setImageDrawable(null);
if(item.imgRequest!=null){
photo.setScaleType(ImageView.ScaleType.CENTER_CROP);
photo.setBackground(null);
photo.setImageTintList(null);
crossfadeDrawable.setSize(card.width, card.height);
crossfadeDrawable.setBlurhashDrawable(card.blurhashPlaceholder);
crossfadeDrawable.setCrossfadeAlpha(0f);
photo.setImageDrawable(null);
photo.setImageDrawable(crossfadeDrawable);
didClear=false;
}else{
photo.setBackgroundColor(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3SurfaceVariant));
photo.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3Outline)));
photo.setScaleType(ImageView.ScaleType.CENTER);
photo.setImageResource(R.drawable.ic_feed_48px);
}
}

View File

@@ -26,6 +26,7 @@ import org.joinmastodon.android.model.Translation;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.PhotoLayoutHelper;
import org.joinmastodon.android.ui.drawables.SpoilerStripesDrawable;
import org.joinmastodon.android.ui.photoviewer.AltTextSheet;
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
import org.joinmastodon.android.ui.utils.MediaAttachmentViewController;
import org.joinmastodon.android.ui.views.FrameLayoutThatOnlyMeasuresFirstChild;
@@ -103,11 +104,6 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
private final ArrayList<MediaAttachmentViewController> controllers=new ArrayList<>();
private final MaxWidthFrameLayout overlays;
private final FrameLayout altTextWrapper;
private final TextView altTextButton;
private final View altTextScroller;
private final ImageButton altTextClose;
private final TextView altText;
private final View sensitiveOverlay;
private final LayerDrawable sensitiveOverlayBG;
@@ -115,9 +111,6 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
private final TextView hideSensitiveButton;
private final TextView sensitiveText;
private int altTextIndex=-1;
private Animator altTextAnimator;
public Holder(Activity activity, ViewGroup parent){
super(new FrameLayoutThatOnlyMeasuresFirstChild(activity));
wrapper=(FrameLayout)itemView;
@@ -129,14 +122,6 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
overlays.setMaxWidth(V.dp(MediaGridLayout.MAX_WIDTH));
wrapper.addView(overlays, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER_HORIZONTAL));
activity.getLayoutInflater().inflate(R.layout.overlay_image_alt_text, overlays);
altTextWrapper=findViewById(R.id.alt_text_wrapper);
altTextButton=findViewById(R.id.alt_button);
altTextScroller=findViewById(R.id.alt_text_scroller);
altTextClose=findViewById(R.id.alt_text_close);
altText=findViewById(R.id.alt_text);
altTextClose.setOnClickListener(this::onAltTextCloseClick);
hideSensitiveButton=(TextView) activity.getLayoutInflater().inflate(R.layout.alt_text_badge, overlays, false);
hideSensitiveButton.setText(R.string.hide);
FrameLayout.LayoutParams lp;
@@ -160,9 +145,6 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
public void onBind(MediaGridStatusDisplayItem item){
wrapper.setPadding(0, 0, 0, item.inset ? 0 : V.dp(8));
if(altTextAnimator!=null)
altTextAnimator.cancel();
layout.setTiledLayout(item.tiledLayout);
for(MediaAttachmentViewController c:controllers){
item.viewPool.reuse(c.type, c);
@@ -212,8 +194,6 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
c.bind(att, item.status);
i++;
}
altTextWrapper.setVisibility(View.GONE);
altTextIndex=-1;
if(!item.sensitiveRevealed){
sensitiveOverlay.setVisibility(View.VISIBLE);
@@ -246,115 +226,9 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
}
private void onAltTextClick(View v){
if(altTextAnimator!=null)
altTextAnimator.cancel();
v.setVisibility(View.INVISIBLE);
int index=(Integer)v.getTag();
altTextIndex=index;
Attachment att=item.attachments.get(index);
altText.setText(att.description);
altTextWrapper.setVisibility(View.VISIBLE);
altTextWrapper.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
altTextWrapper.getViewTreeObserver().removeOnPreDrawListener(this);
int[] loc={0, 0};
v.getLocationInWindow(loc);
int btnL=loc[0], btnT=loc[1];
overlays.getLocationInWindow(loc);
btnL-=loc[0];
btnT-=loc[1];
ArrayList<Animator> anims=new ArrayList<>();
anims.add(ObjectAnimator.ofFloat(altTextButton, View.ALPHA, 1, 0));
anims.add(ObjectAnimator.ofFloat(altTextScroller, View.ALPHA, 0, 1));
anims.add(ObjectAnimator.ofFloat(altTextClose, View.ALPHA, 0, 1));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "left", btnL, altTextWrapper.getLeft()));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "top", btnT, altTextWrapper.getTop()));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "right", btnL+v.getWidth(), altTextWrapper.getRight()));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "bottom", btnT+v.getHeight(), altTextWrapper.getBottom()));
for(Animator a:anims)
a.setDuration(300);
for(MediaAttachmentViewController c:controllers){
if(c.altButton!=null && c.altButton!=v){
anims.add(ObjectAnimator.ofFloat(c.altButton, View.ALPHA, 1, 0).setDuration(150));
}
}
AnimatorSet set=new AnimatorSet();
set.playTogether(anims);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
altTextAnimator=null;
for(MediaAttachmentViewController c:controllers){
if(c.altButton!=null){
c.altButton.setVisibility(View.INVISIBLE);
}
}
}
});
altTextAnimator=set;
set.start();
return true;
}
});
}
private void onAltTextCloseClick(View v){
if(altTextAnimator!=null)
altTextAnimator.cancel();
View btn=controllers.get(altTextIndex).altButton;
int i=0;
for(MediaAttachmentViewController c:controllers){
if(c.altButton!=null && c.altButton!=btn && !TextUtils.isEmpty(item.attachments.get(i).description))
c.altButton.setVisibility(View.VISIBLE);
i++;
}
int[] loc={0, 0};
btn.getLocationInWindow(loc);
int btnL=loc[0], btnT=loc[1];
overlays.getLocationInWindow(loc);
btnL-=loc[0];
btnT-=loc[1];
ArrayList<Animator> anims=new ArrayList<>();
anims.add(ObjectAnimator.ofFloat(altTextButton, View.ALPHA, 1));
anims.add(ObjectAnimator.ofFloat(altTextScroller, View.ALPHA, 0));
anims.add(ObjectAnimator.ofFloat(altTextClose, View.ALPHA, 0));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "left", btnL));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "top", btnT));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "right", btnL+btn.getWidth()));
anims.add(ObjectAnimator.ofInt(altTextWrapper, "bottom", btnT+btn.getHeight()));
for(Animator a:anims)
a.setDuration(300);
for(MediaAttachmentViewController c:controllers){
if(c.altButton!=null && c.altButton!=btn){
anims.add(ObjectAnimator.ofFloat(c.altButton, View.ALPHA, 1).setDuration(150));
}
}
AnimatorSet set=new AnimatorSet();
set.playTogether(anims);
set.setInterpolator(CubicBezierInterpolator.DEFAULT);
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
altTextAnimator=null;
altTextWrapper.setVisibility(View.GONE);
btn.setVisibility(View.VISIBLE);
btn.setAlpha(1);
}
});
altTextAnimator=set;
set.start();
new AltTextSheet(v.getContext(), att).show();
}
public MediaAttachmentViewController getViewController(int index){

View File

@@ -68,7 +68,8 @@ public abstract class StatusDisplayItem{
case AUDIO -> new AudioStatusDisplayItem.Holder(activity, parent);
case POLL_OPTION -> new PollOptionStatusDisplayItem.Holder(activity, parent);
case POLL_FOOTER -> new PollFooterStatusDisplayItem.Holder(activity, parent);
case CARD -> new LinkCardStatusDisplayItem.Holder(activity, parent);
case CARD_LARGE -> new LinkCardStatusDisplayItem.Holder(activity, parent, true);
case CARD_COMPACT -> new LinkCardStatusDisplayItem.Holder(activity, parent, false);
case FOOTER -> new FooterStatusDisplayItem.Holder(activity, parent);
case ACCOUNT -> new AccountStatusDisplayItem.Holder(new AccountViewHolder(parentFragment, parent, null));
case HASHTAG -> new HashtagStatusDisplayItem.Holder(activity, parent);
@@ -208,7 +209,8 @@ public abstract class StatusDisplayItem{
AUDIO,
POLL_OPTION,
POLL_FOOTER,
CARD,
CARD_LARGE,
CARD_COMPACT,
FOOTER,
ACCOUNT,
HASHTAG,

View File

@@ -0,0 +1,149 @@
package org.joinmastodon.android.ui.drawables;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import com.google.zxing.common.BitMatrix;
import java.util.Arrays;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class FancyQrCodeDrawable extends Drawable{
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
private Path path=new Path(), scaledPath=new Path();
private int size, logoOffset, logoSize;
private Drawable logo;
public FancyQrCodeDrawable(BitMatrix code, int color, Drawable logo){
paint.setColor(color);
this.logo=logo;
size=code.getWidth();
addMarker(0, 0);
addMarker(size-7, 0);
addMarker(0, size-7);
float[] radii=new float[8];
logoSize=size/3;
if((size-logoSize)%2!=0){
logoSize--;
}
logoOffset=(size-logoSize)/2;
for(int y=0;y<logoSize;y++){
for(int x=0;x<logoSize;x++){
code.unset(x+logoOffset, y+logoOffset);
}
}
for(int y=0;y<size;y++){
for(int x=0;x<size;x++){
// Skip corner markers because they turn out ugly with this algorithm
if((x<7 && y<7) || (x>size-8 && y<7) || (x<7 && y>size-8)){
continue;
}
if(code.get(x, y)){
boolean t=y>0 && code.get(x, y-1);
boolean b=y<size-1 && code.get(x, y+1);
boolean l=x>0 && code.get(x-1, y);
boolean r=x<size-1 && code.get(x+1, y);
int neighborCount=(l ? 1 : 0)+(t ? 1 : 0)+(r ? 1 : 0)+(b ? 1 : 0);
// Special-case optimizations
if(neighborCount>=3 || (neighborCount==2 && ((l && r) || (t && b)))){ // 3 or 4 neighbors, or part of a straight line
path.addRect(x, y, x+1, y+1, Path.Direction.CW);
continue;
}else if(neighborCount==0){ // No neighbors
path.addCircle(x+0.5f, y+0.5f, 0.5f, Path.Direction.CW);
continue;
}
Arrays.fill(radii, 0);
if(l && t){ // round bottom-right corner
radii[4]=radii[5]=1;
}else if(t && r){ // round bottom-left corner
radii[6]=radii[7]=1;
}else if(r && b){ // round top-left corner
radii[0]=radii[1]=1;
}else if(b && l){ // round top-right corner
radii[2]=radii[3]=1;
}else if(l){ // right side
radii[2]=radii[3]=radii[4]=radii[5]=0.5f;
}else if(t){ // bottom side
radii[4]=radii[5]=radii[6]=radii[7]=0.5f;
}else if(r){ // left side
radii[6]=radii[7]=radii[1]=radii[0]=0.5f;
}else{ // top side
radii[0]=radii[1]=radii[2]=radii[3]=0.5f;
}
path.addRoundRect(x, y, x+1, y+1, radii, Path.Direction.CW);
}
}
}
}
private void addMarker(int x, int y){
path.addRoundRect(x, y, x+7, y+7, 2.38f, 2.38f, Path.Direction.CW);
path.addRoundRect(x+1, y+1, x+6, y+6, 1.33f, 1.33f, Path.Direction.CCW);
path.addRoundRect(x+2, y+2, x+5, y+5, 0.8f, 0.8f, Path.Direction.CW);
}
@Override
public void draw(@NonNull Canvas canvas){
Rect bounds=getBounds();
float factor=Math.min(bounds.width(), bounds.height())/(float)size;
float xOff=0, yOff=0;
float bw=bounds.width(), bh=bounds.height();
if(bw>bh){
xOff=bw/2f-bh/2f;
}else if(bw<bh){
yOff=bh/2f-bw/2f;
}
canvas.save();
canvas.translate(-bounds.left+xOff, -bounds.top+yOff);
canvas.drawPath(scaledPath, paint);
int scaledOffset=Math.round((logoOffset+1)*factor);
int scaledSize=Math.round((logoSize-2)*factor);
logo.setBounds(scaledOffset, scaledOffset, scaledOffset+scaledSize, scaledOffset+scaledSize);
logo.draw(canvas);
canvas.restore();
}
@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 size;
}
@Override
public int getIntrinsicHeight(){
return size;
}
@Override
protected void onBoundsChange(@NonNull Rect bounds){
super.onBoundsChange(bounds);
float factor=Math.min(bounds.width(), bounds.height())/(float)size;
scaledPath.rewind();
Matrix matrix=new Matrix();
matrix.setScale(factor, factor);
scaledPath.addPath(path, matrix);
}
}

View File

@@ -0,0 +1,133 @@
package org.joinmastodon.android.ui.drawables;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.SystemClock;
import java.util.ArrayList;
import java.util.Random;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class RadialParticleSystemDrawable extends Drawable{
private long particleLifetime;
private int birthRate;
private int startColor, endColor;
private float velocity, velocityVariance;
private float size;
private ArrayList<Particle> activeParticles=new ArrayList<>(), nextActiveParticles=new ArrayList<>(), pool=new ArrayList<>();
private int emitterX, emitterY;
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
private float[] linearStartColor, linearEndColor;
private long prevFrameTime;
private Random rand=new Random();
private Rect clipOutBounds=new Rect();
public RadialParticleSystemDrawable(long particleLifetime, int birthRate, int startColor, int endColor, float velocity, float velocityVariance, float size){
this.particleLifetime=particleLifetime;
this.birthRate=birthRate;
this.startColor=startColor;
this.endColor=endColor;
this.velocity=velocity;
this.velocityVariance=velocityVariance;
this.size=size;
linearStartColor=new float[]{
((startColor >> 24) & 0xFF)/255f,
(float)Math.pow(((startColor >> 16) & 0xFF)/255f, 2.2),
(float)Math.pow(((startColor >> 8) & 0xFF)/255f, 2.2),
(float)Math.pow((startColor & 0xFF)/255f, 2.2)
};
linearEndColor=new float[]{
((endColor >> 24) & 0xFF)/255f,
(float)Math.pow(((endColor >> 16) & 0xFF)/255f, 2.2),
(float)Math.pow(((endColor >> 8) & 0xFF)/255f, 2.2),
(float)Math.pow((endColor & 0xFF)/255f, 2.2)
};
}
@Override
public void draw(@NonNull Canvas canvas){
long now=SystemClock.uptimeMillis();
nextActiveParticles.clear();
for(Particle p:activeParticles){
int time=(int)(now-p.birthTime);
if(time>particleLifetime){
pool.add(p);
continue;
}
nextActiveParticles.add(p);
float x=emitterX+time/1000f*p.velX;
float y=emitterY+time/1000f*p.velY;
if(clipOutBounds.contains((int)x, (int)y)){
continue;
}
float fraction=time/(float)particleLifetime;
paint.setColor(interpolateColor(fraction));
canvas.drawCircle(x, y, size, paint);
}
long timeDiff=Math.min(100, now-prevFrameTime);
int newParticleCount=Math.round(timeDiff/1000f*birthRate);
for(int i=0;i<newParticleCount;i++){
Particle p;
if(!pool.isEmpty())
p=pool.remove(pool.size()-1);
else
p=new Particle();
p.birthTime=now;
double angle=rand.nextDouble()*Math.PI*2;
float vel=velocity+velocityVariance*(rand.nextFloat()*2-1f);
p.velX=vel*(float)Math.cos(angle);
p.velY=vel*(float)Math.sin(angle);
nextActiveParticles.add(p);
}
ArrayList<Particle> tmp=nextActiveParticles;
nextActiveParticles=activeParticles;
activeParticles=tmp;
invalidateSelf();
prevFrameTime=now;
}
@Override
public void setAlpha(int alpha){
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter){
}
@Override
public int getOpacity(){
return PixelFormat.TRANSLUCENT;
}
public void setClipOutBounds(int l, int t, int r, int b){
clipOutBounds.set(l, t, r, b);
}
private int interpolateColor(float fraction){
float a=(linearStartColor[0]+(linearEndColor[0]-linearStartColor[0])*fraction)*255f;
float r=(float)Math.pow(linearStartColor[1]+(linearEndColor[1]-linearStartColor[1])*fraction, 1.0/2.2)*255f;
float g=(float)Math.pow(linearStartColor[2]+(linearEndColor[2]-linearStartColor[2])*fraction, 1.0/2.2)*255f;
float b=(float)Math.pow(linearStartColor[3]+(linearEndColor[3]-linearStartColor[3])*fraction, 1.0/2.2)*255f;
return (Math.round(a) << 24) | (Math.round(r) << 16) | (Math.round(g) << 8) | Math.round(b);
}
public void setEmitterPosition(int x, int y){
emitterX=x;
emitterY=y;
}
private static class Particle{
public long birthTime;
public float velX, velY;
}
}

View File

@@ -0,0 +1,37 @@
package org.joinmastodon.android.ui.photoviewer;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import androidx.annotation.NonNull;
import me.grishka.appkit.views.BottomSheet;
public class AltTextSheet extends BottomSheet{
public AltTextSheet(@NonNull Context context, Attachment attachment){
super(context);
View content=context.getSystemService(LayoutInflater.class).inflate(R.layout.sheet_alt_text, null);
setContentView(content);
TextView altText=findViewById(R.id.alt_text);
altText.setText(attachment.description);
findViewById(R.id.alt_text_help).setOnClickListener(v->showAltTextHelp());
setNavigationBarBackground(new ColorDrawable(UiUtils.alphaBlendColors(UiUtils.getThemeColor(context, R.attr.colorM3Surface),
UiUtils.getThemeColor(context, R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
}
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();
}
}

View File

@@ -1,12 +1,19 @@
package org.joinmastodon.android.ui.photoviewer;
import android.Manifest;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
import android.graphics.Insets;
@@ -26,6 +33,8 @@ import android.os.SystemClock;
import android.provider.MediaStore;
import android.provider.Settings;
import android.util.Log;
import android.util.Property;
import android.view.ContextThemeWrapper;
import android.view.DisplayCutout;
import android.view.Gravity;
import android.view.KeyEvent;
@@ -48,8 +57,13 @@ import android.widget.Toolbar;
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.model.Attachment;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.Snackbar;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.io.File;
import java.io.FileOutputStream;
@@ -85,6 +99,8 @@ public class PhotoViewer implements ZoomPanView.Listener{
private int currentIndex;
private WindowManager wm;
private Listener listener;
private Status status;
private String accountID;
private FrameLayout windowView;
private FragmentRootLinearLayout uiOverlay;
@@ -104,17 +120,49 @@ public class PhotoViewer implements ZoomPanView.Listener{
if(uiVisible)
toggleUI();
};
private Animator currentSheetRelatedToolbarAnimation;
private boolean videoPositionNeedsUpdating;
private Runnable videoPositionUpdater=this::updateVideoPosition;
private int videoDuration, videoInitialPosition, videoLastTimeUpdatePosition;
private long videoInitialPositionTime;
private long lastDownloadID;
private boolean receiverRegistered;
public PhotoViewer(Activity activity, List<Attachment> attachments, int index, Listener listener){
private static final Property<FragmentRootLinearLayout, Integer> STATUS_BAR_COLOR_PROPERTY=new Property<>(Integer.class, "Fdsafdsa"){
@Override
public Integer get(FragmentRootLinearLayout object){
return object.getStatusBarColor();
}
@Override
public void set(FragmentRootLinearLayout object, Integer value){
object.setStatusBarColor(value);
}
};
private final BroadcastReceiver downloadCompletedReceiver=new BroadcastReceiver(){
@Override
public void onReceive(Context context, Intent intent){
long id=intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
if(id==lastDownloadID){
new Snackbar.Builder(activity)
.setText(R.string.video_saved)
.setAction(R.string.view_file, ()->activity.startActivity(new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)))
.show();
activity.unregisterReceiver(this);
receiverRegistered=false;
}
}
};
public PhotoViewer(Activity activity, 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;
wm=activity.getWindowManager();
@@ -175,9 +223,15 @@ public class PhotoViewer implements ZoomPanView.Listener{
toolbarWrap=uiOverlay.findViewById(R.id.toolbar_wrap);
toolbar=uiOverlay.findViewById(R.id.toolbar);
toolbar.setNavigationOnClickListener(v->onStartSwipeToDismissTransition(0));
toolbar.getMenu().add(R.string.download).setIcon(R.drawable.ic_download_24px).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
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->{
saveCurrentFile();
if(status!=null)
showInfoSheet();
else
saveCurrentFile();
return true;
});
uiOverlay.setAlpha(0f);
@@ -337,6 +391,9 @@ public class PhotoViewer implements ZoomPanView.Listener{
listener.setPhotoViewVisibility(pager.getCurrentItem(), true);
wm.removeView(windowView);
listener.photoViewerDismissed();
if(receiverRegistered){
activity.unregisterReceiver(downloadCompletedReceiver);
}
}
@Override
@@ -498,7 +555,12 @@ public class PhotoViewer implements ZoomPanView.Listener{
BufferedSink buf=Okio.buffer(sink);
buf.writeAll(src);
buf.flush();
activity.runOnUiThread(()->Toast.makeText(activity, R.string.file_saved, Toast.LENGTH_SHORT).show());
activity.runOnUiThread(()->{
new Snackbar.Builder(activity)
.setText(R.string.image_saved)
.setAction(R.string.view_file, ()->activity.startActivity(new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS)))
.show();
});
if(Build.VERSION.SDK_INT<29){
String fileName=Uri.parse(att.url).getLastPathSegment();
File dstFile=new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName);
@@ -506,12 +568,18 @@ public class PhotoViewer implements ZoomPanView.Listener{
}
}catch(IOException x){
Log.w(TAG, "doSaveCurrentFile: ", x);
activity.runOnUiThread(()->Toast.makeText(activity, R.string.error_saving_file, Toast.LENGTH_SHORT).show());
activity.runOnUiThread(()->{
new Snackbar.Builder(activity)
.setText(R.string.error_saving_file)
.show();
});
}
});
}catch(IOException x){
Log.w(TAG, "doSaveCurrentFile: ", x);
Toast.makeText(activity, R.string.error_saving_file, Toast.LENGTH_SHORT).show();
new Snackbar.Builder(activity)
.setText(R.string.error_saving_file)
.show();
}
}else{
saveViaDownloadManager(att);
@@ -524,8 +592,12 @@ public class PhotoViewer implements ZoomPanView.Listener{
req.allowScanningByMediaScanner();
req.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
req.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, uri.getLastPathSegment());
activity.getSystemService(DownloadManager.class).enqueue(req);
Toast.makeText(activity, R.string.downloading, Toast.LENGTH_SHORT).show();
activity.registerReceiver(downloadCompletedReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
receiverRegistered=true;
lastDownloadID=activity.getSystemService(DownloadManager.class).enqueue(req);
new Snackbar.Builder(activity)
.setText(R.string.downloading)
.show();
}
private void onAudioFocusChanged(int change){
@@ -610,6 +682,93 @@ public class PhotoViewer implements ZoomPanView.Listener{
}
}
private void showInfoSheet(){
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);
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;
}
});
}
public interface Listener{
void setPhotoViewVisibility(int index, boolean visible);

View File

@@ -0,0 +1,180 @@
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(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.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.setOnClickListener(v->dismiss());
FrameLayout.LayoutParams lp=new FrameLayout.LayoutParams(V.dp(48), V.dp(48));
lp.topMargin=toolbarHeight/2-V.dp(24);
lp.leftMargin=lp.rightMargin=V.dp(4);
lp.gravity=Gravity.START | Gravity.TOP;
container.addView(backButton, lp);
lp=new FrameLayout.LayoutParams(lp);
lp.leftMargin=lp.rightMargin=0;
lp.gravity=Gravity.END | Gravity.TOP;
container.addView(infoButton, lp);
boostBtn=findViewById(R.id.btn_boost);
favoriteBtn=findViewById(R.id.btn_favorite);
bookmarkBtn=findViewById(R.id.btn_bookmark);
View.OnClickListener clickListener=v->listener.onButtonClick(v.getId());
boostBtn.setOnClickListener(clickListener);
favoriteBtn.setOnClickListener(clickListener);
findViewById(R.id.btn_share).setOnClickListener(clickListener);
bookmarkBtn.setOnClickListener(clickListener);
findViewById(R.id.btn_download).setOnClickListener(clickListener);
}
private void showAltTextHelp(){
new M3AlertDialogBuilder(getContext())
.setTitle(R.string.what_is_alt_text)
.setMessage(UiUtils.fixBulletListInString(getContext(), R.string.alt_text_help))
.setPositiveButton(R.string.ok, null)
.show();
}
@Override
public void dismiss(){
if(dismissed)
return;
int height=content.getHeight();
int duration=Math.max(60, (int) (180 * (height - content.getTranslationY()) / (float) height));
listener.onBeforeDismiss(duration);
backButton.animate().alpha(0).setDuration(duration).setInterpolator(CubicBezierInterpolator.EASE_OUT).start();
infoButton.animate().alpha(0).setDuration(duration).setInterpolator(CubicBezierInterpolator.EASE_OUT).start();
super.dismiss();
E.unregister(this);
}
@Override
public void show(){
super.show();
E.register(this);
content.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
content.getViewTreeObserver().removeOnPreDrawListener(this);
backButton.animate().alpha(1).setDuration(300).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
infoButton.animate().alpha(1).setDuration(300).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
return true;
}
});
}
public void setStatus(Status status){
statusID=status.id;
boostBtn.setCompoundDrawablesWithIntrinsicBounds(0, switch(status.visibility){
case DIRECT -> R.drawable.ic_boost_disabled_24px;
case PUBLIC, UNLISTED -> R.drawable.ic_boost;
case PRIVATE -> R.drawable.ic_boost_private;
}, 0, 0);
boostBtn.setEnabled(status.visibility!=StatusPrivacy.DIRECT);
setButtonStates(status.reblogged, status.favourited, status.bookmarked);
}
@Subscribe
public void onCountersUpdated(StatusCountersUpdatedEvent ev){
if(ev.id.equals(statusID)){
setButtonStates(ev.reblogged, ev.favorited, ev.bookmarked);
}
}
private void setButtonStates(boolean reblogged, boolean favorited, boolean bookmarked){
boostBtn.setText(reblogged ? R.string.button_reblogged : R.string.button_reblog);
boostBtn.setSelected(reblogged);
favoriteBtn.setText(favorited ? R.string.button_favorited : R.string.button_favorite);
favoriteBtn.setSelected(favorited);
bookmarkBtn.setText(bookmarked ? R.string.bookmarked : R.string.add_bookmark);
bookmarkBtn.setSelected(bookmarked);
}
public interface Listener{
void onBeforeDismiss(int duration);
void onDismissEntireViewer();
void onButtonClick(int id);
}
}

View File

@@ -0,0 +1,90 @@
package org.joinmastodon.android.ui.sheets;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.InsetDrawable;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.drawables.EmptyDrawable;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.BottomSheet;
public abstract class AccountRestrictionConfirmationSheet extends BottomSheet{
private LinearLayout contentWrap;
protected Button cancelBtn;
protected ProgressBarButton confirmBtn, secondaryBtn;
protected TextView titleView, subtitleView;
protected ImageView icon;
protected boolean loading;
public AccountRestrictionConfirmationSheet(@NonNull Context context, Account user, ConfirmCallback confirmCallback){
super(context);
View content=context.getSystemService(LayoutInflater.class).inflate(R.layout.sheet_restrict_account, null);
setContentView(content);
setNavigationBarBackground(new ColorDrawable(UiUtils.alphaBlendColors(UiUtils.getThemeColor(context, R.attr.colorM3Surface),
UiUtils.getThemeColor(context, R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
contentWrap=findViewById(R.id.content_wrap);
titleView=findViewById(R.id.title);
subtitleView=findViewById(R.id.text);
cancelBtn=findViewById(R.id.btn_cancel);
confirmBtn=findViewById(R.id.btn_confirm);
secondaryBtn=findViewById(R.id.btn_secondary);
icon=findViewById(R.id.icon);
contentWrap.setDividerDrawable(new EmptyDrawable(1, V.dp(8)));
contentWrap.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
confirmBtn.setOnClickListener(v->{
if(loading)
return;
loading=true;
confirmBtn.setProgressBarVisible(true);
confirmCallback.onConfirmed(this::dismiss, ()->{
confirmBtn.setProgressBarVisible(false);
loading=false;
});
});
cancelBtn.setOnClickListener(v->{
if(!loading)
dismiss();
});
}
protected void 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.setCompoundDrawableTintList(ColorStateList.valueOf(UiUtils.getThemeColor(getContext(), R.attr.colorM3Primary)));
tv.setGravity(Gravity.START | Gravity.CENTER_VERTICAL);
tv.setText(text);
InsetDrawable drawable=new InsetDrawable(getContext().getResources().getDrawable(icon, getContext().getTheme()), V.dp(8));
drawable.setBounds(0, 0, V.dp(40), V.dp(40));
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));
}
protected void addRow(@DrawableRes int icon, @StringRes int text){
addRow(icon, getContext().getString(text));
}
public interface ConfirmCallback{
void onConfirmed(Runnable onSuccess, Runnable onError);
}
}

View File

@@ -1,14 +1,12 @@
package org.joinmastodon.android.ui;
package org.joinmastodon.android.ui.sheets;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
@@ -25,6 +23,9 @@ import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.SplashFragment;
import org.joinmastodon.android.ui.ClickableSingleViewRecyclerAdapter;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.CheckableRelativeLayout;
@@ -37,7 +38,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.recyclerview.widget.LinearLayoutManager;
import me.grishka.appkit.FragmentStackActivity;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
@@ -60,6 +60,7 @@ public class AccountSwitcherSheet extends BottomSheet{
private UsableRecyclerView list;
private List<WrappedAccount> accounts;
private ListImageLoaderWrapper imgLoader;
private Runnable onLoggedOutCallback;
public AccountSwitcherSheet(@NonNull Activity activity, @Nullable HomeFragment fragment){
super(activity);
@@ -95,6 +96,11 @@ public class AccountSwitcherSheet extends BottomSheet{
UiUtils.getThemeColor(activity, R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
}
public AccountSwitcherSheet setOnLoggedOutCallback(Runnable onLoggedOutCallback){
this.onLoggedOutCallback=onLoggedOutCallback;
return this;
}
private void confirmLogOut(String accountID){
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
new M3AlertDialogBuilder(activity)
@@ -113,7 +119,10 @@ public class AccountSwitcherSheet extends BottomSheet{
}
private void logOut(String accountID){
String activeAccount=AccountSessionManager.getInstance().getLastActiveAccountID();
AccountSessionManager.get(accountID).logOut(activity, ()->{
if(accountID.equals(activeAccount) && onLoggedOutCallback!=null)
onLoggedOutCallback.run();
dismiss();
((MainActivity)activity).restartHomeFragment();
});
@@ -133,6 +142,8 @@ public class AccountSwitcherSheet extends BottomSheet{
AccountSessionManager.getInstance().removeAccount(session.getID());
sessions.remove(session);
if(sessions.isEmpty()){
if(onLoggedOutCallback!=null)
onLoggedOutCallback.run();
progress.dismiss();
Nav.goClearingStack(activity, SplashFragment.class, null);
dismiss();
@@ -144,6 +155,8 @@ public class AccountSwitcherSheet extends BottomSheet{
AccountSessionManager.getInstance().removeAccount(session.getID());
sessions.remove(session);
if(sessions.isEmpty()){
if(onLoggedOutCallback!=null)
onLoggedOutCallback.run();
progress.dismiss();
Nav.goClearingStack(activity, SplashFragment.class, null);
dismiss();

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