Compare commits

...

134 Commits

Author SHA1 Message Date
Matthieu 187c29a751 Merge branch 'translations' into 'master'
Translations update from Weblate

See merge request pixeldroid/PixelDroid!590
2024-04-15 03:09:08 +00:00
Balaraz f9a07a5dd0 Translated using Weblate (Ukrainian)
Currently translated at 73.0% (19 of 26 strings)

Translated using Weblate (Ukrainian)

Currently translated at 97.2% (248 of 255 strings)

Translated using Weblate (Ukrainian)

Currently translated at 97.2% (248 of 255 strings)

Co-authored-by: Balaraz <balaraz@tuta.io>
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/app/uk/
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/fastlane/uk/
Translation: PixelDroid/Fastlane
Translation: PixelDroid/pixeldroid
2024-04-08 06:15:38 +00:00
Alexandre NICOLADIE fb9296187a Translated using Weblate (French)
Currently translated at 100.0% (26 of 26 strings)

Translated using Weblate (French)

Currently translated at 100.0% (255 of 255 strings)

Translated using Weblate (French)

Currently translated at 26.9% (7 of 26 strings)

Translated using Weblate (French)

Currently translated at 80.3% (205 of 255 strings)

Co-authored-by: Alexandre NICOLADIE <github@nicoladie.fr>
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/app/fr/
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/fastlane/fr/
Translation: PixelDroid/Fastlane
Translation: PixelDroid/pixeldroid
2024-04-08 06:15:37 +00:00
Matthieu 04324577ea Merge branch 'dependencies_upgrade' into 'master'
Dependencies upgrade

See merge request pixeldroid/PixelDroid!589
2024-03-29 08:33:37 +00:00
Matthieu 73f08e5a5f Update dependencies 2024-03-29 09:03:48 +01:00
Matthieu a7feab380b Merge remote-tracking branch 'origin/master' 2024-03-23 15:39:07 +01:00
Matthieu 1a4e023091 Merge branch 'translations' into 'master'
Translations update from Weblate

See merge request pixeldroid/PixelDroid!588
2024-03-22 14:35:59 +00:00
Regu_Miabyss 2fb4c91ffd Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (255 of 255 strings)

Co-authored-by: Regu_Miabyss <Regu_Miabyss@outlook.com>
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/app/zh_Hant/
Translation: PixelDroid/pixeldroid
2024-03-22 14:34:56 +00:00
Matthieu 720c099f0a Merge branch 'changelog_update' into 'master'
Adapt to multiplied version numbers

See merge request pixeldroid/PixelDroid!587
2024-03-16 09:11:38 +00:00
Matthieu 869479d53f Adapt to multiplied version numbers 2024-03-16 09:11:31 +00:00
Matthieu 018f893388 Adapt to multiplied version numbers 2024-03-16 10:10:39 +01:00
Matthieu f27ae611be Merge branch 'release33_3' into 'master'
Fix build typo

See merge request pixeldroid/PixelDroid!586
2024-03-16 07:53:28 +00:00
Matthieu 234be72f59 Fix build typo 2024-03-16 08:52:58 +01:00
Matthieu 7eaac2e903 Merge branch 'release33_2' into 'master'
Release33 fixup

See merge request pixeldroid/PixelDroid!585
2024-03-16 07:41:21 +00:00
Matthieu a9849c13e6 Switch submodule to http pull 2024-03-16 08:37:43 +01:00
Matthieu d1562f18e9 Fix order of abiCodes 2024-03-16 08:34:55 +01:00
Matthieu 4a1248bcab Merge branch 'translations' into 'master'
Translations update from Weblate

See merge request pixeldroid/PixelDroid!583
2024-03-15 18:15:14 +00:00
Regu_Miabyss e9122e6d72 Translated using Weblate (Chinese (Traditional))
Currently translated at 29.0% (74 of 255 strings)

Co-authored-by: Regu_Miabyss <Regu_Miabyss@outlook.com>
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/app/zh_Hant/
Translation: PixelDroid/pixeldroid
2024-03-15 18:07:42 +00:00
Matthieu 76eac62d73 Merge branch 'seperate-apks2' into 'master'
Seperate version codes per architecture

See merge request pixeldroid/PixelDroid!584
2024-03-15 18:07:39 +00:00
Matthieu dd27555d83 Release 33 2024-03-15 18:10:10 +01:00
Matthieu c0feb8a37d Seperate version codes per architecture 2024-03-15 18:01:04 +01:00
Matthieu 7acd4cface Merge branch 'view_models' into 'master'
Use ViewModel in AlbumActivity

See merge request pixeldroid/PixelDroid!558
2024-03-10 10:22:07 +00:00
Matthieu 10e93c90b7 Merge branch 'master' into view_models 2024-03-08 11:06:33 +01:00
Matthieu 2311627473 Merge branch 'translations' into 'master'
Translations update from Weblate

See merge request pixeldroid/PixelDroid!582
2024-03-08 09:04:39 +00:00
Regu_Miabyss 6c7ab2333e Added translation using Weblate (Chinese (Traditional))
Co-authored-by: Regu_Miabyss <Regu_Miabyss@outlook.com>
2024-03-08 09:04:03 +00:00
Matthieu 54711d4e81 Hide dot indicator in fullscreen album 2024-03-07 07:56:30 +01:00
Matthieu 908d1a54c9 Merge branch 'master' into view_models 2024-03-05 10:59:52 +01:00
Matthieu 2b91543137 Merge branch 'seperate-apks' into 'master'
Split apks into ABIs to make them smaller

See merge request pixeldroid/PixelDroid!581
2024-03-01 15:41:30 +00:00
Matthieu 0688dd4d02 Release 32 2024-03-01 16:41:02 +01:00
Matthieu 319da7c11c Split apks into ABIs to make them smaller 2024-03-01 16:36:08 +01:00
Matthieu 7b327fc0d6 Merge branch 'readme-matrix' into 'master'
Update README.md

See merge request pixeldroid/PixelDroid!580
2024-02-27 13:48:08 +00:00
Matthieu 64ab2c2ac5 Update README.md 2024-02-27 13:23:07 +00:00
Matthieu 37b83f5ae2 Merge branch 'fix_uri_mess' into 'master'
Fix crashes due to ClassCastException

See merge request pixeldroid/PixelDroid!579
2024-02-25 11:03:55 +00:00
Matthieu 1516452ab5 Release 31 2024-02-25 12:03:05 +01:00
Matthieu cb50db7730 Fix crashes due to ClassCastException 2024-02-25 11:54:09 +01:00
Matthieu afe6f71152 Merge branch 'release30' into 'master'
Release 30

See merge request pixeldroid/PixelDroid!578
2024-02-14 21:23:13 +00:00
Matthieu d66c365934 Update dependencies 2024-02-14 22:00:48 +01:00
Matthieu 7815ecba08 Release 30 2024-02-14 21:59:49 +01:00
Matthieu b4533014b3 Update media_editor (bugfix) 2024-02-14 21:57:42 +01:00
Matthieu 905c1c2d66 Merge branch 'release29' into 'master'
New release

See merge request pixeldroid/PixelDroid!577
2024-02-10 17:52:11 +00:00
Matthieu 46ee92a19f New release 2024-02-10 18:48:40 +01:00
Matthieu ed7ff877fb Merge branch 'translations' into 'master'
Translations update from Weblate

See merge request pixeldroid/PixelDroid!576
2024-02-10 17:44:05 +00:00
Francesco Marinucci 5c3b231e16 Translated using Weblate (Italian)
Currently translated at 40.9% (9 of 22 strings)

Co-authored-by: Francesco Marinucci <francesco.marinucci@linux.it>
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/fastlane/it/
Translation: PixelDroid/Fastlane
2024-02-10 17:43:09 +00:00
Matthieu 80e021e1a2 Merge branch 'hilt' into 'master'
Migration from Dagger to Hilt

See merge request pixeldroid/PixelDroid!575
2024-02-10 17:43:07 +00:00
Matthieu 0aa3d86c11 Migration from Dagger to Hilt 2024-02-10 17:43:07 +00:00
Matthieu 05cb615f15 Merge branch 'release28' into 'master'
Release 28

See merge request pixeldroid/PixelDroid!574
2024-01-31 15:45:29 +00:00
Matthieu 9313f321cd Release 28 2024-01-31 16:45:04 +01:00
Matthieu 175438115d Merge branch 'image_editing_improvements' into 'master'
Update dependency on media_editor

See merge request pixeldroid/PixelDroid!566
2024-01-31 11:41:20 +00:00
Matthieu f2600b85e2 Update dependency on media_editor 2024-01-31 11:41:20 +00:00
Matthieu 711a5b310f Merge branch 'translations' into 'master'
Translations update from Weblate

See merge request pixeldroid/PixelDroid!573
2024-01-31 10:00:49 +00:00
Francesco Marinucci 0192190c6d Translated using Weblate (Italian)
Currently translated at 38.0% (8 of 21 strings)

Translated using Weblate (Italian)

Currently translated at 99.6% (254 of 255 strings)

Co-authored-by: Francesco Marinucci <francesco.marinucci@linux.it>
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/app/it/
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/fastlane/it/
Translation: PixelDroid/Fastlane
Translation: PixelDroid/pixeldroid
2024-01-31 02:10:13 +00:00
Matthieu bb935e73ad Merge branch 'improve_profile_layout' into 'master'
Improve/change profile and post layouts

See merge request pixeldroid/PixelDroid!572
2024-01-30 13:06:44 +00:00
Matthieu ddf1b273de Improve/change profile and post layouts 2024-01-30 13:44:07 +01:00
Matthieu 14dee5463e Merge branch 'fix_crash_cutout' into 'master'
Update dependency to fix crash

See merge request pixeldroid/PixelDroid!571
2024-01-27 14:25:35 +00:00
Matthieu 416d36b1a8 Update dependency to fix crash 2024-01-27 14:30:53 +01:00
Matthieu 370aeda4a6 Merge branch 'edit_profile_picture' into 'master'
Fix profile pic edit

Closes #377

See merge request pixeldroid/PixelDroid!569
2024-01-26 12:22:51 +00:00
Matthieu eb65e24099 Circlecrop directly without RequestOptions 2024-01-26 13:16:33 +01:00
Fred 1ea4371b3e Crop newly changed profile pic to circle 2024-01-24 23:22:58 +01:00
Matthieu 2e399884e9 Fix #377 2024-01-24 22:20:08 +01:00
Matthieu 1dcf605976 Improve consistency of ViewModel and UI 2024-01-23 17:27:45 +01:00
Fred 06478cf8a7 Increment version number 2024-01-22 21:50:04 +00:00
Fred 580f7ca911 Fix profile pic edit
See https://github.com/pixelfed/pixelfed/issues/4250
2024-01-22 21:50:04 +00:00
Matthieu 23fbebfe44 Merge branch 'upgrade_pixel_common' into 'master'
Upgrade common library to latest commit

See merge request pixeldroid/PixelDroid!568
2024-01-22 20:25:43 +00:00
Fred 6602d912a9 Upgrade common library to latest commit
- Solves issue with displaying AboutActivity
2024-01-22 21:03:25 +01:00
Matthieu 1f41268d55 Merge branch 'translations' into 'master'
Translations update from Weblate

See merge request pixeldroid/PixelDroid!567
2024-01-20 08:25:50 +00:00
Francesco Marinucci 15efdcabad Translated using Weblate (Italian)
Currently translated at 95.6% (244 of 255 strings)

Co-authored-by: Francesco Marinucci <francesco.marinucci@linux.it>
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/app/it/
Translation: PixelDroid/pixeldroid
2024-01-20 00:32:36 +00:00
Matthieu 97069a76db Merge branch 'translations' into 'master'
Translations update from Weblate

See merge request pixeldroid/PixelDroid!564
2024-01-19 09:02:11 +00:00
Francesco Marinucci 834b3f86bd Translated using Weblate (Italian)
Currently translated at 81.8% (208 of 254 strings)

Co-authored-by: Francesco Marinucci <francesco.marinucci@linux.it>
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/app/it/
Translation: PixelDroid/pixeldroid
2024-01-19 08:25:31 +00:00
ButterflyOfFire adc4ef0199 Translated using Weblate (French)
Currently translated at 72.4% (184 of 254 strings)

Co-authored-by: ButterflyOfFire <butterflyoffire+pixeldroid@protonmail.com>
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/app/fr/
Translation: PixelDroid/pixeldroid
2024-01-19 08:25:31 +00:00
Matthieu dafe827a5c Merge branch 'fix_comment_string' into 'master'
Fix comment string being both verb and noun

See merge request pixeldroid/PixelDroid!565
2024-01-19 08:25:29 +00:00
Matthieu 0f8602b3f1 Fix comment string being both verb and noun 2024-01-19 09:04:28 +01:00
Matthieu 6edb394ccb Merge branch 'translations' into 'master'
Translations update from Weblate

See merge request pixeldroid/PixelDroid!563
2024-01-12 09:11:58 +00:00
Weblate 13fa45cdb0 Added translation using Weblate (Interlingua)
Co-authored-by: Weblate <noreply@weblate.org>
2024-01-12 03:37:15 +00:00
Matthieu e108d08916 Merge branch 'fix_share_and_save' into 'master'
Fix sharing and saving images & album crash on older Android versions

Closes #369, #371, and #372

See merge request pixeldroid/PixelDroid!562
2024-01-03 15:44:38 +00:00
Matthieu 27413cd08e Fix crashes on older Android versions for save 2024-01-03 16:26:13 +01:00
Matthieu b975a255f9 Fix crash on older Android versions due to cutout 2024-01-03 12:33:10 +01:00
Matthieu 49ec982464 Remove duplicate drawable 2024-01-03 12:08:44 +01:00
Matthieu 9b1573dd8b Remove old permission from AndroidManifest 2023-12-31 13:28:45 +01:00
Matthieu c91b8a391a Fix back button AlbumActivity 2023-12-31 13:28:03 +01:00
Matthieu 7dea955261 Fix share and save of images 2023-12-31 13:27:40 +01:00
Matthieu 69a0c13d32 Remove unneeded file permission checks
This fixes sharing and saving images
2023-12-27 14:08:46 +01:00
Matthieu 5149150f27 Merge branch 'stories' into 'master'
Stories

Closes #368

See merge request pixeldroid/PixelDroid!548
2023-12-27 11:25:36 +00:00
Matthieu 2d988705d5 Release increment and update dependencies 2023-12-27 12:24:07 +01:00
Matthieu d9458f50e3 Update dependency 2023-12-27 11:48:32 +01:00
Matthieu 54eb7f20ef Actually use story settings 2023-12-27 11:47:15 +01:00
Matthieu f3a870d83e Update dependency 2023-12-25 19:55:11 +00:00
Matthieu 89bf93c0e5 Fix crash on image loading in coroutine 2023-12-25 19:55:11 +00:00
Matthieu 77996d26ba Update dependencies 2023-12-25 19:55:11 +00:00
Matthieu f61bf5b1df Improve color 2023-12-25 19:55:11 +00:00
Matthieu 860a639d42 GitLab CI fix for git submodule 2023-12-25 19:55:11 +00:00
Matthieu 1e6c3a9d5a Add TODO for possible performance optimisation of blurhash 2023-12-25 19:55:11 +00:00
Matthieu 8703287d90 Story creation integration 2023-12-25 19:55:11 +00:00
Matthieu bb3c9afb13 Add Self view for stories 2023-12-25 19:55:11 +00:00
Matthieu 0290e6f8d5 Update pixel_common 2023-12-25 19:55:11 +00:00
Matthieu 4008d2a2fc Update dependencies 2023-12-25 19:55:11 +00:00
Matthieu 3fdb7762b6 Use fancier Story progress bars 2023-12-25 19:55:11 +00:00
Matthieu f2c1ae3942 Remove duplicate values that are in pixel_common 2023-12-25 19:55:11 +00:00
Matthieu 4ac7aa6bcb Avoid leaks of bindings 2023-12-25 19:55:11 +00:00
Matthieu 59e29ef232 Clarify Story API 2023-12-25 19:55:11 +00:00
Matthieu f16f1a9927 Remove unused gps permission 2023-12-25 19:55:11 +00:00
Matthieu 6c85115b67 Stories progress 2023-12-25 19:55:11 +00:00
Matthieu f0face3fb0 Update dependencies 2023-12-25 19:55:11 +00:00
Matthieu c40c6f70dc Update dependencies 2023-12-25 19:55:11 +00:00
Matthieu 49d76ff866 Add error mascots to repo 2023-12-25 19:55:11 +00:00
Matthieu 64ffaf081e Move dimensions to library 2023-12-25 19:55:11 +00:00
Matthieu a5334fb637 Fix crash navigating up 2023-12-25 19:55:11 +00:00
Matthieu 70e49ee60c Move from onBackPressed (was deprecated) 2023-12-25 19:55:11 +00:00
Matthieu fc43bc2ff4 Update dependencies 2023-12-25 19:55:11 +00:00
Matthieu d393b083ae Move things AboutActivity and ThemedActivity to library 2023-12-25 19:55:11 +00:00
Matthieu 03e2ab8d49 Center discover tiles in layout 2023-12-25 19:55:11 +00:00
Matthieu 795b54c3cd WIP refactor top bar 2023-12-25 19:55:11 +00:00
Matthieu 8268e9e8b5 Fix encode message showing when it shouldn't 2023-12-25 19:55:11 +00:00
Matthieu eb014290ff Move to automatic generation of locales_config 2023-12-25 19:55:11 +00:00
Matthieu db3da57b7b implement story creation 2023-12-25 19:55:11 +00:00
Matthieu ae54b83ec7 Small improvement to error showing 2023-12-25 19:55:11 +00:00
Matthieu dda06b1cd5 Stories progress 2023-12-25 19:55:11 +00:00
Matthieu 73193abd95 More progress on stories :) 2023-12-25 19:55:11 +00:00
Matthieu f60889ea14 More work on stories 2023-12-25 19:55:11 +00:00
Matthieu 888c6328d9 Super rudimentary support for stories 2023-12-25 19:55:11 +00:00
Matthieu 6876b1c449 Merge branch 'translations' into 'master'
Translations update from Weblate

See merge request pixeldroid/PixelDroid!561
2023-12-21 13:08:56 +00:00
mittwerk a4a2505adb Translated using Weblate (Russian)
Currently translated at 100.0% (241 of 241 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (241 of 241 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (241 of 241 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (241 of 241 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (241 of 241 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (241 of 241 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (241 of 241 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (19 of 19 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (241 of 241 strings)

Translated using Weblate (Russian)

Currently translated at 98.7% (238 of 241 strings)

Co-authored-by: mittwerk <mittwerk3@gmail.com>
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/app/ru/
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/fastlane/ru/
Translation: PixelDroid/Fastlane
Translation: PixelDroid/pixeldroid
2023-12-15 14:32:36 +00:00
Weblate Admin d48e63c65e Translated using Weblate (Dutch)
Currently translated at 48.5% (117 of 241 strings)

Translated using Weblate (Dutch)

Currently translated at 48.1% (116 of 241 strings)

Co-authored-by: Weblate Admin <contact@pixeldroid.org>
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/app/nl/
Translation: PixelDroid/pixeldroid
2023-12-15 14:32:36 +00:00
Matthieu 1956469b7d Merge branch 'translations' into 'master'
Translated using Weblate (Italian)

See merge request pixeldroid/PixelDroid!560
2023-10-04 10:36:26 +00:00
Alex Camilleri 4817188a5b Translated using Weblate (Italian)
Currently translated at 66.3% (160 of 241 strings)

Co-authored-by: Alex Camilleri <camilleri.alex@gmail.com>
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/app/it/
Translation: PixelDroid/pixeldroid
2023-10-03 21:34:58 +00:00
Matthieu 2d61ff855a Merge branch 'translations' into 'master'
Translated using Weblate (Polish)

See merge request pixeldroid/PixelDroid!559
2023-09-14 16:00:54 +00:00
Grzegorz Cichocki aad904dd07 Translated using Weblate (Polish)
Currently translated at 99.1% (239 of 241 strings)

Co-authored-by: Grzegorz Cichocki <grzegorz.cichocki@pollub.edu.pl>
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/app/pl/
Translation: PixelDroid/pixeldroid
2023-09-09 21:05:48 +00:00
Fred f15c23cceb Prepare code for click listener in AlbumActivity 2023-07-30 13:45:33 +02:00
Fred d9da842df7 Create ViewModel for AlbumActivity 2023-07-18 18:22:30 +02:00
Matthieu 88cd015325 Merge branch 'translations' into 'master'
Translated using Weblate (Finnish)

See merge request pixeldroid/PixelDroid!557
2023-06-30 06:38:50 +00:00
Petri Salmela 764d610898 Translated using Weblate (Finnish)
Currently translated at 26.5% (64 of 241 strings)

Co-authored-by: Petri Salmela <pesasa@iki.fi>
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/app/fi/
Translation: PixelDroid/pixeldroid
2023-06-30 03:06:57 +00:00
Matthieu d8adbbfae4 Merge branch 'translations' into 'master'
Translated using Weblate (Persian)

See merge request pixeldroid/PixelDroid!556
2023-06-26 12:19:18 +00:00
Mostafa Ahangarha 0d449551d7 Translated using Weblate (Persian)
Currently translated at 100.0% (241 of 241 strings)

Co-authored-by: Mostafa Ahangarha <ahangarha@riseup.net>
Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/app/fa/
Translation: PixelDroid/pixeldroid
2023-06-26 12:06:07 +00:00
Matthieu 8f7cedb508 Merge branch 'artectrex-master-patch-91572' into 'master'
Update .gitlab-ci.yml

See merge request pixeldroid/PixelDroid!555
2023-06-22 12:47:01 +00:00
Matthieu 21c68fdfae Update .gitlab-ci.yml 2023-06-22 12:46:55 +00:00
228 changed files with 15076 additions and 10415 deletions

View File

@ -2,6 +2,7 @@ image: registry.gitlab.com/fdroid/fdroidserver:buildserver-bullseye
variables: variables:
GIT_SUBMODULE_STRATEGY: recursive GIT_SUBMODULE_STRATEGY: recursive
GIT_SUBMODULE_FORCE_HTTPS: "true"
before_script: before_script:
- export GRADLE_USER_HOME=`pwd`/.gradle - export GRADLE_USER_HOME=`pwd`/.gradle
@ -19,7 +20,7 @@ before_script:
- test -e $cmdline_tools_latest && export PATH="$cmdline_tools_latest:$PATH" - test -e $cmdline_tools_latest && export PATH="$cmdline_tools_latest:$PATH"
- export GRADLE_USER_HOME=$PWD/.gradle - export GRADLE_USER_HOME=$PWD/.gradle
- export ANDROID_COMPILE_SDK=`sed -n 's,.*compileSdkVersion\s*\([0-9][0-9]*\).*,\1,p' app/build.gradle` - export ANDROID_COMPILE_SDK=`sed -n 's,.*compileSdk\s*\([0-9][0-9]*\).*,\1,p' app/build.gradle`
- echo y | sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" > /dev/null - echo y | sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" > /dev/null
- apt-get update || apt-get update - apt-get update || apt-get update
@ -130,6 +131,7 @@ fdroid build:
# each `fdroid build --on-server` run expects sudo, then uninstalls it # each `fdroid build --on-server` run expects sudo, then uninstalls it
- set -x - set -x
- apt-get install sudo - apt-get install sudo
- fdroid fetchsrclibs --verbose
# this builds the latest version of the app from its source dir, using the build recipe in .fdroid.yml # this builds the latest version of the app from its source dir, using the build recipe in .fdroid.yml
- fdroid build --verbose --on-server --no-tarball - fdroid build --verbose --on-server --no-tarball
# create a keystore if we dont have one # create a keystore if we dont have one

3
.gitmodules vendored
View File

@ -1,3 +1,6 @@
[submodule "scrambler"] [submodule "scrambler"]
path = scrambler path = scrambler
url = https://gitlab.com/artectrex/scrambler.git url = https://gitlab.com/artectrex/scrambler.git
[submodule "pixel_common"]
path = pixel_common
url = https://gitlab.shinice.net/pixeldroid/pixel_common.git

View File

@ -9,13 +9,16 @@ Free (as in freedom) Android client for Pixelfed, the federated image sharing pl
<img src="https://pixeldroid.org/badge-fdroid.png" alt="Get it on F-Droid" width="206"> <img src="https://pixeldroid.org/badge-fdroid.png" alt="Get it on F-Droid" width="206">
</a> </a>
Come talk to us on Matrix, at <a href="https://matrix.to/#/#pixeldroid:gnugen.ch">#pixeldroid:gnugen.ch</a> !
## 🔧 Compiling the code yourself ## 🔧 Compiling the code yourself
If you want to try out PixelDroid on your own device, you can compile the source code yourself. To do that you can install [Android Studio](https://developer.android.com/studio/). If you want to try out PixelDroid on your own device, you can compile the source code yourself. To do that you can install [Android Studio](https://developer.android.com/studio/).
## 🎨 Art attribution ## 🎨 Art attribution
Our mascot was commissioned using funds from NLnet. The original file is `pixeldroid_mascot.svg` and it is adapted to work as an Android Drawable. This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/) (CC BY-SA). Our mascot was commissioned using funds from NLnet. The original file is `pixeldroid_mascot.svg` and it is adapted to work as an Android Drawable. This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/) (CC BY-SA).
Various works have been used from the pixelfed branding repository ( https://github.com/pixelfed/brand-assets ). In addition, a drawing of a red panda is used for some error messages ( https://thenounproject.com/search/?q=red+panda&i=2877785 ). Various works have been used from the pixelfed branding repository ( https://github.com/pixelfed/brand-assets ).
## 🤝 Contribute ## 🤝 Contribute
If you want to contribute, you can check out [CONTRIBUTING.md](CONTRIBUTING.md) and/or [TRANSLATION.md](TRANSLATION.md) If you want to contribute, you can check out [CONTRIBUTING.md](CONTRIBUTING.md) and/or [TRANSLATION.md](TRANSLATION.md)

View File

@ -1,41 +1,52 @@
import com.android.build.api.dsl.ManagedVirtualDevice import com.android.build.api.dsl.ManagedVirtualDevice
plugins { plugins {
id "com.mikepenz.aboutlibraries.plugin" version "10.5.2" id("com.android.application")
id("com.google.dagger.hilt.android")
id("kotlin-android")
id("jacoco")
id("kotlin-parcelize")
id("com.google.devtools.ksp")
} }
// Map for the version code that gives each ABI a value.
ext.abiCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, x86: 3, x86_64: 4]
apply plugin: 'com.android.application' //Different version codes per architecture (for F-Droid support)
apply plugin: 'kotlin-android' android.applicationVariants.configureEach { variant ->
apply plugin: 'com.mikepenz.aboutlibraries.plugin' variant.outputs.each { output ->
apply plugin: 'kotlin-kapt' def baseAbiVersionCode = project.ext.abiCodes.get(output.getFilter(com.android.build.OutputFile.ABI))
apply plugin: 'jacoco' if (baseAbiVersionCode != null) {
apply plugin: "kotlin-parcelize" output.versionCodeOverride = (100 * project.android.defaultConfig.versionCode) + baseAbiVersionCode
} else {
output.versionCodeOverride = 100 * project.android.defaultConfig.versionCode
}
// Force latest version of Jacoco, initially done to resolve https://github.com/jacoco/jacoco/issues/1155 }
jacoco.toolVersion = "0.8.7"
}
android { android {
namespace 'org.pixeldroid.app' namespace 'org.pixeldroid.app'
compileSdkVersion 33 compileSdk 34
buildToolsVersion '33.0.0'
compileOptions { compileOptions {
coreLibraryDesugaringEnabled true coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
} }
androidResources {
generateLocaleConfig true
}
kotlin {
jvmToolchain(17)
}
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
freeCompilerArgs += ["-opt-in=kotlin.RequiresOptIn"] freeCompilerArgs += ["-opt-in=kotlin.RequiresOptIn"]
} }
defaultConfig { defaultConfig {
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 33 targetSdkVersion 34
versionCode 24 versionCode 33
versionName "1.0.beta" + versionCode versionName "1.0.beta" + versionCode
//TODO add resConfigs("en", "fr", "ja",...) ? //TODO add resConfigs("en", "fr", "ja",...) ?
@ -84,11 +95,36 @@ android {
proguardFiles 'proguard-rules.pro' proguardFiles 'proguard-rules.pro'
} }
} }
splits {
// Configures multiple APKs based on ABI.
abi {
// Enables building multiple APKs per ABI.
enable true
// By default all ABIs are included, so use reset() and include to specify that we only
// want APKs for "x86", "x86_64", "arm64-v8a" and "armeabi-v7a".
// Resets the list of ABIs for Gradle to create APKs for to none.
reset()
// Specifies a list of ABIs for Gradle to create APKs for.
//noinspection ChromeOsAbiSupport
include project.ext.abiCodes.keySet() as String[]
// Specifies that we don't want to also generate a universal APK that includes all ABIs.
universalApk false
}
}
/** /**
* Make a string with the application_id (available in xml etc) * Make a string with the application_id (available in xml etc)
*/ */
android.applicationVariants.all { variant -> android.applicationVariants.configureEach { variant ->
variant.resValue 'string', 'application_id', variant.applicationId variant.resValue 'string', 'application_id', variant.applicationId
variant.resValue "string", "versionName", variant.versionName
} }
testOptions { testOptions {
@ -113,11 +149,9 @@ android {
} }
buildFeatures { buildFeatures {
viewBinding true viewBinding true
dataBinding = true
buildConfig = true buildConfig = true
} }
apply plugin: 'kotlin-kapt'
lint { lint {
//We can't expect translators to always keep up immediately: //We can't expect translators to always keep up immediately:
// don't fail if a a string is untranslated // don't fail if a a string is untranslated
@ -131,40 +165,40 @@ android {
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
/** /**
* AndroidX dependencies: * AndroidX dependencies:
*/ */
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-splashscreen:1.0.0' implementation 'androidx.core:core-splashscreen:1.0.1'
implementation 'androidx.core:core-ktx:1.9.0' implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3' implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.3' implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
implementation "androidx.browser:browser:1.5.0" implementation "androidx.browser:browser:1.8.0"
implementation 'androidx.recyclerview:recyclerview:1.3.0' implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3' implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.3' implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
implementation 'androidx.paging:paging-runtime-ktx:3.1.1' implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.6.1' implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.7.0'
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.1" implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
implementation "androidx.lifecycle:lifecycle-common-java8:2.6.1" implementation "androidx.lifecycle:lifecycle-common-java8:2.7.0"
implementation "androidx.annotation:annotation:1.6.0" implementation "androidx.annotation:annotation:1.7.1"
implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation "androidx.activity:activity-ktx:1.7.0" implementation "androidx.activity:activity-ktx:1.8.2"
implementation 'androidx.fragment:fragment-ktx:1.5.6' implementation 'androidx.fragment:fragment-ktx:1.6.2'
implementation 'androidx.work:work-runtime-ktx:2.8.1' implementation 'androidx.work:work-runtime-ktx:2.9.0'
implementation 'androidx.media2:media2-widget:1.2.1' implementation 'androidx.media2:media2-widget:1.3.0'
implementation 'androidx.media2:media2-player:1.2.1' implementation 'androidx.media2:media2-player:1.3.0'
// Use the most recent version of CameraX // Use the most recent version of CameraX
def cameraX_version = '1.2.2' def cameraX_version = '1.3.2'
implementation "androidx.camera:camera-core:$cameraX_version" implementation "androidx.camera:camera-core:$cameraX_version"
implementation "androidx.camera:camera-camera2:$cameraX_version" implementation "androidx.camera:camera-camera2:$cameraX_version"
// CameraX Lifecycle library // CameraX Lifecycle library
@ -173,9 +207,9 @@ dependencies {
// CameraX View class // CameraX View class
implementation "androidx.camera:camera-view:$cameraX_version" implementation "androidx.camera:camera-view:$cameraX_version"
def room_version = "2.5.1" def room_version = "2.6.1"
implementation "androidx.room:room-runtime:$room_version" implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version" ksp "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version" implementation "androidx.room:room-ktx:$room_version"
implementation "androidx.room:room-paging:$room_version" implementation "androidx.room:room-paging:$room_version"
@ -186,61 +220,56 @@ dependencies {
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
implementation 'com.google.android.material:material:1.8.0' implementation 'com.google.android.material:material:1.11.0'
//Dagger (dependency injection) //Dagger (dependency injection)
implementation 'com.google.dagger:dagger-android:2.45' implementation 'com.google.dagger:dagger:2.51'
implementation 'com.google.dagger:dagger-android-support:2.44' ksp 'com.google.dagger:dagger-compiler:2.51'
// if you use the support libraries
kapt 'com.google.dagger:dagger-android-processor:2.44'
kapt 'com.google.dagger:dagger-compiler:2.44'
implementation 'com.squareup.okhttp3:okhttp:4.9.3' implementation('com.google.dagger:hilt-android:2.51')
implementation 'com.squareup.retrofit2:retrofit:2.9.0' ksp 'com.google.dagger:hilt-compiler:2.51'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.retrofit2:adapter-rxjava3:2.9.0' implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'io.reactivex.rxjava3:rxjava:3.1.6' implementation 'com.squareup.retrofit2:retrofit:2.10.0'
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0' implementation 'com.squareup.retrofit2:converter-gson:2.10.0'
implementation 'com.squareup.retrofit2:adapter-rxjava3:2.10.0'
implementation 'io.reactivex.rxjava3:rxjava:3.1.8'
implementation 'io.reactivex.rxjava3:rxandroid:3.0.2'
implementation 'com.github.connyduck:sparkbutton:4.1.0' implementation 'com.github.connyduck:sparkbutton:4.1.0'
implementation 'org.pixeldroid.pixeldroid:android-media-editor:2.0'
implementation 'org.pixeldroid.pixeldroid:android-media-editor:1.4'
implementation project(path: ':scrambler') implementation project(path: ':scrambler')
implementation project(path: ':pixel_common')
implementation('com.github.bumptech.glide:glide:4.14.2') { implementation('com.github.bumptech.glide:glide:4.16.0') {
exclude group: "com.android.support" exclude group: "com.android.support"
} }
implementation 'com.github.bumptech.glide:okhttp3-integration:4.14.2' implementation 'com.github.bumptech.glide:okhttp3-integration:4.16.0'
implementation('com.github.bumptech.glide:recyclerview-integration:4.14.2') { implementation('com.github.bumptech.glide:recyclerview-integration:4.16.0') {
// Excludes the support library because it's already included by Glide. // Excludes the support library because it's already included by Glide.
transitive = false transitive = false
} }
implementation 'com.github.bumptech.glide:annotations:4.14.2' implementation 'com.github.bumptech.glide:annotations:4.16.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.14.2' annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
kapt 'com.github.bumptech.glide:compiler:4.14.2' ksp 'com.github.bumptech.glide:ksp:4.16.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'com.mikepenz:materialdrawer:9.0.1' implementation 'com.mikepenz:materialdrawer:9.0.2'
// Add for NavController support // Add for NavController support
implementation 'com.mikepenz:materialdrawer-nav:9.0.1' implementation 'com.mikepenz:materialdrawer-nav:9.0.2'
//iconics //iconics
implementation 'com.mikepenz:iconics-core:5.4.0' implementation 'com.mikepenz:iconics-core:5.4.0'
implementation 'com.mikepenz:materialdrawer-iconics:9.0.1' implementation 'com.mikepenz:materialdrawer-iconics:9.0.2'
implementation 'com.mikepenz:iconics-views:5.4.0' implementation 'com.mikepenz:iconics-views:5.4.0'
implementation 'com.mikepenz:google-material-typeface:4.0.0.2-kotlin@aar' implementation 'com.mikepenz:google-material-typeface:4.0.0.2-kotlin@aar'
implementation 'com.karumi:dexter:6.2.3'
implementation 'com.github.ligi:tracedroid:4.1' implementation 'com.github.ligi:tracedroid:4.1'
implementation 'me.relex:circleindicator:2.1.6' implementation 'me.relex:circleindicator:2.1.6'
implementation 'com.mikepenz:aboutlibraries-core:10.6.0'
/** /**
* Not in release, so not mentioned in licenses list * Not in release, so not mentioned in licenses list
*/ */
@ -251,14 +280,14 @@ dependencies {
androidTestImplementation 'com.linkedin.testbutler:test-butler-library:2.2.1' androidTestImplementation 'com.linkedin.testbutler:test-butler-library:2.2.1'
androidTestUtil 'com.linkedin.testbutler:test-butler-app:2.2.1' androidTestUtil 'com.linkedin.testbutler:test-butler-app:2.2.1'
androidTestImplementation 'androidx.work:work-testing:2.8.1' androidTestImplementation 'androidx.work:work-testing:2.9.0'
testImplementation 'com.github.tomakehurst:wiremock-jre8:2.34.0' testImplementation 'com.github.tomakehurst:wiremock-jre8:2.34.0'
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
testImplementation "androidx.room:room-testing:$room_version" testImplementation "androidx.room:room-testing:$room_version"
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.3.0'
androidTestImplementation 'androidx.test:runner:1.5.2' androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test:rules:1.5.0' androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.ext:junit:1.1.5'
@ -267,11 +296,13 @@ dependencies {
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1' androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1'
androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.2'
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.9.2' androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.12.0'
} }
tasks.withType(Test) {
tasks.withType(Test).configureEach {
jacoco.includeNoLocationClasses = true jacoco.includeNoLocationClasses = true
jacoco.excludes = ['jdk.internal.*'] jacoco.excludes = ['jdk.internal.*']
} }

View File

@ -8,7 +8,7 @@
<intent <intent
android:action="android.intent.action.VIEW" android:action="android.intent.action.VIEW"
android:targetPackage="org.pixeldroid.app.debug" android:targetPackage="org.pixeldroid.app.debug"
android:targetClass="org.pixeldroid.app.postCreation.camera.CameraActivityShortcut" /> android:targetClass="org.pixeldroid.app.postCreation.camera.CameraActivity" />
<categories android:name="android.shortcut.conversation" /> <categories android:name="android.shortcut.conversation" />
<capability-binding android:key="actions.intent.CREATE_MESSAGE" /> <capability-binding android:key="actions.intent.CREATE_MESSAGE" />
</shortcut> </shortcut>

View File

@ -5,14 +5,13 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
<uses-feature <uses-feature
android:name="android.hardware.camera.any" android:name="android.hardware.camera.any"
android:required="false" /> android:required="false" />
<uses-feature android:name="android.hardware.location.gps" />
<uses-feature <uses-feature
android:name="android.hardware.camera" android:name="android.hardware.camera"
android:required="false" /> android:required="false" />
@ -26,7 +25,6 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:localeConfig="@xml/locales_config"
android:theme="@style/BaseAppTheme"> android:theme="@style/BaseAppTheme">
<service <service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService" android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
@ -40,35 +38,32 @@
<activity <activity
android:name=".posts.AlbumActivity" android:name=".posts.AlbumActivity"
android:exported="false" android:exported="false"
android:theme="@style/AppTheme.ActionBar.Transparent"/> android:theme="@style/TransparentAlbumActivity"/>
<activity <activity
android:name=".profile.EditProfileActivity" android:name=".profile.EditProfileActivity"
android:exported="false"/> android:exported="false"
android:theme="@style/BaseAppTheme" />
<activity <activity
android:name=".posts.MediaViewerActivity" android:name=".posts.MediaViewerActivity"
android:configChanges="keyboardHidden|orientation|screenSize" android:configChanges="keyboardHidden|orientation|screenSize"
android:exported="false" android:exported="false"
android:theme="@style/BaseAppTheme.NoActionBar" /> android:theme="@style/BaseAppTheme" />
<activity android:name=".postCreation.camera.CameraActivity"/> <activity android:name=".postCreation.camera.CameraActivity"
<activity android:name=".postCreation.camera.CameraActivityShortcut" android:theme="@style/BaseAppTheme"/>
android:exported = "true"
android:parentActivityName=".MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity" />
<intent-filter>
<action android:name="android.intent.action.VIEW" />
</intent-filter>
</activity>
<activity <activity
android:name=".posts.ReportActivity" android:name=".posts.ReportActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
android:theme="@style/BaseAppTheme"
tools:ignore="LockedOrientationActivity" /> tools:ignore="LockedOrientationActivity" />
<activity
android:name=".stories.StoriesActivity" />
<activity <activity
android:name=".postCreation.PostCreationActivity" android:name=".postCreation.PostCreationActivity"
android:exported="true" android:exported="true"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize"
android:theme="@style/BaseAppTheme.NoActionBar"> android:theme="@style/BaseAppTheme">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" /> <action android:name="android.intent.action.SEND_MULTIPLE" />
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
@ -81,27 +76,32 @@
</activity> </activity>
<activity <activity
android:name=".profile.FollowsActivity" android:name=".profile.FollowsActivity"
android:theme="@style/BaseAppTheme"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
tools:ignore="LockedOrientationActivity" /> tools:ignore="LockedOrientationActivity" />
<activity <activity
android:name=".posts.feeds.uncachedFeeds.hashtags.HashTagActivity" android:name=".posts.feeds.uncachedFeeds.hashtags.HashTagActivity"
android:theme="@style/BaseAppTheme"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
tools:ignore="LockedOrientationActivity" /> tools:ignore="LockedOrientationActivity" />
<activity <activity
android:name=".posts.PostActivity" android:name=".posts.PostActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
tools:ignore="LockedOrientationActivity" /> tools:ignore="LockedOrientationActivity"
android:theme="@style/BaseAppTheme" />
<activity <activity
android:name=".profile.ProfileActivity" android:name=".profile.ProfileActivity"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
tools:ignore="LockedOrientationActivity" /> tools:ignore="LockedOrientationActivity"
<activity android:name=".profile.CollectionActivity"/> android:theme="@style/BaseAppTheme"/>
<activity android:name=".profile.CollectionActivity"
android:theme="@style/BaseAppTheme"/>
<activity <activity
android:name=".settings.SettingsActivity" android:name=".settings.SettingsActivity"
android:label="@string/title_activity_settings2" android:label="@string/title_activity_settings2"
android:parentActivityName=".MainActivity" android:parentActivityName=".MainActivity"
android:screenOrientation="sensorPortrait" android:theme="@style/BaseAppTheme" />
tools:ignore="LockedOrientationActivity" />
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
@ -125,7 +125,7 @@
android:name=".LoginActivity" android:name=".LoginActivity"
android:exported="true" android:exported="true"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
android:theme="@style/BaseAppTheme.NoActionBar" android:theme="@style/BaseAppTheme"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize"
tools:ignore="LockedOrientationActivity"> tools:ignore="LockedOrientationActivity">
<intent-filter> <intent-filter>
@ -142,6 +142,7 @@
<activity <activity
android:name=".searchDiscover.SearchActivity" android:name=".searchDiscover.SearchActivity"
android:exported="true" android:exported="true"
android:theme="@style/BaseAppTheme"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
tools:ignore="LockedOrientationActivity"> tools:ignore="LockedOrientationActivity">
@ -153,17 +154,8 @@
android:name="android.app.searchable" android:name="android.app.searchable"
android:resource="@xml/searchable" /> android:resource="@xml/searchable" />
</activity> </activity>
<activity android:name=".searchDiscover.TrendingActivity"/> <activity android:name=".searchDiscover.TrendingActivity"
<activity android:theme="@style/BaseAppTheme" />
android:name=".settings.AboutActivity"
android:parentActivityName=".settings.SettingsActivity"
android:screenOrientation="sensorPortrait"
tools:ignore="LockedOrientationActivity" />
<activity
android:name=".settings.LicenseActivity"
android:parentActivityName=".settings.AboutActivity"
android:screenOrientation="sensorPortrait"
tools:ignore="LockedOrientationActivity" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"

View File

@ -1,6 +1,5 @@
package org.pixeldroid.app package org.pixeldroid.app
import android.app.AlertDialog
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
@ -16,7 +15,7 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.pixeldroid.app.databinding.ActivityLoginBinding import org.pixeldroid.app.databinding.ActivityLoginBinding
import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.PixelfedAPI import org.pixeldroid.app.utils.api.PixelfedAPI
import org.pixeldroid.app.utils.api.objects.Application import org.pixeldroid.app.utils.api.objects.Application
import org.pixeldroid.app.utils.api.objects.Instance import org.pixeldroid.app.utils.api.objects.Instance
@ -45,7 +44,7 @@ since they do not depend on each other)
*/ */
class LoginActivity : BaseThemedWithoutBarActivity() { class LoginActivity : BaseActivity() {
companion object { companion object {
private const val PACKAGE_ID = BuildConfig.APPLICATION_ID private const val PACKAGE_ID = BuildConfig.APPLICATION_ID

View File

@ -12,7 +12,9 @@ import android.util.Log
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -28,6 +30,7 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColors
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
@ -35,7 +38,12 @@ import com.mikepenz.materialdrawer.iconics.iconicsIcon
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
import com.mikepenz.materialdrawer.model.ProfileDrawerItem import com.mikepenz.materialdrawer.model.ProfileDrawerItem
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem
import com.mikepenz.materialdrawer.model.interfaces.* import com.mikepenz.materialdrawer.model.interfaces.IProfile
import com.mikepenz.materialdrawer.model.interfaces.descriptionRes
import com.mikepenz.materialdrawer.model.interfaces.descriptionText
import com.mikepenz.materialdrawer.model.interfaces.iconUrl
import com.mikepenz.materialdrawer.model.interfaces.nameRes
import com.mikepenz.materialdrawer.model.interfaces.nameText
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
import com.mikepenz.materialdrawer.util.DrawerImageLoader import com.mikepenz.materialdrawer.util.DrawerImageLoader
import com.mikepenz.materialdrawer.widget.AccountHeaderView import com.mikepenz.materialdrawer.widget.AccountHeaderView
@ -50,12 +58,12 @@ import org.pixeldroid.app.posts.feeds.cachedFeeds.postFeeds.PostFeedFragment
import org.pixeldroid.app.profile.ProfileActivity import org.pixeldroid.app.profile.ProfileActivity
import org.pixeldroid.app.searchDiscover.SearchDiscoverFragment import org.pixeldroid.app.searchDiscover.SearchDiscoverFragment
import org.pixeldroid.app.settings.SettingsActivity import org.pixeldroid.app.settings.SettingsActivity
import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.objects.Notification import org.pixeldroid.app.utils.api.objects.Notification
import org.pixeldroid.app.utils.db.addUser
import org.pixeldroid.app.utils.db.entities.HomeStatusDatabaseEntity import org.pixeldroid.app.utils.db.entities.HomeStatusDatabaseEntity
import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import org.pixeldroid.app.utils.db.updateUserInfoDb
import org.pixeldroid.app.utils.hasInternet import org.pixeldroid.app.utils.hasInternet
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.INSTANCE_NOTIFICATION_TAG import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.INSTANCE_NOTIFICATION_TAG
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.SHOW_NOTIFICATION_TAG import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker.Companion.SHOW_NOTIFICATION_TAG
@ -65,11 +73,13 @@ import org.pixeldroid.app.utils.notificationsWorker.removeNotificationChannelsFr
import java.time.Instant import java.time.Instant
class MainActivity : BaseThemedWithoutBarActivity() { class MainActivity : BaseActivity() {
private lateinit var header: AccountHeaderView private lateinit var header: AccountHeaderView
private var user: UserDatabaseEntity? = null private var user: UserDatabaseEntity? = null
private val model: MainActivityViewModel by viewModels()
companion object { companion object {
const val ADD_ACCOUNT_IDENTIFIER: Long = -13 const val ADD_ACCOUNT_IDENTIFIER: Long = -13
} }
@ -195,6 +205,7 @@ class MainActivity : BaseThemedWithoutBarActivity() {
Glide.with(this@MainActivity) Glide.with(this@MainActivity)
.load(uri) .load(uri)
.placeholder(placeholder) .placeholder(placeholder)
.circleCrop()
.into(imageView) .into(imageView)
} }
@ -229,7 +240,8 @@ class MainActivity : BaseThemedWithoutBarActivity() {
primaryDrawerItem { primaryDrawerItem {
nameRes = R.string.logout nameRes = R.string.logout
iconicsIcon = GoogleMaterial.Icon.gmd_close iconicsIcon = GoogleMaterial.Icon.gmd_close
}) },
)
binding.drawer.onDrawerItemClickListener = { v, drawerItem, position -> binding.drawer.onDrawerItemClickListener = { v, drawerItem, position ->
when (position){ when (position){
1 -> launchActivity(ProfileActivity()) 1 -> launchActivity(ProfileActivity())
@ -238,6 +250,18 @@ class MainActivity : BaseThemedWithoutBarActivity() {
} }
false false
} }
// Closes the drawer if it is open, when we press the back button
onBackPressedDispatcher.addCallback(this) {
// Handle the back button event
if(binding.drawerLayout.isDrawerOpen(GravityCompat.START)){
binding.drawerLayout.closeDrawer(GravityCompat.START)
}
else {
this.isEnabled = false
super.onBackPressedDispatcher.onBackPressed()
}
}
} }
private fun logOut(){ private fun logOut(){
@ -250,13 +274,13 @@ class MainActivity : BaseThemedWithoutBarActivity() {
val remainingUsers = db.userDao().getAll() val remainingUsers = db.userDao().getAll()
if (remainingUsers.isEmpty()){ if (remainingUsers.isEmpty()){
//no more users, start first-time login flow // No more users, start first-time login flow
launchActivity(LoginActivity(), firstTime = true) launchActivity(LoginActivity(), firstTime = true)
} else { } else {
val newActive = remainingUsers.first() val newActive = remainingUsers.first()
db.userDao().activateUser(newActive.user_id, newActive.instance_uri) db.userDao().activateUser(newActive.user_id, newActive.instance_uri)
apiHolder.setToCurrentUser() apiHolder.setToCurrentUser()
//relaunch the app // Relaunch the app
launchActivity(MainActivity(), firstTime = true) launchActivity(MainActivity(), firstTime = true)
} }
} }
@ -267,16 +291,12 @@ class MainActivity : BaseThemedWithoutBarActivity() {
lifecycleScope.launchWhenCreated { lifecycleScope.launchWhenCreated {
try { try {
val domain = user?.instance_uri.orEmpty()
val accessToken = user?.accessToken.orEmpty()
val refreshToken = user?.refreshToken
val clientId = user?.clientId.orEmpty()
val clientSecret = user?.clientSecret.orEmpty()
val api = apiHolder.api ?: apiHolder.setToCurrentUser() val api = apiHolder.api ?: apiHolder.setToCurrentUser()
val account = api.verifyCredentials() val account = api.verifyCredentials()
addUser(db, account, domain, accessToken = accessToken, refreshToken = refreshToken, clientId = clientId, clientSecret = clientSecret) updateUserInfoDb(db, account)
fillDrawerAccountInfo(account.id!!)
//No need to update drawer account info here, the ViewModel listens to db updates
} catch (exception: Exception) { } catch (exception: Exception) {
Log.e("ACCOUNT UPDATE:", exception.toString()) Log.e("ACCOUNT UPDATE:", exception.toString())
} }
@ -308,9 +328,11 @@ class MainActivity : BaseThemedWithoutBarActivity() {
} }
private fun switchUser(userId: String, instance_uri: String) { private fun switchUser(userId: String, instance_uri: String) {
db.userDao().deActivateActiveUsers() db.runInTransaction{
db.userDao().activateUser(userId, instance_uri) db.userDao().deActivateActiveUsers()
apiHolder.setToCurrentUser() db.userDao().activateUser(userId, instance_uri)
apiHolder.setToCurrentUser()
}
} }
private inline fun primaryDrawerItem(block: PrimaryDrawerItem.() -> Unit): PrimaryDrawerItem { private inline fun primaryDrawerItem(block: PrimaryDrawerItem.() -> Unit): PrimaryDrawerItem {
@ -323,35 +345,41 @@ class MainActivity : BaseThemedWithoutBarActivity() {
} }
private fun fillDrawerAccountInfo(account: String) { private fun fillDrawerAccountInfo(account: String) {
val users = db.userDao().getAll().toMutableList() lifecycleScope.launch {
users.sortWith { l, r -> lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
when { model.users.collect { list ->
l.isActive && !r.isActive -> -1 val users = list.toMutableList()
r.isActive && !l.isActive -> 1 users.sortWith { l, r ->
else -> 0 when {
l.isActive && !r.isActive -> -1
r.isActive && !l.isActive -> 1
else -> 0
}
}
val profiles: MutableList<IProfile> = users.map { user ->
ProfileDrawerItem().apply {
isSelected = user.isActive
nameText = user.display_name
iconUrl = user.avatar_static
isNameShown = true
identifier = user.user_id.toLong()
descriptionText = user.fullHandle
tag = user.instance_uri
}
}.toMutableList()
// reuse the already existing "add account" item
header.profiles.orEmpty()
.filter { it.identifier == ADD_ACCOUNT_IDENTIFIER }
.take(1)
.forEach { profiles.add(it) }
header.clear()
header.profiles = profiles
header.setActiveProfile(account.toLong())
}
} }
} }
val profiles: MutableList<IProfile> = users.map { user ->
ProfileDrawerItem().apply {
isSelected = user.isActive
nameText = user.display_name
iconUrl = user.avatar_static
isNameShown = true
identifier = user.user_id.toLong()
descriptionText = user.fullHandle
tag = user.instance_uri
}
}.toMutableList()
// reuse the already existing "add account" item
header.profiles.orEmpty()
.filter { it.identifier == ADD_ACCOUNT_IDENTIFIER }
.take(1)
.forEach { profiles.add(it) }
header.clear()
header.profiles = profiles
header.setActiveProfile(account.toLong())
} }
/** /**
@ -480,16 +508,4 @@ class MainActivity : BaseThemedWithoutBarActivity() {
} }
startActivity(intent) startActivity(intent)
} }
/**
* Closes the drawer if it is open, when we press the back button
*/
override fun onBackPressed() {
if(binding.drawerLayout.isDrawerOpen(GravityCompat.START)){
binding.drawerLayout.closeDrawer(GravityCompat.START)
} else {
super.onBackPressed()
}
}
} }

View File

@ -0,0 +1,40 @@
package org.pixeldroid.app
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import javax.inject.Inject
@HiltViewModel
class MainActivityViewModel @Inject constructor(
private val db: AppDatabase
): ViewModel() {
// Mutable state flow that will be used internally in the ViewModel, empty list is given as initial value.
private val _users = MutableStateFlow(emptyList<UserDatabaseEntity>())
// Immutable state flow exposed to UI
val users = _users.asStateFlow()
init {
getUsers()
}
private fun getUsers() {
viewModelScope.launch {
db.userDao().getAllFlow().flowOn(Dispatchers.IO)
.collect { users: List<UserDatabaseEntity> ->
_users.update { users }
}
}
}
}

View File

@ -1,43 +1,57 @@
package org.pixeldroid.app.postCreation package org.pixeldroid.app.postCreation
import android.os.* import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityPostCreationBinding import org.pixeldroid.app.databinding.ActivityPostCreationBinding
import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
const val TAG = "Post Creation Activity" class PostCreationActivity : BaseActivity() {
class PostCreationActivity : BaseThemedWithoutBarActivity() {
companion object { companion object {
internal const val PICTURE_DESCRIPTION = "picture_description" internal const val POST_DESCRIPTION = "post_description"
internal const val PICTURE_DESCRIPTIONS = "picture_descriptions"
internal const val POST_REDRAFT = "post_redraft" internal const val POST_REDRAFT = "post_redraft"
internal const val POST_NSFW = "post_nsfw" internal const val POST_NSFW = "post_nsfw"
internal const val TEMP_FILES = "temp_files" internal const val TEMP_FILES = "temp_files"
fun intentForUris(context: Context, uris: List<Uri>) =
Intent(Intent.ACTION_SEND_MULTIPLE).apply {
// Pass downloaded images to new post creation activity
putParcelableArrayListExtra(
Intent.EXTRA_STREAM, ArrayList(uris)
)
uris.forEach {
// Why are we using ClipData in addition to parcelableArrayListExtra here?
// Because the FLAG_GRANT_READ_URI_PERMISSION needs to be applied to the URIs, and
// for some reason it doesn't get applied to all of them when not using ClipData
if (clipData == null) {
clipData = ClipData("", emptyArray(), ClipData.Item(it))
} else {
clipData!!.addItem(ClipData.Item(it))
}
}
setClass(context, PostCreationActivity::class.java)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
}
} }
private var user: UserDatabaseEntity? = null
private lateinit var instance: InstanceDatabaseEntity
private lateinit var binding: ActivityPostCreationBinding private lateinit var binding: ActivityPostCreationBinding
private lateinit var navController: NavController private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
user = db.userDao().getActiveUser()
instance = user?.run {
db.instanceDao().getAll().first { instanceDatabaseEntity ->
instanceDatabaseEntity.uri.contains(instance_uri)
}
} ?: InstanceDatabaseEntity("", "")
binding = ActivityPostCreationBinding.inflate(layoutInflater) binding = ActivityPostCreationBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
val navHostFragment = val navHostFragment =
@ -46,8 +60,6 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
navController.setGraph(R.navigation.post_creation_graph) navController.setGraph(R.navigation.post_creation_graph)
} }
override fun onSupportNavigateUp(): Boolean { override fun onSupportNavigateUp() = navController.navigateUp() || super.onSupportNavigateUp()
return navController.navigateUp() || super.onSupportNavigateUp()
}
} }

View File

@ -35,25 +35,22 @@ import org.pixeldroid.app.databinding.FragmentPostCreationBinding
import org.pixeldroid.app.postCreation.camera.CameraActivity import org.pixeldroid.app.postCreation.camera.CameraActivity
import org.pixeldroid.app.postCreation.carousel.CarouselItem import org.pixeldroid.app.postCreation.carousel.CarouselItem
import org.pixeldroid.app.utils.BaseFragment import org.pixeldroid.app.utils.BaseFragment
import org.pixeldroid.app.utils.bindingLifecycleAware
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import org.pixeldroid.app.utils.fileExtension import org.pixeldroid.app.utils.fileExtension
import org.pixeldroid.app.utils.getMimeType import org.pixeldroid.app.utils.getMimeType
import org.pixeldroid.media_editor.common.PICTURE_POSITION
import org.pixeldroid.media_editor.common.PICTURE_URI
import org.pixeldroid.media_editor.photoEdit.PhotoEditActivity import org.pixeldroid.media_editor.photoEdit.PhotoEditActivity
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity import org.pixeldroid.media_editor.videoEdit.VideoEditActivity
import java.io.File import java.io.File
import java.io.OutputStream import java.io.OutputStream
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
class PostCreationFragment : BaseFragment() { class PostCreationFragment : BaseFragment() {
private var binding: FragmentPostCreationBinding by bindingLifecycleAware()
private var user: UserDatabaseEntity? = null private val model: PostCreationViewModel by activityViewModels()
private var instance: InstanceDatabaseEntity = InstanceDatabaseEntity("", "")
private lateinit var binding: FragmentPostCreationBinding
private lateinit var model: PostCreationViewModel
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
@ -63,35 +60,23 @@ class PostCreationFragment : BaseFragment() {
// Inflate the layout for this fragment // Inflate the layout for this fragment
binding = FragmentPostCreationBinding.inflate(layoutInflater) binding = FragmentPostCreationBinding.inflate(layoutInflater)
return binding.root return binding.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
user = db.userDao().getActiveUser() val user = db.userDao().getActiveUser()
instance = user?.run { val instance = user?.run {
db.instanceDao().getAll().first { instanceDatabaseEntity -> db.instanceDao().getInstance(instance_uri)
instanceDatabaseEntity.uri.contains(instance_uri)
}
} ?: InstanceDatabaseEntity("", "") } ?: InstanceDatabaseEntity("", "")
val _model: PostCreationViewModel by activityViewModels { model.getPhotoData().observe(viewLifecycleOwner) { newPhotoData: MutableList<PhotoData>? ->
PostCreationViewModelFactory(
requireActivity().application,
requireActivity().intent.clipData!!,
instance,
requireActivity().intent.getStringExtra(PostCreationActivity.PICTURE_DESCRIPTION),
requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false)
)
}
model = _model
model.getPhotoData().observe(viewLifecycleOwner) { newPhotoData ->
// update UI // update UI
binding.carousel.addData( binding.carousel.addData(
newPhotoData.map { newPhotoData.orEmpty().map {
CarouselItem( CarouselItem(
it.imageUri, it.imageDescription, it.video, it.imageUri, it.imageDescription, it.video,
it.videoEncodeProgress, it.videoEncodeStabilizationFirstPass, it.videoEncodeProgress, it.videoEncodeStabilizationFirstPass,
@ -99,6 +84,7 @@ class PostCreationFragment : BaseFragment() {
) )
} }
) )
binding.postCreationNextButton.isEnabled = newPhotoData?.isNotEmpty() ?: false
} }
lifecycleScope.launch { lifecycleScope.launch {
@ -119,13 +105,26 @@ class PostCreationFragment : BaseFragment() {
binding.toolbarPostCreation.visibility = binding.toolbarPostCreation.visibility =
if (uiState.isCarousel) View.VISIBLE else View.INVISIBLE if (uiState.isCarousel) View.VISIBLE else View.INVISIBLE
binding.carousel.layoutCarousel = uiState.isCarousel binding.carousel.layoutCarousel = uiState.isCarousel
if(uiState.storyCreation){
binding.toggleStoryPost.check(binding.buttonStory.id)
binding.buttonStory.isPressed = true
binding.carousel.showLayoutSwitchButton = false
binding.carousel.showIndicator = false
} else {
binding.toggleStoryPost.check(binding.buttonPost.id)
binding.carousel.showLayoutSwitchButton = true
binding.carousel.showIndicator = true
}
binding.carousel.maxEntries = uiState.maxEntries
} }
} }
} }
binding.carousel.apply { binding.carousel.apply {
layoutCarouselCallback = { model.becameCarousel(it)} layoutCarouselCallback = { model.becameCarousel(it)}
maxEntries = instance.albumLimit maxEntries = if(model.uiState.value.storyCreation) 1 else instance.albumLimit
addPhotoButtonCallback = { addPhotoButtonCallback = {
addPhoto() addPhoto()
} }
@ -133,9 +132,10 @@ class PostCreationFragment : BaseFragment() {
model.updateDescription(position, description) model.updateDescription(position, description)
} }
} }
// get the description and send the post
binding.postCreationSendButton.setOnClickListener { // Validate the post and go to the next step of the post creation process
if (validatePost() && model.isNotEmpty()) { binding.postCreationNextButton.setOnClickListener {
if (validatePost()) {
findNavController().navigate(R.id.action_postCreationFragment_to_postSubmissionFragment) findNavController().navigate(R.id.action_postCreationFragment_to_postSubmissionFragment)
} }
} }
@ -163,6 +163,23 @@ class PostCreationFragment : BaseFragment() {
} }
} }
binding.toggleStoryPost.addOnButtonCheckedListener { _, checkedId, isChecked ->
// Only handle checked events
if (!isChecked) return@addOnButtonCheckedListener
when (checkedId) {
R.id.buttonStory -> {
model.storyMode(true)
}
R.id.buttonPost -> {
model.storyMode(false)
}
}
}
binding.backbutton.setOnClickListener{requireActivity().onBackPressedDispatcher.onBackPressed()}
// Clean up temporary files, if any // Clean up temporary files, if any
val tempFiles = requireActivity().intent.getStringArrayExtra(PostCreationActivity.TEMP_FILES) val tempFiles = requireActivity().intent.getStringArrayExtra(PostCreationActivity.TEMP_FILES)
tempFiles?.asList()?.forEach { tempFiles?.asList()?.forEach {
@ -191,10 +208,9 @@ class PostCreationFragment : BaseFragment() {
} }
private val addPhotoResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> private val addPhotoResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK && result.data?.clipData != null) { val uris = result.data?.extras?.getParcelableArrayList<Uri>(Intent.EXTRA_STREAM)
result.data?.clipData?.let { if (result.resultCode == Activity.RESULT_OK && uris != null) {
model.setImages(model.addPossibleImages(it)) model.setImages(model.addPossibleImages(uris, emptyList()))
}
} else if (result.resultCode != Activity.RESULT_CANCELED) { } else if (result.resultCode != Activity.RESULT_CANCELED) {
Toast.makeText(requireActivity(), R.string.add_images_error, Toast.LENGTH_SHORT).show() Toast.makeText(requireActivity(), R.string.add_images_error, Toast.LENGTH_SHORT).show()
} }
@ -275,21 +291,24 @@ class PostCreationFragment : BaseFragment() {
private fun validatePost(): Boolean { private fun validatePost(): Boolean {
if (model.getPhotoData().value?.all { !it.video || it.videoEncodeComplete } == false) { if (model.getPhotoData().value?.none { it.video && it.videoEncodeComplete == false } == true) {
MaterialAlertDialogBuilder(requireActivity()).apply { // Encoding is done, i.e. none of the items are both a video and not done encoding.
setMessage(R.string.still_encoding) // We return true if the post is not empty, false otherwise.
setNegativeButton(android.R.string.ok) { _, _ -> } return model.getPhotoData().value?.isNotEmpty() == true
}.show()
return false
} }
return true // Encoding is not done, show a dialog and return false to indicate validation failed
MaterialAlertDialogBuilder(requireActivity()).apply {
setMessage(R.string.still_encoding)
setNegativeButton(android.R.string.ok) { _, _ -> }
}.show()
return false
} }
private val editResultContract: ActivityResultLauncher<Intent> = registerForActivityResult( private val editResultContract: ActivityResultLauncher<Intent> = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()){ ActivityResultContracts.StartActivityForResult()){
result: ActivityResult? -> result: ActivityResult? ->
if (result?.resultCode == Activity.RESULT_OK && result.data != null) { if (result?.resultCode == Activity.RESULT_OK && result.data != null) {
val position: Int = result.data!!.getIntExtra(PhotoEditActivity.PICTURE_POSITION, 0) val position: Int = result.data!!.getIntExtra(PICTURE_POSITION, 0)
model.modifyAt(position, result.data!!) model.modifyAt(position, result.data!!)
?: Toast.makeText(requireActivity(), R.string.error_editing, Toast.LENGTH_SHORT).show() ?: Toast.makeText(requireActivity(), R.string.error_editing, Toast.LENGTH_SHORT).show()
} else if(result?.resultCode != Activity.RESULT_CANCELED){ } else if(result?.resultCode != Activity.RESULT_CANCELED){
@ -302,8 +321,8 @@ class PostCreationFragment : BaseFragment() {
requireActivity(), requireActivity(),
if (model.getPhotoData().value!![position].video) VideoEditActivity::class.java else PhotoEditActivity::class.java if (model.getPhotoData().value!![position].video) VideoEditActivity::class.java else PhotoEditActivity::class.java
) )
.putExtra(PhotoEditActivity.PICTURE_URI, model.getPhotoData().value!![position].imageUri) .putExtra(PICTURE_URI, model.getPhotoData().value!![position].imageUri)
.putExtra(PhotoEditActivity.PICTURE_POSITION, position) .putExtra(PICTURE_POSITION, position)
editResultContract.launch(intent) editResultContract.launch(intent)
} }

View File

@ -1,7 +1,6 @@
package org.pixeldroid.app.postCreation package org.pixeldroid.app.postCreation
import android.app.Application import android.content.Context
import android.content.ClipData
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Parcelable import android.os.Parcelable
@ -12,16 +11,18 @@ import android.widget.Toast
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.jarsilio.android.scrambler.exceptions.UnsupportedFileFormatException import com.jarsilio.android.scrambler.exceptions.UnsupportedFileFormatException
import com.jarsilio.android.scrambler.stripMetadata import com.jarsilio.android.scrambler.stripMetadata
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -32,36 +33,25 @@ import kotlinx.parcelize.Parcelize
import okhttp3.MultipartBody import okhttp3.MultipartBody
import org.pixeldroid.app.MainActivity import org.pixeldroid.app.MainActivity
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.utils.PixelDroidApplication import org.pixeldroid.app.postCreation.camera.CameraFragment
import org.pixeldroid.app.utils.api.objects.Attachment import org.pixeldroid.app.utils.api.objects.Attachment
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import org.pixeldroid.app.utils.di.PixelfedAPIHolder import org.pixeldroid.app.utils.di.PixelfedAPIHolder
import org.pixeldroid.app.utils.fileExtension import org.pixeldroid.app.utils.fileExtension
import org.pixeldroid.app.utils.getMimeType import org.pixeldroid.app.utils.getMimeType
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity import org.pixeldroid.media_editor.common.PICTURE_URI
import org.pixeldroid.media_editor.videoEdit.VideoEditActivity
import retrofit2.HttpException import retrofit2.HttpException
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
import java.net.URI import java.net.URI
import javax.inject.Inject import javax.inject.Inject
import kotlin.collections.ArrayList
import kotlin.collections.MutableList
import kotlin.collections.MutableMap
import kotlin.collections.arrayListOf
import kotlin.collections.forEach
import kotlin.collections.get
import kotlin.collections.getOrNull
import kotlin.collections.indexOfFirst
import kotlin.collections.isNotEmpty
import kotlin.collections.mutableListOf
import kotlin.collections.mutableMapOf
import kotlin.collections.plus
import kotlin.collections.set import kotlin.collections.set
import kotlin.collections.toMutableList
import kotlin.math.ceil import kotlin.math.ceil
const val TAG = "Post Creation ViewModel"
// Models the UI state for the PostCreationActivity // Models the UI state for the PostCreationActivity
data class PostCreationActivityUiState( data class PostCreationActivityUiState(
@ -70,6 +60,7 @@ data class PostCreationActivityUiState(
val addPhotoButtonEnabled: Boolean = true, val addPhotoButtonEnabled: Boolean = true,
val editPhotoButtonEnabled: Boolean = true, val editPhotoButtonEnabled: Boolean = true,
val removePhotoButtonEnabled: Boolean = true, val removePhotoButtonEnabled: Boolean = true,
val maxEntries: Int?,
val isCarousel: Boolean = true, val isCarousel: Boolean = true,
@ -86,6 +77,11 @@ data class PostCreationActivityUiState(
val uploadErrorVisible: Boolean = false, val uploadErrorVisible: Boolean = false,
val uploadErrorExplanationText: String = "", val uploadErrorExplanationText: String = "",
val uploadErrorExplanationVisible: Boolean = false, val uploadErrorExplanationVisible: Boolean = false,
val storyCreation: Boolean,
val storyDuration: Int = 10,
val storyReplies: Boolean = true,
val storyReactions: Boolean = true,
) )
@Parcelize @Parcelize
@ -98,37 +94,62 @@ data class PhotoData(
var video: Boolean, var video: Boolean,
var videoEncodeProgress: Int? = null, var videoEncodeProgress: Int? = null,
var videoEncodeStabilizationFirstPass: Boolean? = null, var videoEncodeStabilizationFirstPass: Boolean? = null,
var videoEncodeComplete: Boolean = true, var videoEncodeComplete: Boolean? = null,
var videoEncodeError: Boolean = false, var videoEncodeError: Boolean = false,
) : Parcelable ) : Parcelable
class PostCreationViewModel( @HiltViewModel
application: Application, class PostCreationViewModel @Inject constructor(
clipdata: ClipData? = null, private val state: SavedStateHandle,
val instance: InstanceDatabaseEntity? = null, @ApplicationContext private val applicationContext: Context,
existingDescription: String? = null, db: AppDatabase,
existingNSFW: Boolean = false ): ViewModel() {
) : AndroidViewModel(application) { private var storyPhotoDataBackup: MutableList<PhotoData>? = null
private val photoData: MutableLiveData<MutableList<PhotoData>> by lazy { private val photoData: MutableLiveData<MutableList<PhotoData>> by lazy {
MutableLiveData<MutableList<PhotoData>>().also { //FIXME We should be able to access the Intent action somehow, to determine if there are
it.value = clipdata?.let { it1 -> addPossibleImages(it1, mutableListOf()) } // 1 or multiple Uris instead of relying on the ClassCastException
// This should not work like this (reading its source code, get() function should return null
// if it's the wrong type but instead throws ClassCastException).
// Lucky for us that it does though: we first try to get a single Uri (which we could be
// getting from a share of a single picture to the app), when the cast to Uri fails
// we try to get a list of Uris instead (casting ourselves from Parcelable as suggested
// in get() documentation)
val uris = try {
val singleUri: Uri? = state[Intent.EXTRA_STREAM]
listOfNotNull(singleUri)
} catch (e: ClassCastException) {
state.get<ArrayList<Parcelable>>(Intent.EXTRA_STREAM)?.map { it as Uri }
} }
MutableLiveData<MutableList<PhotoData>>(
addPossibleImages(
uris,
state.get<ArrayList<String>>(PostCreationActivity.PICTURE_DESCRIPTIONS),
previousList = mutableListOf()
)
)
} }
private val instance = db.instanceDao().getActiveInstance()
@Inject @Inject
lateinit var apiHolder: PixelfedAPIHolder lateinit var apiHolder: PixelfedAPIHolder
private val _uiState: MutableStateFlow<PostCreationActivityUiState> private val _uiState: MutableStateFlow<PostCreationActivityUiState>
init { init {
(application as PixelDroidApplication).getAppComponent().inject(this)
val sharedPreferences = val sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(application) PreferenceManager.getDefaultSharedPreferences(applicationContext)
val templateDescription = sharedPreferences.getString("prefill_description", "") ?: "" val templateDescription = sharedPreferences.getString("prefill_description", "") ?: ""
val storyCreation: Boolean = state[CameraFragment.CAMERA_ACTIVITY_STORY] ?: false
_uiState = MutableStateFlow(PostCreationActivityUiState( _uiState = MutableStateFlow(PostCreationActivityUiState(
newPostDescriptionText = existingDescription ?: templateDescription, newPostDescriptionText = state[PostCreationActivity.POST_DESCRIPTION] ?: templateDescription,
nsfw = existingNSFW nsfw = state[PostCreationActivity.POST_NSFW] ?: false,
maxEntries = if(storyCreation) 1 else instance?.albumLimit,
storyCreation = storyCreation
)) ))
} }
@ -145,35 +166,50 @@ class PostCreationViewModel(
} }
} }
/**
* Read-only public view on [photoData]
*/
fun getPhotoData(): LiveData<MutableList<PhotoData>> = photoData fun getPhotoData(): LiveData<MutableList<PhotoData>> = photoData
/** /**
* Will add as many images as possible to [photoData], from the [clipData], and if * Will add as many images as possible to [photoData], from the [uris], and if
* ([photoData].size + [clipData].itemCount) > [InstanceDatabaseEntity.albumLimit] then it will only add as many images * ([photoData].size + [uris].size) > uiState.value.maxEntries then it will only add as many images
* as are legal (if any) and a dialog will be shown to the user alerting them of this fact. * as are legal (if any) and a dialog will be shown to the user alerting them of this fact.
*/ */
fun addPossibleImages(clipData: ClipData, previousList: MutableList<PhotoData>? = photoData.value): MutableList<PhotoData> { fun addPossibleImages(
uris: List<Uri>?,
descriptions: List<String>?,
previousList: MutableList<PhotoData>? = photoData.value,
): MutableList<PhotoData> {
val dataToAdd: ArrayList<PhotoData> = arrayListOf() val dataToAdd: ArrayList<PhotoData> = arrayListOf()
var count = clipData.itemCount var count = uris?.size ?: 0
if(count + (previousList?.size ?: 0) > instance!!.albumLimit){ uiState.value.maxEntries?.let { maxEntries ->
_uiState.update { currentUiState -> if(count + (previousList?.size ?: 0) > maxEntries){
currentUiState.copy(userMessage = getApplication<PixelDroidApplication>().getString(R.string.total_exceeds_album_limit).format(instance.albumLimit)) _uiState.update { currentUiState ->
currentUiState.copy(userMessage = applicationContext.getString(R.string.total_exceeds_album_limit).format(maxEntries))
}
count = count.coerceAtMost(maxEntries - (previousList?.size ?: 0))
} }
count = count.coerceAtMost(instance.albumLimit - (previousList?.size ?: 0)) if (count + (previousList?.size ?: 0) >= maxEntries) {
} // Disable buttons to add more images
if (count + (previousList?.size ?: 0) >= instance.albumLimit) { _uiState.update { currentUiState ->
// Disable buttons to add more images currentUiState.copy(addPhotoButtonEnabled = false)
_uiState.update { currentUiState -> }
currentUiState.copy(addPhotoButtonEnabled = false)
} }
} for ((i, uri) in uris.orEmpty().withIndex()) {
for (i in 0 until count) {
clipData.getItemAt(i).let {
val sizeAndVideoPair: Pair<Long, Boolean> = val sizeAndVideoPair: Pair<Long, Boolean> =
getSizeAndVideoValidate(it.uri, (previousList?.size ?: 0) + dataToAdd.size + 1) getSizeAndVideoValidate(uri, (previousList?.size ?: 0) + dataToAdd.size + 1)
dataToAdd.add(PhotoData(imageUri = it.uri, size = sizeAndVideoPair.first, video = sizeAndVideoPair.second, imageDescription = it.text?.toString())) dataToAdd.add(
PhotoData(
imageUri = uri,
size = sizeAndVideoPair.first,
video = sizeAndVideoPair.second,
imageDescription = descriptions?.getOrNull(i)
)
)
} }
} }
return previousList?.plus(dataToAdd)?.toMutableList() ?: mutableListOf() return previousList?.plus(dataToAdd)?.toMutableList() ?: mutableListOf()
} }
@ -185,46 +221,47 @@ class PostCreationViewModel(
* Returns the size of the file of the Uri, and whether it is a video, * Returns the size of the file of the Uri, and whether it is a video,
* and opens a dialog in case it is too big or in case the file is unsupported. * and opens a dialog in case it is too big or in case the file is unsupported.
*/ */
fun getSizeAndVideoValidate(uri: Uri, editPosition: Int): Pair<Long, Boolean> { private fun getSizeAndVideoValidate(uri: Uri, editPosition: Int): Pair<Long, Boolean> {
val size: Long = val size: Long =
if (uri.scheme =="content") { if (uri.scheme =="content") {
getApplication<PixelDroidApplication>().contentResolver.query(uri, null, null, null, null) applicationContext.contentResolver.query(uri, null, null, null, null)
?.use { cursor -> ?.use { cursor ->
/* Get the column indexes of the data in the Cursor, /* Get the column indexes of the data in the Cursor,
* move to the first row in the Cursor, get the data, * move to the first row in the Cursor, get the data,
* and display it. * and display it.
*/ */
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
cursor.moveToFirst() if(sizeIndex >= 0) {
cursor.getLong(sizeIndex) cursor.moveToFirst()
cursor.getLong(sizeIndex)
} else null
} ?: 0 } ?: 0
} else { } else {
uri.toFile().length() uri.toFile().length()
} }
val sizeInkBytes = ceil(size.toDouble() / 1000).toLong() val sizeInkBytes = ceil(size.toDouble() / 1000).toLong()
val type = uri.getMimeType(getApplication<PixelDroidApplication>().contentResolver) val type = uri.getMimeType(applicationContext.contentResolver)
val isVideo = type.startsWith("video/") val isVideo = type.startsWith("video/")
if (isVideo && !instance!!.videoEnabled) { if (isVideo && !instance!!.videoEnabled) {
_uiState.update { currentUiState -> _uiState.update { currentUiState ->
currentUiState.copy(userMessage = getApplication<PixelDroidApplication>().getString(R.string.video_not_supported)) currentUiState.copy(userMessage = applicationContext.getString(R.string.video_not_supported))
} }
} }
if ((!isVideo && sizeInkBytes > instance!!.maxPhotoSize) || (isVideo && sizeInkBytes > instance!!.maxVideoSize)) { if ((!isVideo && sizeInkBytes > instance!!.maxPhotoSize) || (isVideo && sizeInkBytes > instance!!.maxVideoSize)) {
//TODO Offer remedy for too big file: re-compress it
val maxSize = if (isVideo) instance.maxVideoSize else instance.maxPhotoSize val maxSize = if (isVideo) instance.maxVideoSize else instance.maxPhotoSize
_uiState.update { currentUiState -> _uiState.update { currentUiState ->
currentUiState.copy( currentUiState.copy(
userMessage = getApplication<PixelDroidApplication>().getString(R.string.size_exceeds_instance_limit, editPosition, sizeInkBytes, maxSize) userMessage = applicationContext.getString(R.string.size_exceeds_instance_limit, editPosition, sizeInkBytes, maxSize)
) )
} }
} }
return Pair(size, isVideo) return Pair(size, isVideo)
} }
fun isNotEmpty(): Boolean = photoData.value?.isNotEmpty() ?: false
fun updateDescription(position: Int, description: String) { fun updateDescription(position: Int, description: String) {
photoData.value?.getOrNull(position)?.imageDescription = description photoData.value?.getOrNull(position)?.imageDescription = description
photoData.value = photoData.value photoData.value = photoData.value
@ -234,8 +271,8 @@ class PostCreationViewModel(
photoData.value?.removeAt(currentPosition) photoData.value?.removeAt(currentPosition)
_uiState.update { _uiState.update {
it.copy( it.copy(
addPhotoButtonEnabled = true addPhotoButtonEnabled = (photoData.value?.size ?: 0) < (uiState.value.maxEntries ?: 0),
) )
} }
photoData.value = photoData.value photoData.value = photoData.value
} }
@ -254,8 +291,8 @@ class PostCreationViewModel(
videoEncodeProgress = 0 videoEncodeProgress = 0
videoEncodeComplete = false videoEncodeComplete = false
VideoEditActivity.startEncoding(imageUri, it, VideoEditActivity.startEncoding(imageUri, null, it,
context = getApplication<PixelDroidApplication>(), context = applicationContext,
registerNewFFmpegSession = ::registerNewFFmpegSession, registerNewFFmpegSession = ::registerNewFFmpegSession,
trackTempFile = ::trackTempFile, trackTempFile = ::trackTempFile,
videoEncodeProgress = ::videoEncodeProgress videoEncodeProgress = ::videoEncodeProgress
@ -263,7 +300,7 @@ class PostCreationViewModel(
} }
} }
} else { } else {
imageUri = data.getStringExtra(org.pixeldroid.media_editor.photoEdit.PhotoEditActivity.PICTURE_URI)!!.toUri() imageUri = data.getStringExtra(PICTURE_URI)!!.toUri()
val (imageSize, imageVideo) = getSizeAndVideoValidate(imageUri, position) val (imageSize, imageVideo) = getSizeAndVideoValidate(imageUri, position)
size = imageSize size = imageSize
video = imageVideo video = imageVideo
@ -370,17 +407,17 @@ class PostCreationViewModel(
} }
for (data: PhotoData in getPhotoData().value ?: emptyList()) { for (data: PhotoData in getPhotoData().value ?: emptyList()) {
val extension = data.imageUri.fileExtension(getApplication<PixelDroidApplication>().contentResolver) val extension = data.imageUri.fileExtension(applicationContext.contentResolver)
val strippedImage = File.createTempFile("temp_img", ".$extension", getApplication<PixelDroidApplication>().cacheDir) val strippedImage = File.createTempFile("temp_img", ".$extension", applicationContext.cacheDir)
val imageUri = data.imageUri val imageUri = data.imageUri
val (strippedOrNot, size) = try { val (strippedOrNot, size) = try {
val orientation = ExifInterface(getApplication<PixelDroidApplication>().contentResolver.openInputStream(imageUri)!!).getAttributeInt( val orientation = ExifInterface(applicationContext.contentResolver.openInputStream(imageUri)!!).getAttributeInt(
ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
stripMetadata(imageUri, strippedImage, getApplication<PixelDroidApplication>().contentResolver) stripMetadata(imageUri, strippedImage, applicationContext.contentResolver)
// Restore EXIF orientation // Restore EXIF orientation
val exifInterface = ExifInterface(strippedImage) val exifInterface = ExifInterface(strippedImage)
@ -392,11 +429,11 @@ class PostCreationViewModel(
strippedImage.delete() strippedImage.delete()
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete() if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
val imageInputStream = try { val imageInputStream = try {
getApplication<PixelDroidApplication>().contentResolver.openInputStream(imageUri)!! applicationContext.contentResolver.openInputStream(imageUri)!!
} catch (e: FileNotFoundException){ } catch (e: FileNotFoundException){
_uiState.update { currentUiState -> _uiState.update { currentUiState ->
currentUiState.copy( currentUiState.copy(
userMessage = getApplication<PixelDroidApplication>().getString(R.string.file_not_found, userMessage = applicationContext.getString(R.string.file_not_found,
data.imageUri) data.imageUri)
) )
} }
@ -408,14 +445,14 @@ class PostCreationViewModel(
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete() if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
_uiState.update { currentUiState -> _uiState.update { currentUiState ->
currentUiState.copy( currentUiState.copy(
userMessage = getApplication<PixelDroidApplication>().getString(R.string.file_not_found, userMessage = applicationContext.getString(R.string.file_not_found,
data.imageUri) data.imageUri)
) )
} }
return return
} }
val type = data.imageUri.getMimeType(getApplication<PixelDroidApplication>().contentResolver) val type = data.imageUri.getMimeType(applicationContext.contentResolver)
val imagePart = ProgressRequestBody(strippedOrNot, size, type) val imagePart = ProgressRequestBody(strippedOrNot, size, type)
val requestBody = MultipartBody.Builder() val requestBody = MultipartBody.Builder()
.setType(MultipartBody.FORM) .setType(MultipartBody.FORM)
@ -442,7 +479,10 @@ class PostCreationViewModel(
apiHolder.setToCurrentUser(it) apiHolder.setToCurrentUser(it)
} ?: apiHolder.api ?: apiHolder.setToCurrentUser() } ?: apiHolder.api ?: apiHolder.setToCurrentUser()
val inter = api.mediaUpload(description, requestBody.parts[0]) val inter: Observable<Attachment> =
//TODO validate that image is correct (?) aspect ratio
if (uiState.value.storyCreation) api.storyUpload(requestBody.parts[0])
else api.mediaUpload(description, requestBody.parts[0])
apiHolder.api = null apiHolder.api = null
postSub = inter postSub = inter
@ -451,14 +491,18 @@ class PostCreationViewModel(
.subscribe( .subscribe(
{ attachment: Attachment -> { attachment: Attachment ->
data.progress = 0 data.progress = 0
data.uploadId = attachment.id!! data.uploadId = if(uiState.value.storyCreation){
attachment.media_id!!
} else {
attachment.id!!
}
}, },
{ e: Throwable -> { e: Throwable ->
_uiState.update { currentUiState -> _uiState.update { currentUiState ->
currentUiState.copy( currentUiState.copy(
uploadErrorVisible = true, uploadErrorVisible = true,
uploadErrorExplanationText = if(e is HttpException){ uploadErrorExplanationText = if(e is HttpException){
getApplication<PixelDroidApplication>().getString(R.string.upload_error, e.code()) applicationContext.getString(R.string.upload_error, e.code())
} else "", } else "",
uploadErrorExplanationVisible = e is HttpException, uploadErrorExplanationVisible = e is HttpException,
) )
@ -507,19 +551,31 @@ class PostCreationViewModel(
apiHolder.setToCurrentUser(it) apiHolder.setToCurrentUser(it)
} ?: apiHolder.api ?: apiHolder.setToCurrentUser() } ?: apiHolder.api ?: apiHolder.setToCurrentUser()
api.postStatus( if(uiState.value.storyCreation){
statusText = description, val canReact = if (uiState.value.storyReactions) "1" else "0"
media_ids = getPhotoData().value!!.mapNotNull { it.uploadId }.toList(), val canReply = if (uiState.value.storyReplies) "1" else "0"
sensitive = nsfw
) api.storyPublish(
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_success), media_id = getPhotoData().value!!.firstNotNullOf { it.uploadId },
can_react = canReact,
can_reply = canReply,
duration = uiState.value.storyDuration
)
} else {
api.postStatus(
statusText = description,
media_ids = getPhotoData().value!!.mapNotNull { it.uploadId }.toList(),
sensitive = nsfw
)
}
Toast.makeText(applicationContext, applicationContext.getString(R.string.upload_post_success),
Toast.LENGTH_SHORT).show() Toast.LENGTH_SHORT).show()
val intent = Intent(getApplication(), MainActivity::class.java) val intent = Intent(applicationContext, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
//TODO make the activity launch this instead (and surrounding toasts too) //TODO make the activity launch this instead (and surrounding toasts too)
getApplication<PixelDroidApplication>().startActivity(intent) applicationContext.startActivity(intent)
} catch (exception: IOException) { } catch (exception: IOException) {
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_error), Toast.makeText(applicationContext, applicationContext.getString(R.string.upload_post_error),
Toast.LENGTH_SHORT).show() Toast.LENGTH_SHORT).show()
Log.e(TAG, exception.toString()) Log.e(TAG, exception.toString())
_uiState.update { currentUiState -> _uiState.update { currentUiState ->
@ -528,7 +584,7 @@ class PostCreationViewModel(
) )
} }
} catch (exception: HttpException) { } catch (exception: HttpException) {
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_failed), Toast.makeText(applicationContext, applicationContext.getString(R.string.upload_post_failed),
Toast.LENGTH_SHORT).show() Toast.LENGTH_SHORT).show()
Log.e(TAG, exception.response().toString() + exception.message().toString()) Log.e(TAG, exception.response().toString() + exception.message().toString())
_uiState.update { currentUiState -> _uiState.update { currentUiState ->
@ -551,10 +607,46 @@ class PostCreationViewModel(
fun chooseAccount(which: UserDatabaseEntity) { fun chooseAccount(which: UserDatabaseEntity) {
_uiState.update { it.copy(chosenAccount = which) } _uiState.update { it.copy(chosenAccount = which) }
} }
}
class PostCreationViewModelFactory(val application: Application, val clipdata: ClipData, val instance: InstanceDatabaseEntity, val existingDescription: String?, val existingNSFW: Boolean) : ViewModelProvider.Factory { fun storyMode(storyMode: Boolean) {
override fun <T : ViewModel> create(modelClass: Class<T>): T { //TODO check ratio of files in story mode? What is acceptable?
return modelClass.getConstructor(Application::class.java, ClipData::class.java, InstanceDatabaseEntity::class.java, String::class.java, Boolean::class.java).newInstance(application, clipdata, instance, existingDescription, existingNSFW)
val newMaxEntries = if (storyMode) 1 else instance?.albumLimit
var newUiState = _uiState.value.copy(
storyCreation = storyMode,
maxEntries = newMaxEntries,
addPhotoButtonEnabled = (photoData.value?.size ?: 0) < (newMaxEntries ?: 0),
)
// Carousel on if in story mode
if (storyMode) newUiState = newUiState.copy(isCarousel = true)
// If switching to story, and there are too many pictures, keep the first and backup the rest
if (storyMode && (photoData.value?.size ?: 0) > 1){
storyPhotoDataBackup = photoData.value
photoData.value = photoData.value?.let { mutableListOf(it.firstOrNull()).filterNotNull().toMutableList() }
//Show message saying extraneous pictures were removed but can be restored
newUiState = newUiState.copy(
userMessage = applicationContext.getString(R.string.extraneous_pictures_stories)
)
}
// Restore if backup not null and first value is unchanged
else if (storyPhotoDataBackup != null && storyPhotoDataBackup?.firstOrNull() == photoData.value?.firstOrNull()){
photoData.value = storyPhotoDataBackup
storyPhotoDataBackup = null
}
_uiState.update { newUiState }
} }
}
fun storyDuration(value: Int) {
_uiState.update {
it.copy(storyDuration = value)
}
}
fun updateStoryReactions(checked: Boolean) { _uiState.update { it.copy(storyReactions = checked) } }
fun updateStoryReplies(checked: Boolean) { _uiState.update { it.copy(storyReplies = checked) } }
}

View File

@ -20,10 +20,13 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.FragmentPostSubmissionBinding import org.pixeldroid.app.databinding.FragmentPostSubmissionBinding
import org.pixeldroid.app.postCreation.camera.CameraFragment
import org.pixeldroid.app.utils.BaseFragment import org.pixeldroid.app.utils.BaseFragment
import org.pixeldroid.app.utils.bindingLifecycleAware
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import org.pixeldroid.app.utils.setSquareImageFromURL import org.pixeldroid.app.utils.setSquareImageFromURL
import kotlin.math.roundToInt
class PostSubmissionFragment : BaseFragment() { class PostSubmissionFragment : BaseFragment() {
@ -34,8 +37,8 @@ class PostSubmissionFragment : BaseFragment() {
private var user: UserDatabaseEntity? = null private var user: UserDatabaseEntity? = null
private lateinit var instance: InstanceDatabaseEntity private lateinit var instance: InstanceDatabaseEntity
private lateinit var binding: FragmentPostSubmissionBinding private var binding: FragmentPostSubmissionBinding by bindingLifecycleAware()
private lateinit var model: PostCreationViewModel private val model: PostCreationViewModel by activityViewModels()
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
@ -57,26 +60,25 @@ class PostSubmissionFragment : BaseFragment() {
accounts = db.userDao().getAll() accounts = db.userDao().getAll()
instance = user?.run { instance = user?.run {
db.instanceDao().getAll().first { instanceDatabaseEntity -> db.instanceDao().getInstance(instance_uri)
instanceDatabaseEntity.uri.contains(instance_uri)
}
} ?: InstanceDatabaseEntity("", "") } ?: InstanceDatabaseEntity("", "")
val _model: PostCreationViewModel by activityViewModels {
PostCreationViewModelFactory(
requireActivity().application,
requireActivity().intent.clipData!!,
instance,
requireActivity().intent.getStringExtra(PostCreationActivity.PICTURE_DESCRIPTION),
requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false)
)
}
model = _model
// Display the values from the view model // Display the values from the view model
binding.nsfwSwitch.isChecked = model.uiState.value.nsfw binding.nsfwSwitch.isChecked = model.uiState.value.nsfw
binding.newPostDescriptionInputField.setText(model.uiState.value.newPostDescriptionText) binding.newPostDescriptionInputField.setText(model.uiState.value.newPostDescriptionText)
if(model.uiState.value.storyCreation){
binding.nsfwSwitch.visibility = View.GONE
binding.postTextInputLayout.visibility = View.GONE
binding.privateTitle.visibility = View.GONE
binding.postPreview.visibility = View.GONE
binding.storyOptions.visibility = View.VISIBLE
binding.storyDurationSlider.value = model.uiState.value.storyDuration.toFloat()
binding.storyRepliesSwitch.isChecked = model.uiState.value.storyReplies
binding.storyReactionsSwitch.isChecked = model.uiState.value.storyReactions
}
lifecycleScope.launch { lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
model.uiState.collect { uiState -> model.uiState.collect { uiState ->
@ -114,13 +116,24 @@ class PostSubmissionFragment : BaseFragment() {
binding.nsfwSwitch.setOnCheckedChangeListener { _, isChecked -> binding.nsfwSwitch.setOnCheckedChangeListener { _, isChecked ->
model.updateNSFW(isChecked) model.updateNSFW(isChecked)
} }
binding.storyRepliesSwitch.setOnCheckedChangeListener { _, isChecked ->
model.updateStoryReplies(isChecked)
}
binding.storyReactionsSwitch.setOnCheckedChangeListener { _, isChecked ->
model.updateStoryReactions(isChecked)
}
binding.postTextInputLayout.counterMaxLength = instance.maxStatusChars binding.postTextInputLayout.counterMaxLength = instance.maxStatusChars
binding.storyDurationSlider.addOnChangeListener { _, value, _ ->
// Responds to when slider's value is changed
model.storyDuration(value.roundToInt())
}
setSquareImageFromURL(View(requireActivity()), model.getPhotoData().value?.get(0)?.imageUri.toString(), binding.postPreview) setSquareImageFromURL(View(requireActivity()), model.getPhotoData().value?.get(0)?.imageUri.toString(), binding.postPreview)
// Get the description and send the post // Get the description and send the post
binding.postCreationSendButton.setOnClickListener { binding.postSubmissionSendButton.setOnClickListener {
if (validatePost()) model.upload() if (validatePost()) model.upload()
} }
@ -179,13 +192,13 @@ class PostSubmissionFragment : BaseFragment() {
} }
private fun enableButton(enable: Boolean = true){ private fun enableButton(enable: Boolean = true){
binding.postCreationSendButton.isEnabled = enable binding.postSubmissionSendButton.isEnabled = enable
if(enable){ if(enable){
binding.postingProgressBar.visibility = View.GONE binding.postingProgressBar.visibility = View.GONE
binding.postCreationSendButton.visibility = View.VISIBLE binding.postSubmissionSendButton.visibility = View.VISIBLE
} else { } else {
binding.postingProgressBar.visibility = View.VISIBLE binding.postingProgressBar.visibility = View.VISIBLE
binding.postCreationSendButton.visibility = View.GONE binding.postSubmissionSendButton.visibility = View.GONE
} }
} }

View File

@ -5,47 +5,51 @@ import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import org.pixeldroid.app.MainActivity import org.pixeldroid.app.MainActivity
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.utils.BaseThemedWithBarActivity import org.pixeldroid.app.databinding.ActivityCameraBinding
import org.pixeldroid.app.postCreation.camera.CameraFragment.Companion.CAMERA_ACTIVITY
import org.pixeldroid.app.postCreation.camera.CameraFragment.Companion.CAMERA_ACTIVITY_STORY
import org.pixeldroid.app.utils.BaseActivity
class CameraActivity : BaseThemedWithBarActivity() { class CameraActivity : BaseActivity() {
private lateinit var binding: ActivityCameraBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_camera)
binding = ActivityCameraBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.topBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setTitle(R.string.add_photo)
val cameraFragment = CameraFragment() val cameraFragment = CameraFragment()
val arguments = Bundle() val story: Boolean = intent.getBooleanExtra(CAMERA_ACTIVITY_STORY, false)
arguments.putBoolean("CameraActivity", true)
cameraFragment.arguments = arguments
supportFragmentManager.beginTransaction() if(story) supportActionBar?.setTitle(R.string.add_story)
.add(R.id.camera_activity_fragment, cameraFragment).commit() else supportActionBar?.setTitle(R.string.add_photo)
}
}
/** // If this CameraActivity wasn't started from the shortcut,
* Launch without arguments so that it will open the // tell the fragment it's in an activity (so that it sends back the result instead of
* [org.pixeldroid.app.postCreation.PostCreationActivity] instead of "returning" to a non-existent // starting a new post creation process)
* [org.pixeldroid.app.postCreation.PostCreationActivity] if (intent.action != Intent.ACTION_VIEW) {
*/ val arguments = Bundle()
class CameraActivityShortcut : BaseThemedWithBarActivity() { arguments.putBoolean(CAMERA_ACTIVITY, true)
override fun onCreate(savedInstanceState: Bundle?) { arguments.putBoolean(CAMERA_ACTIVITY_STORY, story)
super.onCreate(savedInstanceState) cameraFragment.arguments = arguments
setContentView(R.layout.activity_camera) } else {
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setTitle(R.string.new_post_shortcut_long)
supportActionBar?.setTitle(R.string.new_post_shortcut_long) }
val cameraFragment = CameraFragment()
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.add(R.id.camera_activity_fragment, cameraFragment).commit() .add(R.id.camera_activity_fragment, cameraFragment).commit()
} }
//Start a new MainActivity when "going back" on this activity
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
// If this CameraActivity wasn't started from the shortcut, behave as usual
if (intent.action != Intent.ACTION_VIEW) return super.onOptionsItemSelected(item)
// Else, start a new MainActivity when "going back" on this activity
when (item.itemId) { when (item.itemId) {
android.R.id.home -> { android.R.id.home -> {
val intent = Intent(this, MainActivity::class.java) val intent = Intent(this, MainActivity::class.java)

View File

@ -2,7 +2,6 @@ package org.pixeldroid.app.postCreation.camera
import android.Manifest import android.Manifest
import android.app.Activity import android.app.Activity
import android.content.ClipData
import android.content.ContentUris import android.content.ContentUris
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
@ -34,10 +33,8 @@ import androidx.core.view.setPadding
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.FragmentCameraBinding import org.pixeldroid.app.databinding.FragmentCameraBinding
import org.pixeldroid.app.postCreation.PostCreationActivity import org.pixeldroid.app.postCreation.PostCreationActivity
import org.pixeldroid.app.utils.BaseFragment import org.pixeldroid.app.utils.BaseFragment
@ -70,6 +67,7 @@ class CameraFragment : BaseFragment() {
private var camera: Camera? = null private var camera: Camera? = null
private var inActivity by Delegates.notNull<Boolean>() private var inActivity by Delegates.notNull<Boolean>()
private var addToStory by Delegates.notNull<Boolean>()
private var filePermissionDialogLaunched: Boolean = false private var filePermissionDialogLaunched: Boolean = false
@ -89,7 +87,8 @@ class CameraFragment : BaseFragment() {
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
super.onCreateView(inflater, container, savedInstanceState) super.onCreateView(inflater, container, savedInstanceState)
inActivity = arguments?.getBoolean("CameraActivity") ?: false inActivity = arguments?.getBoolean(CAMERA_ACTIVITY) ?: false
addToStory = arguments?.getBoolean(CAMERA_ACTIVITY_STORY) ?: false
binding = FragmentCameraBinding.inflate(layoutInflater) binding = FragmentCameraBinding.inflate(layoutInflater)
@ -106,7 +105,7 @@ class CameraFragment : BaseFragment() {
thumbnail.setPadding(10) thumbnail.setPadding(10)
// Load thumbnail into circular button using Glide // Load thumbnail into circular button using Glide
Glide.with(thumbnail) if(activity?.isDestroyed == false) Glide.with(thumbnail)
.load(uri) .load(uri)
.apply(RequestOptions.circleCropTransform()) .apply(RequestOptions.circleCropTransform())
.into(thumbnail) .into(thumbnail)
@ -326,7 +325,7 @@ class CameraFragment : BaseFragment() {
} }
private fun setupUploadImage() { private fun setupUploadImage() {
val videoEnabled: Boolean = db.instanceDao().getInstance(db.userDao().getActiveUser()!!.instance_uri).videoEnabled val videoEnabled: Boolean = db.instanceDao().getActiveInstance().videoEnabled
var mimeTypes: Array<String> = arrayOf("image/*") var mimeTypes: Array<String> = arrayOf("image/*")
if(videoEnabled) mimeTypes += "video/*" if(videoEnabled) mimeTypes += "video/*"
@ -337,7 +336,8 @@ class CameraFragment : BaseFragment() {
putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes) putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
action = Intent.ACTION_GET_CONTENT action = Intent.ACTION_GET_CONTENT
addCategory(Intent.CATEGORY_OPENABLE) addCategory(Intent.CATEGORY_OPENABLE)
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) // Don't allow multiple for story
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !addToStory)
uploadImageResultContract.launch( uploadImageResultContract.launch(
Intent.createChooser(this, null) Intent.createChooser(this, null)
) )
@ -448,31 +448,22 @@ class CameraFragment : BaseFragment() {
private fun startAlbumCreation(uris: ArrayList<String>) { private fun startAlbumCreation(uris: ArrayList<String>) {
val intent = Intent(requireActivity(), PostCreationActivity::class.java) val intent = PostCreationActivity.intentForUris(requireContext(), uris.map { it.toUri() })
.apply {
uris.forEach{
//Why are we using ClipData here? Because the FLAG_GRANT_READ_URI_PERMISSION
//needs to be applied to the URIs, and this flag only applies to the
//Intent's data and any URIs specified in its ClipData.
if(clipData == null){
clipData = ClipData("", emptyArray(), ClipData.Item(it.toUri()))
} else {
clipData!!.addItem(ClipData.Item(it.toUri()))
}
}
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
if(inActivity){ if(inActivity && !addToStory){
requireActivity().setResult(Activity.RESULT_OK, intent) requireActivity().setResult(Activity.RESULT_OK, intent)
requireActivity().finish() requireActivity().finish()
} else { } else {
if(addToStory){
intent.putExtra(CAMERA_ACTIVITY_STORY, addToStory)
}
startActivity(intent) startActivity(intent)
} }
} }
companion object { companion object {
const val CAMERA_ACTIVITY = "CameraActivity"
const val CAMERA_ACTIVITY_STORY = "CameraActivityStory"
private const val TAG = "CameraFragment" private const val TAG = "CameraFragment"
private const val RATIO_4_3_VALUE = 4.0 / 3.0 private const val RATIO_4_3_VALUE = 4.0 / 3.0

View File

@ -8,6 +8,6 @@ data class CarouselItem constructor(
val video: Boolean, val video: Boolean,
var encodeProgress: Int?, var encodeProgress: Int?,
var stabilizationFirstPass: Boolean?, var stabilizationFirstPass: Boolean?,
var encodeComplete: Boolean = false, var encodeComplete: Boolean? = null,
var encodeError: Boolean = false, var encodeError: Boolean = false,
) )

View File

@ -18,6 +18,9 @@ import androidx.recyclerview.widget.*
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ImageCarouselBinding import org.pixeldroid.app.databinding.ImageCarouselBinding
import me.relex.circleindicator.CircleIndicator2 import me.relex.circleindicator.CircleIndicator2
import org.pixeldroid.common.dpToPx
import org.pixeldroid.common.getSnapPosition
import org.pixeldroid.common.spToPx
class ImageCarousel( class ImageCarousel(
context: Context, context: Context,
@ -40,7 +43,6 @@ class ImageCarousel(
) )
private lateinit var recyclerView: RecyclerView private lateinit var recyclerView: RecyclerView
private lateinit var tvCaption: TextView
private var snapHelper: SnapHelper = PagerSnapHelper() private var snapHelper: SnapHelper = PagerSnapHelper()
var indicator: CircleIndicator2? = null var indicator: CircleIndicator2? = null
@ -107,7 +109,7 @@ class ImageCarousel(
set(value) { set(value) {
field = value field = value
tvCaption.visibility = if (showCaption) View.VISIBLE else View.GONE binding.tvCaption.visibility = if (showCaption) View.VISIBLE else View.GONE
} }
@Dimension(unit = Dimension.PX) @Dimension(unit = Dimension.PX)
@ -115,7 +117,7 @@ class ImageCarousel(
set(value) { set(value) {
field = value field = value
tvCaption.setTextSize(TypedValue.COMPLEX_UNIT_PX, captionTextSize.toFloat()) binding.tvCaption.setTextSize(TypedValue.COMPLEX_UNIT_PX, captionTextSize.toFloat())
} }
var showIndicator = false var showIndicator = false
@ -245,14 +247,14 @@ class ImageCarousel(
showNavigationButtons = showNavigationButtons showNavigationButtons = showNavigationButtons
binding.editMediaDescriptionLayout.visibility = if(editingMediaDescription) VISIBLE else INVISIBLE binding.editMediaDescriptionLayout.visibility = if(editingMediaDescription) VISIBLE else INVISIBLE
tvCaption.visibility = if(editingMediaDescription) INVISIBLE else VISIBLE binding.tvCaption.visibility = if(editingMediaDescription || !showCaption) INVISIBLE else VISIBLE
} else { } else {
recyclerView.layoutManager = GridLayoutManager(context, 3) recyclerView.layoutManager = GridLayoutManager(context, 3)
binding.btnNext.visibility = GONE binding.btnNext.visibility = GONE
binding.btnPrevious.visibility = GONE binding.btnPrevious.visibility = GONE
binding.editMediaDescriptionLayout.visibility = INVISIBLE binding.editMediaDescriptionLayout.visibility = INVISIBLE
tvCaption.visibility = INVISIBLE binding.tvCaption.visibility = INVISIBLE
} }
showIndicator = value showIndicator = value
@ -279,8 +281,7 @@ class ImageCarousel(
updateDescriptionCallback?.invoke(currentPosition, description) updateDescriptionCallback?.invoke(currentPosition, description)
} }
binding.editMediaDescriptionLayout.visibility = if(value) VISIBLE else INVISIBLE binding.editMediaDescriptionLayout.visibility = if(value) VISIBLE else INVISIBLE
tvCaption.visibility = if(value) INVISIBLE else VISIBLE binding.tvCaption.visibility = if(value || !showCaption) INVISIBLE else VISIBLE
} }
} }
@ -289,10 +290,10 @@ class ImageCarousel(
set(value) { set(value) {
if(!value.isNullOrEmpty()) { if(!value.isNullOrEmpty()) {
field = value field = value
tvCaption.text = value binding.tvCaption.text = value
} else { } else {
field = null field = null
tvCaption.text = context.getText(R.string.no_media_description) binding.tvCaption.text = context.getText(R.string.no_media_description)
} }
} }
@ -317,12 +318,11 @@ class ImageCarousel(
binding = ImageCarouselBinding.inflate(LayoutInflater.from(context),this, true) binding = ImageCarouselBinding.inflate(LayoutInflater.from(context),this, true)
recyclerView = binding.recyclerView recyclerView = binding.recyclerView
tvCaption = binding.tvCaption
recyclerView.setHasFixedSize(true) recyclerView.setHasFixedSize(true)
// For marquee effect // For marquee effect
tvCaption.isSelected = true binding.tvCaption.isSelected = true
} }
@ -441,7 +441,7 @@ class ImageCarousel(
caption.apply { caption.apply {
if(layoutCarousel){ if(layoutCarousel){
binding.editMediaDescriptionLayout.visibility = INVISIBLE binding.editMediaDescriptionLayout.visibility = INVISIBLE
tvCaption.visibility = VISIBLE showCaption = true
} }
currentDescription = this currentDescription = this
} }
@ -472,7 +472,7 @@ class ImageCarousel(
} }
}) })
tvCaption.setOnClickListener { binding.tvCaption.setOnClickListener {
editingMediaDescription = true editingMediaDescription = true
} }
@ -562,7 +562,7 @@ class ImageCarousel(
binding.encodeInfoText.setText(R.string.encode_error) binding.encodeInfoText.setText(R.string.encode_error)
binding.encodeInfoText.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.error), binding.encodeInfoText.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.error),
null, null, null) null, null, null)
} else if(it.encodeComplete){ } else if(it.encodeComplete == true){
binding.encodeInfoCard.visibility = VISIBLE binding.encodeInfoCard.visibility = VISIBLE
binding.encodeProgress.visibility = GONE binding.encodeProgress.visibility = GONE
binding.encodeInfoText.setText(R.string.encode_success) binding.encodeInfoText.setText(R.string.encode_success)

View File

@ -1,52 +0,0 @@
package org.pixeldroid.app.postCreation.carousel
import android.content.Context
import android.util.DisplayMetrics
import android.util.TypedValue
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SnapHelper
/**
* This method converts device specific pixels to density independent pixels.
*/
fun Int.pxToDp(context: Context): Int {
return (this / (context.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)).toInt()
}
/**
* This method converts dp unit to equivalent pixels, depending on device density.
*/
fun Int.dpToPx(context: Context): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
this.toFloat(),
context.resources.displayMetrics
).toInt()
}
/**
* This method converts sp unit to equivalent pixels, depending on device density.
*/
fun Int.spToPx(context: Context): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP,
this.toFloat(),
context.resources.displayMetrics
).toInt()
}
/**
* Get current snap item position of a recyclerView.
*
* @param layoutManager Target recyclerView
* @return Position of the item or RecyclerView.NO_POSITION (-1)
*/
fun SnapHelper.getSnapPosition(layoutManager: RecyclerView.LayoutManager?): Int {
if (layoutManager == null) {
return RecyclerView.NO_POSITION
}
val snapView: View = this.findSnapView(layoutManager) ?: return RecyclerView.NO_POSITION
return layoutManager.getPosition(snapView)
}

View File

@ -1,39 +1,113 @@
package org.pixeldroid.app.posts package org.pixeldroid.app.posts
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem
import android.view.View import android.view.View
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.viewpager2.widget.ViewPager2
import kotlinx.coroutines.launch
import org.pixeldroid.app.databinding.ActivityAlbumBinding import org.pixeldroid.app.databinding.ActivityAlbumBinding
import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.objects.Attachment
class AlbumActivity : BaseActivity() {
class AlbumActivity : AppCompatActivity() {
private val model: AlbumViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val binding = ActivityAlbumBinding.inflate(layoutInflater)
val binding = ActivityAlbumBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
val mediaAttachments = intent.getSerializableExtra("images") as ArrayList<Attachment>
val index = intent.getIntExtra("index", 0) binding.albumPager.adapter = AlbumViewPagerAdapter(
binding.albumPager.adapter = AlbumViewPagerAdapter(mediaAttachments, model.uiState.value.mediaAttachments,
sensitive = false, sensitive = false,
opened = true, opened = true,
//In the activity, we assume we want to show everything //In the activity, we assume we want to show everything
alwaysShowNsfw = true alwaysShowNsfw = true,
clickCallback = ::clickCallback
) )
binding.albumPager.currentItem = index
if(mediaAttachments.size == 1){ binding.albumPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) { model.positionSelected(position) }
})
if (model.uiState.value.mediaAttachments.size == 1) {
binding.albumPager.isUserInputEnabled = false binding.albumPager.isUserInputEnabled = false
} } else if ((model.uiState.value.mediaAttachments.size) > 1) {
else if((mediaAttachments.size) > 1) {
binding.postIndicator.setViewPager(binding.albumPager) binding.postIndicator.setViewPager(binding.albumPager)
binding.postIndicator.visibility = View.VISIBLE binding.postIndicator.visibility = View.VISIBLE
} else { } else {
binding.postIndicator.visibility = View.GONE binding.postIndicator.visibility = View.GONE
} }
// Not really necessary because the ViewPager saves its state in onSaveInstanceState, but
// it's good to stay consistent in case something gets out of sync
binding.albumPager.setCurrentItem(model.uiState.value.index, false)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowTitleEnabled(false) supportActionBar?.setDisplayShowTitleEnabled(false)
supportActionBar?.setBackgroundDrawable(null) supportActionBar?.setBackgroundDrawable(null)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
model.uiState.collect { uiState ->
binding.albumPager.currentItem = uiState.index
}
}
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
model.isActionBarHidden.collect { isActionBarHidden ->
val windowInsetsController =
WindowCompat.getInsetsController(this@AlbumActivity.window, binding.albumPager)
if (isActionBarHidden) {
// Configure the behavior of the hidden system bars
windowInsetsController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
// Hide both the status bar and the navigation bar
supportActionBar?.hide()
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
binding.postIndicator.visibility = View.GONE
} else {
// Configure the behavior of the hidden system bars
windowInsetsController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
// Show both the status bar and the navigation bar
supportActionBar?.show()
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
if ((model.uiState.value.mediaAttachments.size) > 1) {
binding.postIndicator.visibility = View.VISIBLE
}
}
}
}
}
}
/**
* Callback passed to the AlbumViewPagerAdapter to signal a single click on the image
*/
private fun clickCallback(){
model.barHide()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
// Handle up arrow manually,
// since "up" isn't defined for this activity
onBackPressedDispatcher.onBackPressed()
true
}
else -> super.onOptionsItemSelected(item)
}
} }
} }

View File

@ -0,0 +1,46 @@
package org.pixeldroid.app.posts
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import org.pixeldroid.app.utils.api.objects.Attachment
import javax.inject.Inject
data class AlbumUiState(
val mediaAttachments: ArrayList<Attachment> = arrayListOf(),
val index: Int = 0,
)
@HiltViewModel
class AlbumViewModel @Inject constructor(state: SavedStateHandle) : ViewModel() {
companion object {
const val ALBUM_IMAGES = "AlbumViewImages"
const val ALBUM_INDEX = "AlbumViewIndex"
}
private val _uiState: MutableStateFlow<AlbumUiState>
private val _isActionBarHidden: MutableStateFlow<Boolean>
init {
_uiState = MutableStateFlow(AlbumUiState(
mediaAttachments = state[ALBUM_IMAGES] ?: ArrayList(),
index = state[ALBUM_INDEX] ?: 0
))
_isActionBarHidden = MutableStateFlow(false)
}
val uiState: StateFlow<AlbumUiState> = _uiState.asStateFlow()
val isActionBarHidden: StateFlow<Boolean> = _isActionBarHidden
fun barHide() {
_isActionBarHidden.update { !it }
}
fun positionSelected(position: Int) {
_uiState.update { it.copy(index = position) }
}
}

View File

@ -11,6 +11,7 @@ import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.core.text.toSpanned import androidx.core.text.toSpanned
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import kotlinx.coroutines.launch
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.utils.api.PixelfedAPI import org.pixeldroid.app.utils.api.PixelfedAPI
import org.pixeldroid.app.utils.api.objects.Account.Companion.openAccountFromId import org.pixeldroid.app.utils.api.objects.Account.Companion.openAccountFromId
@ -106,7 +107,7 @@ fun parseHTMLText(
override fun onClick(widget: View) { override fun onClick(widget: View) {
// Retrieve the account for the given profile // Retrieve the account for the given profile
lifecycleScope.launchWhenCreated { lifecycleScope.launch {
val api: PixelfedAPI = apiHolder.api ?: apiHolder.setToCurrentUser() val api: PixelfedAPI = apiHolder.api ?: apiHolder.setToCurrentUser()
openAccountFromId(accountId, api, context) openAccountFromId(accountId, api, context)
} }
@ -130,7 +131,7 @@ fun parseHTMLText(
} }
fun setTextViewFromISO8601(date: Instant, textView: TextView, absoluteTime: Boolean, context: Context) { fun setTextViewFromISO8601(date: Instant, textView: TextView, absoluteTime: Boolean) {
val now = Date.from(Instant.now()).time val now = Date.from(Instant.now()).time
try { try {
@ -140,7 +141,7 @@ fun setTextViewFromISO8601(date: Instant, textView: TextView, absoluteTime: Bool
android.text.format.DateUtils.SECOND_IN_MILLIS, android.text.format.DateUtils.SECOND_IN_MILLIS,
android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE).toString() android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE).toString()
textView.text = if(absoluteTime) context.getString(R.string.posted_on).format(date) textView.text = if(absoluteTime) textView.context.getString(R.string.posted_on).format(date)
else formattedDate else formattedDate
} catch (e: ParseException) { } catch (e: ParseException) {

View File

@ -14,9 +14,9 @@ import androidx.media2.common.MediaMetadata
import androidx.media2.common.UriMediaItem import androidx.media2.common.UriMediaItem
import androidx.media2.player.MediaPlayer import androidx.media2.player.MediaPlayer
import org.pixeldroid.app.databinding.ActivityMediaviewerBinding import org.pixeldroid.app.databinding.ActivityMediaviewerBinding
import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity import org.pixeldroid.app.utils.BaseActivity
class MediaViewerActivity : BaseThemedWithoutBarActivity() { class MediaViewerActivity : BaseActivity() {
private lateinit var mediaPlayer: MediaPlayer private lateinit var mediaPlayer: MediaPlayer
private lateinit var binding: ActivityMediaviewerBinding private lateinit var binding: ActivityMediaviewerBinding

View File

@ -88,19 +88,20 @@ class NestedScrollableHost(context: Context, attrs: AttributeSet? = null) :
} }
val intent = Intent(context, AlbumActivity::class.java) val intent = Intent(context, AlbumActivity::class.java)
intent.putExtra("images", images) intent.putExtra(AlbumViewModel.ALBUM_IMAGES, images)
intent.putExtra("index", (child as ViewPager2).currentItem) intent.putExtra(AlbumViewModel.ALBUM_INDEX, (child as ViewPager2).currentItem)
context.startActivity(intent) context.startActivity(intent)
return super.onSingleTapConfirmed(e) return super.onSingleTapConfirmed(e)
} }
override fun onScroll( override fun onScroll(
e1: MotionEvent, e1: MotionEvent?,
e2: MotionEvent, e2: MotionEvent,
distanceX: Float, distanceX: Float,
distanceY: Float distanceY: Float
): Boolean { ): Boolean {
if (e1 == null) return false
val orientation = parentViewPager?.orientation ?: return true val orientation = parentViewPager?.orientation ?: return true
val dx = e2.x - e1.x val dx = e2.x - e1.x

View File

@ -5,13 +5,16 @@ import android.util.Log
import android.view.View import android.view.View
import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE import android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityPostBinding import org.pixeldroid.app.databinding.ActivityPostBinding
import org.pixeldroid.app.posts.feeds.uncachedFeeds.comments.CommentFragment import org.pixeldroid.app.posts.feeds.uncachedFeeds.comments.CommentFragment
import org.pixeldroid.app.posts.feeds.uncachedFeeds.comments.CommentFragment.Companion.COMMENT_DOMAIN import org.pixeldroid.app.posts.feeds.uncachedFeeds.comments.CommentFragment.Companion.COMMENT_DOMAIN
import org.pixeldroid.app.posts.feeds.uncachedFeeds.comments.CommentFragment.Companion.COMMENT_STATUS_ID import org.pixeldroid.app.posts.feeds.uncachedFeeds.comments.CommentFragment.Companion.COMMENT_STATUS_ID
import org.pixeldroid.app.utils.BaseThemedWithBarActivity import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.PixelfedAPI import org.pixeldroid.app.utils.api.PixelfedAPI
import org.pixeldroid.app.utils.api.objects.Status import org.pixeldroid.app.utils.api.objects.Status
import org.pixeldroid.app.utils.api.objects.Status.Companion.POST_COMMENT_TAG import org.pixeldroid.app.utils.api.objects.Status.Companion.POST_COMMENT_TAG
@ -19,18 +22,21 @@ import org.pixeldroid.app.utils.api.objects.Status.Companion.POST_TAG
import org.pixeldroid.app.utils.api.objects.Status.Companion.VIEW_COMMENTS_TAG import org.pixeldroid.app.utils.api.objects.Status.Companion.VIEW_COMMENTS_TAG
import org.pixeldroid.app.utils.displayDimensionsInPx import org.pixeldroid.app.utils.displayDimensionsInPx
class PostActivity : BaseThemedWithBarActivity() { class PostActivity : BaseActivity() {
private lateinit var binding: ActivityPostBinding private lateinit var binding: ActivityPostBinding
private var commentFragment = CommentFragment() private lateinit var commentFragment: CommentFragment
private lateinit var status: Status private lateinit var status: Status
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityPostBinding.inflate(layoutInflater) binding = ActivityPostBinding.inflate(layoutInflater)
setContentView(binding.root)
commentFragment = CommentFragment(binding.swipeRefreshLayout)
setContentView(binding.root)
setSupportActionBar(binding.topBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
status = intent.getSerializableExtra(POST_TAG) as Status status = intent.getSerializableExtra(POST_TAG) as Status
@ -43,7 +49,10 @@ class PostActivity : BaseThemedWithBarActivity() {
val holder = StatusViewHolder(binding.postFragmentSingle) val holder = StatusViewHolder(binding.postFragmentSingle)
holder.bind(status, apiHolder, db, lifecycleScope, displayDimensionsInPx(), isActivity = true) holder.bind(
status, apiHolder, db, lifecycleScope, displayDimensionsInPx(),
requestPermissionDownloadPic, isActivity = true
)
activateCommenter() activateCommenter()
initCommentsFragment(domain = user?.instance_uri.orEmpty()) initCommentsFragment(domain = user?.instance_uri.orEmpty())
@ -60,6 +69,17 @@ class PostActivity : BaseThemedWithBarActivity() {
} }
} }
private val requestPermissionDownloadPic =
registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (!isGranted) {
MaterialAlertDialogBuilder(this)
.setMessage(R.string.write_permission_download_pic)
.setNegativeButton(android.R.string.ok) { _, _ -> }
.show()
}
}
private fun activateCommenter() { private fun activateCommenter() {
//Activate commenter //Activate commenter
binding.submitComment.setOnClickListener { binding.submitComment.setOnClickListener {
@ -89,6 +109,11 @@ class PostActivity : BaseThemedWithBarActivity() {
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.add(R.id.commentFragment, commentFragment).commit() .add(R.id.commentFragment, commentFragment).commit()
binding.swipeRefreshLayout.setOnRefreshListener {
commentFragment.adapter.refresh()
commentFragment.adapter.notifyDataSetChanged()
}
} }
private suspend fun postComment( private suspend fun postComment(

View File

@ -5,10 +5,10 @@ import android.view.View
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityReportBinding import org.pixeldroid.app.databinding.ActivityReportBinding
import org.pixeldroid.app.utils.BaseThemedWithBarActivity import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.objects.Status import org.pixeldroid.app.utils.api.objects.Status
class ReportActivity : BaseThemedWithBarActivity() { class ReportActivity : BaseActivity() {
private lateinit var binding: ActivityReportBinding private lateinit var binding: ActivityReportBinding
@ -16,9 +16,9 @@ class ReportActivity : BaseThemedWithBarActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityReportBinding.inflate(layoutInflater) binding = ActivityReportBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
setSupportActionBar(binding.topBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setTitle(R.string.report)
val status = intent.getSerializableExtra(Status.POST_TAG) as Status? val status = intent.getSerializableExtra(Status.POST_TAG) as Status?

View File

@ -1,14 +1,13 @@
package org.pixeldroid.app.posts package org.pixeldroid.app.posts
import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.content.ClipData
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager.PERMISSION_DENIED
import android.graphics.Typeface import android.graphics.Typeface
import android.graphics.drawable.AnimatedVectorDrawable import android.graphics.drawable.AnimatedVectorDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Looper import android.os.Looper
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.util.Log import android.util.Log
@ -17,11 +16,8 @@ import android.view.Menu
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.* import android.widget.*
import androidx.appcompat.app.AppCompatActivity import androidx.activity.result.ActivityResultLauncher
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -36,10 +32,6 @@ import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.karumi.dexter.Dexter
import com.karumi.dexter.listener.PermissionDeniedResponse
import com.karumi.dexter.listener.PermissionGrantedResponse
import com.karumi.dexter.listener.single.BasePermissionListener
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.* import okhttp3.*
import okio.BufferedSink import okio.BufferedSink
@ -75,7 +67,11 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
private var status: Status? = null private var status: Status? = null
fun bind(status: Status?, pixelfedAPI: PixelfedAPIHolder, db: AppDatabase, lifecycleScope: LifecycleCoroutineScope, displayDimensionsInPx: Pair<Int, Int>, isActivity: Boolean = false) { fun bind(
status: Status?, pixelfedAPI: PixelfedAPIHolder, db: AppDatabase,
lifecycleScope: LifecycleCoroutineScope, displayDimensionsInPx: Pair<Int, Int>,
requestPermissionDownloadPic: ActivityResultLauncher<String>, isActivity: Boolean = false,
) {
this.itemView.visibility = View.VISIBLE this.itemView.visibility = View.VISIBLE
this.status = status this.status = status
@ -104,7 +100,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
setupPost(picRequest, user.instance_uri, isActivity) setupPost(picRequest, user.instance_uri, isActivity)
activateButtons(pixelfedAPI, db, lifecycleScope, isActivity) activateButtons(pixelfedAPI, db, lifecycleScope, isActivity, requestPermissionDownloadPic)
} }
@ -139,8 +135,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
setTextViewFromISO8601( setTextViewFromISO8601(
status?.created_at!!, status?.created_at!!,
binding.postDate, binding.postDate,
isActivity, isActivity
binding.root.context
) )
binding.postDomain.text = status?.getStatusDomain(domain, binding.postDomain.context) binding.postDomain.text = status?.getStatusDomain(domain, binding.postDomain.context)
@ -233,6 +228,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
db: AppDatabase, db: AppDatabase,
lifecycleScope: LifecycleCoroutineScope, lifecycleScope: LifecycleCoroutineScope,
isActivity: Boolean, isActivity: Boolean,
requestPermissionDownloadPic: ActivityResultLauncher<String>,
){ ){
//Set the special HTML text //Set the special HTML text
setDescription(apiHolder, lifecycleScope) setDescription(apiHolder, lifecycleScope)
@ -262,7 +258,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
showComments(lifecycleScope, isActivity) showComments(lifecycleScope, isActivity)
activateMoreButton(apiHolder, db, lifecycleScope) activateMoreButton(apiHolder, db, lifecycleScope, requestPermissionDownloadPic)
} }
private fun activateReblogger( private fun activateReblogger(
@ -364,7 +360,12 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
return null return null
} }
private fun activateMoreButton(apiHolder: PixelfedAPIHolder, db: AppDatabase, lifecycleScope: LifecycleCoroutineScope){ private fun activateMoreButton(
apiHolder: PixelfedAPIHolder,
db: AppDatabase,
lifecycleScope: LifecycleCoroutineScope,
requestPermissionDownloadPic: ActivityResultLauncher<String>,
){
var bookmarked: Boolean? = null var bookmarked: Boolean? = null
binding.statusMore.setOnClickListener { binding.statusMore.setOnClickListener {
PopupMenu(it.context, it).apply { PopupMenu(it.context, it).apply {
@ -402,50 +403,29 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
true true
} }
R.id.post_more_menu_save_to_gallery -> { R.id.post_more_menu_save_to_gallery -> {
Dexter.withContext(binding.root.context) // Check permissions on old Android versions: on new versions it is not
.withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) // needed when storing a file.
.withListener(object : BasePermissionListener() { if(Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission(binding.root.context, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) == PERMISSION_DENIED) {
override fun onPermissionDenied(p0: PermissionDeniedResponse?) { requestPermissionDownloadPic.launch(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
Toast.makeText( } else {
binding.root.context, status?.downloadImage(
binding.root.context.getString(R.string.write_permission_download_pic), binding.root.context,
Toast.LENGTH_SHORT status?.media_attachments?.getOrNull(binding.postPager.currentItem)?.url
).show() ?: "",
} binding.root
)
override fun onPermissionGranted(p0: PermissionGrantedResponse?) { }
status?.downloadImage(
binding.root.context,
status?.media_attachments?.getOrNull(binding.postPager.currentItem)?.url
?: "",
binding.root
)
}
}).check()
true true
} }
R.id.post_more_menu_share_picture -> {
Dexter.withContext(binding.root.context)
.withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.withListener(object : BasePermissionListener() {
override fun onPermissionDenied(p0: PermissionDeniedResponse?) {
Toast.makeText(
binding.root.context,
binding.root.context.getString(R.string.write_permission_share_pic),
Toast.LENGTH_SHORT
).show()
}
override fun onPermissionGranted(p0: PermissionGrantedResponse?) { R.id.post_more_menu_share_picture -> {
status?.downloadImage( status?.downloadImage(
binding.root.context, binding.root.context,
status?.media_attachments?.getOrNull(binding.postPager.currentItem)?.url status?.media_attachments?.getOrNull(binding.postPager.currentItem)?.url
?: "", ?: "",
binding.root, binding.root,
share = true, share = true,
) )
}
}).check()
true true
} }
R.id.post_more_menu_delete -> { R.id.post_more_menu_delete -> {
@ -462,178 +442,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
true true
} }
R.id.post_more_menu_redraft -> { R.id.post_more_menu_redraft -> launchRedraftDialog(lifecycleScope, apiHolder, db)
MaterialAlertDialogBuilder(binding.root.context).apply {
setMessage(R.string.redraft_dialog_launch)
setPositiveButton(android.R.string.ok) { _, _ ->
lifecycleScope.launch {
try {
// Create new post creation activity
val intent =
Intent(context, PostCreationActivity::class.java)
// Get descriptions and images from original post
val postDescription = status?.content ?: ""
val postAttachments =
status?.media_attachments!! // Catch possible exception from !! (?)
val postNSFW = status?.sensitive
val imageUriStrings = postAttachments.map { postAttachment ->
postAttachment.url ?: ""
}
val imageNames = imageUriStrings.map { imageUriString ->
Uri.parse(imageUriString).lastPathSegment.toString()
}
val downloadedFiles = imageNames.map { imageName ->
File(context.cacheDir, imageName)
}
val imageUris = downloadedFiles.map { downloadedFile ->
Uri.fromFile(downloadedFile)
}
val imageDescriptions = postAttachments.map { postAttachment ->
fromHtml(postAttachment.description ?: "").toString()
}
val downloadRequests: List<Request> = imageUriStrings.map { imageUriString ->
Request.Builder().url(imageUriString).build()
}
val counter = AtomicInteger(0)
// Define callback function for after downloading the images
fun continuation() {
// Wait for all outstanding downloads to finish
if (counter.incrementAndGet() == imageUris.size) {
if (allFilesExist(imageNames)) {
// Delete original post
lifecycleScope.launch {
deletePost(apiHolder.api ?: apiHolder.setToCurrentUser(), db)
}
val counterInt = counter.get()
Toast.makeText(
binding.root.context,
binding.root.context.resources.getQuantityString(
R.plurals.items_load_success,
counterInt,
counterInt
),
Toast.LENGTH_SHORT
).show()
// Pass downloaded images to new post creation activity
intent.apply {
imageUris.zip(imageDescriptions).map { (imageUri, imageDescription) ->
ClipData.Item(imageDescription, null, imageUri)
}.forEach { imageItem ->
if (clipData == null) {
clipData = ClipData(
"",
emptyArray(),
imageItem
)
} else {
clipData!!.addItem(imageItem)
}
}
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
// Pass post description of existing post to new post creation activity
intent.putExtra(
PostCreationActivity.PICTURE_DESCRIPTION,
fromHtml(postDescription).toString()
)
if (imageNames.isNotEmpty()) {
intent.putExtra(
PostCreationActivity.TEMP_FILES,
imageNames.toTypedArray()
)
}
intent.putExtra(
PostCreationActivity.POST_REDRAFT,
true
)
intent.putExtra(
PostCreationActivity.POST_NSFW,
postNSFW
)
// Launch post creation activity
binding.root.context.startActivity(intent)
}
}
}
if (!allFilesExist(imageNames)) {
// Track download progress
Toast.makeText(
binding.root.context,
binding.root.context.getString(R.string.image_download_downloading),
Toast.LENGTH_SHORT
).show()
}
// Iterate through all pictures of the original post
downloadRequests.zip(downloadedFiles).forEach { (downloadRequest, downloadedFile) ->
// Check whether image is in cache directory already (maybe rather do so using Glide in the future?)
if (!downloadedFile.exists()) {
OkHttpClient().newCall(downloadRequest)
.enqueue(object : Callback {
override fun onFailure(
call: Call,
e: IOException
) {
Looper.prepare()
downloadedFile.delete()
Toast.makeText(
binding.root.context,
binding.root.context.getString(R.string.redraft_post_failed_io_except),
Toast.LENGTH_SHORT
).show()
}
@Throws(IOException::class)
override fun onResponse(
call: Call,
response: Response
) {
val sink: BufferedSink =
downloadedFile.sink().buffer()
sink.writeAll(response.body!!.source())
sink.close()
Looper.prepare()
continuation()
}
})
} else {
continuation()
}
}
} catch (exception: HttpException) {
Toast.makeText(
binding.root.context,
binding.root.context.getString(
R.string.redraft_post_failed_error,
exception.code()
),
Toast.LENGTH_SHORT
).show()
} catch (exception: IOException) {
Toast.makeText(
binding.root.context,
binding.root.context.getString(R.string.redraft_post_failed_io_except),
Toast.LENGTH_SHORT
).show()
}
}
}
setNegativeButton(android.R.string.cancel) { _, _ -> }
show()
}
true
}
else -> false else -> false
} }
} }
@ -658,6 +467,165 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
} }
} }
private fun launchRedraftDialog(
lifecycleScope: LifecycleCoroutineScope,
apiHolder: PixelfedAPIHolder,
db: AppDatabase
): Boolean {
MaterialAlertDialogBuilder(binding.root.context).apply {
setMessage(R.string.redraft_dialog_launch)
setPositiveButton(android.R.string.ok) { _, _ ->
lifecycleScope.launch {
try {
// Get descriptions and images from original post
val postDescription = status?.content ?: ""
val postAttachments =
status?.media_attachments!! // TODO Catch possible exception from !! (?)
val postNSFW = status?.sensitive
val imageUriStrings = postAttachments.map { postAttachment ->
postAttachment.url ?: ""
}
val imageNames = imageUriStrings.map { imageUriString ->
Uri.parse(imageUriString).lastPathSegment.toString()
}
val downloadedFiles = imageNames.map { imageName ->
File(context.cacheDir, imageName)
}
val imageDescriptions = postAttachments.map { postAttachment ->
fromHtml(
postAttachment.description ?: ""
).toString()
}
val downloadRequests: List<Request> =
imageUriStrings.map { imageUriString ->
Request.Builder().url(imageUriString).build()
}
val imageUris = downloadedFiles.map { downloadedFile ->
Uri.fromFile(downloadedFile)
}
val counter = AtomicInteger(0)
// Define callback function for after downloading the images
fun continuation() {
// Wait for all outstanding downloads to finish
if (counter.incrementAndGet() == imageUris.size) {
if (allFilesExist(imageNames)) {
// Delete original post
lifecycleScope.launch {
deletePost(
apiHolder.api ?: apiHolder.setToCurrentUser(), db
)
}
val counterInt = counter.get()
Toast.makeText(
binding.root.context,
binding.root.context.resources.getQuantityString(
R.plurals.items_load_success, counterInt, counterInt
),
Toast.LENGTH_SHORT
).show()
// Create new post creation activity
val intent = PostCreationActivity.intentForUris(context, imageUris).apply {
putExtra(
PostCreationActivity.PICTURE_DESCRIPTIONS,
ArrayList(imageDescriptions)
)
// Pass post description of existing post to new post creation activity
putExtra(
PostCreationActivity.POST_DESCRIPTION,
fromHtml(postDescription).toString()
)
if (imageNames.isNotEmpty()) {
putExtra(
PostCreationActivity.TEMP_FILES,
imageNames.toTypedArray()
)
}
putExtra(PostCreationActivity.POST_REDRAFT, true)
putExtra(PostCreationActivity.POST_NSFW, postNSFW)
}
// Launch post creation activity
binding.root.context.startActivity(intent)
}
}
}
if (!allFilesExist(imageNames)) {
// Track download progress
Toast.makeText(
binding.root.context,
binding.root.context.getString(R.string.image_download_downloading),
Toast.LENGTH_SHORT
).show()
}
// Iterate through all pictures of the original post
downloadRequests.zip(downloadedFiles)
.forEach { (downloadRequest, downloadedFile) ->
// Check whether image is in cache directory already (maybe rather do so using Glide in the future?)
if (!downloadedFile.exists()) {
OkHttpClient().newCall(downloadRequest)
.enqueue(object : Callback {
override fun onFailure(
call: Call,
e: IOException,
) {
Looper.prepare()
downloadedFile.delete()
Toast.makeText(
binding.root.context,
binding.root.context.getString(
R.string.redraft_post_failed_io_except
),
Toast.LENGTH_SHORT
).show()
}
@Throws(IOException::class)
override fun onResponse(
call: Call,
response: Response,
) {
val sink: BufferedSink =
downloadedFile.sink().buffer()
sink.writeAll(response.body!!.source())
sink.close()
Looper.prepare()
continuation()
}
})
} else {
continuation()
}
}
} catch (exception: HttpException) {
Toast.makeText(
binding.root.context, binding.root.context.getString(
R.string.redraft_post_failed_error, exception.code()
), Toast.LENGTH_SHORT
).show()
} catch (exception: IOException) {
Toast.makeText(
binding.root.context,
binding.root.context.getString(R.string.redraft_post_failed_io_except),
Toast.LENGTH_SHORT
).show()
}
}
}
setNegativeButton(android.R.string.cancel) { _, _ -> }
show()
}
return true
}
private fun activateLiker( private fun activateLiker(
apiHolder: PixelfedAPIHolder, apiHolder: PixelfedAPIHolder,
isLiked: Boolean, isLiked: Boolean,
@ -833,17 +801,15 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold
class AlbumViewPagerAdapter( class AlbumViewPagerAdapter(
private val media_attachments: List<Attachment>, private var sensitive: Boolean?, private val media_attachments: List<Attachment>, private var sensitive: Boolean?,
private val opened: Boolean, private val alwaysShowNsfw: Boolean, private val opened: Boolean, private val alwaysShowNsfw: Boolean,
) : private val clickCallback: (() -> Unit)? = null
RecyclerView.Adapter<AlbumViewPagerAdapter.ViewHolder>() { ) : RecyclerView.Adapter<AlbumViewPagerAdapter.ViewHolder>() {
private var isActionBarHidden: Boolean = false
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return if(!opened) ViewHolderClosed(AlbumImageViewBinding.inflate( return if(!opened) ViewHolderClosed(AlbumImageViewBinding.inflate(
LayoutInflater.from(parent.context), parent, false LayoutInflater.from(parent.context), parent, false
)) else ViewHolderOpen(OpenedAlbumBinding.inflate( )) else ViewHolderOpen(OpenedAlbumBinding.inflate(
LayoutInflater.from(parent.context), parent, false LayoutInflater.from(parent.context), parent, false
)) ), clickCallback!!)
} }
override fun getItemCount() = media_attachments.size override fun getItemCount() = media_attachments.size
@ -874,24 +840,6 @@ class AlbumViewPagerAdapter(
setDoubleTapZoomDpi(240) setDoubleTapZoomDpi(240)
resetScaleAndCenter() resetScaleAndCenter()
} }
holder.image.setOnClickListener {
val windowInsetsController = WindowCompat.getInsetsController((it.context as Activity).window, it)
// Configure the behavior of the hidden system bars
if (isActionBarHidden) {
windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
// Hide both the status bar and the navigation bar
(it.context as AppCompatActivity).supportActionBar?.show()
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
isActionBarHidden = false
} else {
// Configure the behavior of the hidden system bars
windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
// Hide both the status bar and the navigation bar
(it.context as AppCompatActivity).supportActionBar?.hide()
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
isActionBarHidden = true
}
}
} }
else Glide.with(holder.binding.root) else Glide.with(holder.binding.root)
.asDrawable().fitCenter() .asDrawable().fitCenter()
@ -937,9 +885,13 @@ class AlbumViewPagerAdapter(
abstract val videoPlayButton: ImageView abstract val videoPlayButton: ImageView
} }
class ViewHolderOpen(override val binding: OpenedAlbumBinding) : ViewHolder(binding) { class ViewHolderOpen(override val binding: OpenedAlbumBinding, clickCallback: () -> Unit) : ViewHolder(binding) {
override val image: SubsamplingScaleImageView = binding.imageImageView override val image: SubsamplingScaleImageView = binding.imageImageView
override val videoPlayButton: ImageView = binding.videoPlayButton override val videoPlayButton: ImageView = binding.videoPlayButton
init {
image.setOnClickListener { clickCallback() }
}
} }
class ViewHolderClosed(override val binding: AlbumImageViewBinding) : ViewHolder(binding) { class ViewHolderClosed(override val binding: AlbumImageViewBinding) : ViewHolder(binding) {
override val image: ImageView = binding.imageImageView override val image: ImageView = binding.imageImageView

View File

@ -6,13 +6,16 @@ import android.widget.ProgressBar
import androidx.constraintlayout.motion.widget.MotionLayout import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import androidx.paging.CombinedLoadStates
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.paging.LoadStateAdapter import androidx.paging.LoadStateAdapter
import androidx.paging.PagingDataAdapter import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -20,6 +23,7 @@ import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ErrorLayoutBinding import org.pixeldroid.app.databinding.ErrorLayoutBinding
import org.pixeldroid.app.databinding.LoadStateFooterViewItemBinding import org.pixeldroid.app.databinding.LoadStateFooterViewItemBinding
import org.pixeldroid.app.posts.feeds.uncachedFeeds.FeedViewModel import org.pixeldroid.app.posts.feeds.uncachedFeeds.FeedViewModel
import org.pixeldroid.app.stories.StoriesAdapter
import org.pixeldroid.app.utils.api.objects.FeedContent import org.pixeldroid.app.utils.api.objects.FeedContent
import org.pixeldroid.app.utils.api.objects.Status import org.pixeldroid.app.utils.api.objects.Status
import retrofit2.HttpException import retrofit2.HttpException
@ -48,14 +52,29 @@ private fun showError(
internal fun <T: Any> initAdapter( internal fun <T: Any> initAdapter(
progressBar: ProgressBar, swipeRefreshLayout: SwipeRefreshLayout, progressBar: ProgressBar, swipeRefreshLayout: SwipeRefreshLayout,
recyclerView: RecyclerView, motionLayout: MotionLayout, errorLayout: ErrorLayoutBinding, recyclerView: RecyclerView, motionLayout: MotionLayout, errorLayout: ErrorLayoutBinding,
adapter: PagingDataAdapter<T, RecyclerView.ViewHolder>) { adapter: PagingDataAdapter<T, RecyclerView.ViewHolder>,
header: StoriesAdapter? = null
) {
recyclerView.adapter = adapter.withLoadStateFooter(
footer = ReposLoadStateAdapter { adapter.retry() } val footer = ReposLoadStateAdapter { adapter.retry() }
adapter.addLoadStateListener { loadStates: CombinedLoadStates ->
footer.loadState = loadStates.append
}
recyclerView.adapter = ConcatAdapter(
*listOfNotNull(
header, // need to filter it if null
adapter,
footer
).toTypedArray()
) )
swipeRefreshLayout.setOnRefreshListener { swipeRefreshLayout.setOnRefreshListener {
adapter.refresh() adapter.refresh()
adapter.notifyDataSetChanged()
header?.refreshStories()
} }
adapter.addLoadStateListener { loadState -> adapter.addLoadStateListener { loadState ->
@ -80,6 +99,11 @@ internal fun <T: Any> initAdapter(
?: loadState.append as? LoadState.Error ?: loadState.append as? LoadState.Error
?: loadState.prepend as? LoadState.Error ?: loadState.prepend as? LoadState.Error
?: loadState.refresh as? LoadState.Error ?: loadState.refresh as? LoadState.Error
if(errorState?.error is CancellationException){
return@addLoadStateListener
}
errorState?.let { errorState?.let {
val error: String = (it.error as? HttpException)?.response()?.errorBody()?.string()?.ifEmpty { null }?.let { s -> val error: String = (it.error as? HttpException)?.response()?.errorBody()?.string()?.ifEmpty { null }?.let { s ->
try { try {
@ -143,6 +167,8 @@ class ReposLoadStateAdapter(
} }
} }
/** /**
* [RecyclerView.ViewHolder] that is shown at the end of the feed to indicate loading or errors * [RecyclerView.ViewHolder] that is shown at the end of the feed to indicate loading or errors
* in the loading of appending values. * in the loading of appending values.

View File

@ -18,8 +18,10 @@ import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import org.pixeldroid.app.databinding.FragmentFeedBinding import org.pixeldroid.app.databinding.FragmentFeedBinding
import org.pixeldroid.app.posts.feeds.initAdapter import org.pixeldroid.app.posts.feeds.initAdapter
import org.pixeldroid.app.stories.StoriesAdapter
import org.pixeldroid.app.utils.BaseFragment import org.pixeldroid.app.utils.BaseFragment
import org.pixeldroid.app.utils.api.objects.FeedContentDatabase import org.pixeldroid.app.utils.api.objects.FeedContentDatabase
import org.pixeldroid.app.utils.bindingLifecycleAware
import org.pixeldroid.app.utils.db.AppDatabase import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.db.dao.feedContent.FeedContentDao import org.pixeldroid.app.utils.db.dao.feedContent.FeedContentDao
import org.pixeldroid.app.utils.limitedLengthSmoothScrollToPosition import org.pixeldroid.app.utils.limitedLengthSmoothScrollToPosition
@ -31,8 +33,9 @@ open class CachedFeedFragment<T: FeedContentDatabase> : BaseFragment() {
internal lateinit var viewModel: FeedViewModel<T> internal lateinit var viewModel: FeedViewModel<T>
internal lateinit var adapter: PagingDataAdapter<T, RecyclerView.ViewHolder> internal lateinit var adapter: PagingDataAdapter<T, RecyclerView.ViewHolder>
internal var headerAdapter: StoriesAdapter? = null
private lateinit var binding: FragmentFeedBinding private var binding: FragmentFeedBinding by bindingLifecycleAware()
private var job: Job? = null private var job: Job? = null
@ -49,6 +52,7 @@ open class CachedFeedFragment<T: FeedContentDatabase> : BaseFragment() {
} }
} }
//TODO rename function to something that makes sense
internal fun initSearch() { internal fun initSearch() {
// Scroll to top when the list is refreshed from network. // Scroll to top when the list is refreshed from network.
lifecycleScope.launchWhenStarted { lifecycleScope.launchWhenStarted {
@ -73,7 +77,9 @@ open class CachedFeedFragment<T: FeedContentDatabase> : BaseFragment() {
binding = FragmentFeedBinding.inflate(layoutInflater) binding = FragmentFeedBinding.inflate(layoutInflater)
initAdapter(binding.progressBar, binding.swipeRefreshLayout, initAdapter(binding.progressBar, binding.swipeRefreshLayout,
binding.list, binding.motionLayout, binding.errorLayout, adapter) binding.list, binding.motionLayout, binding.errorLayout, adapter,
headerAdapter
)
return binding.root return binding.root
} }

View File

@ -16,18 +16,20 @@
package org.pixeldroid.app.posts.feeds.cachedFeeds package org.pixeldroid.app.posts.feeds.cachedFeeds
import androidx.paging.* import androidx.paging.ExperimentalPagingApi
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.RemoteMediator
import kotlinx.coroutines.flow.Flow
import org.pixeldroid.app.utils.api.objects.FeedContentDatabase
import org.pixeldroid.app.utils.db.AppDatabase import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.db.dao.feedContent.FeedContentDao import org.pixeldroid.app.utils.db.dao.feedContent.FeedContentDao
import org.pixeldroid.app.utils.api.objects.FeedContentDatabase
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
/** /**
* Repository class that works with local and remote data sources. * Repository class that works with local and remote data sources.
*/ */
class FeedContentRepository<T: FeedContentDatabase> @ExperimentalPagingApi class FeedContentRepository<T: FeedContentDatabase> @ExperimentalPagingApi constructor(
@Inject constructor(
private val db: AppDatabase, private val db: AppDatabase,
private val dao: FeedContentDao<T>, private val dao: FeedContentDao<T>,
private val mediator: RemoteMediator<Int, T> private val mediator: RemoteMediator<Int, T>

View File

@ -221,8 +221,7 @@ class NotificationsFragment : CachedFeedFragment<Notification>() {
setTextViewFromISO8601( setTextViewFromISO8601(
it, it,
notificationTime, notificationTime,
false, false
itemView.context
) )
} }

View File

@ -1,12 +1,14 @@
package org.pixeldroid.app.posts.feeds.cachedFeeds.postFeeds package org.pixeldroid.app.posts.feeds.cachedFeeds.postFeeds
import androidx.paging.* import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.PagingSource
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.room.withTransaction import androidx.room.withTransaction
import org.pixeldroid.app.utils.db.AppDatabase import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
import org.pixeldroid.app.utils.db.entities.HomeStatusDatabaseEntity import org.pixeldroid.app.utils.db.entities.HomeStatusDatabaseEntity
import java.lang.NullPointerException import org.pixeldroid.app.utils.di.PixelfedAPIHolder
import javax.inject.Inject
/** /**
@ -17,7 +19,7 @@ import javax.inject.Inject
* a local db cache. * a local db cache.
*/ */
@OptIn(ExperimentalPagingApi::class) @OptIn(ExperimentalPagingApi::class)
class HomeFeedRemoteMediator @Inject constructor( class HomeFeedRemoteMediator(
private val apiHolder: PixelfedAPIHolder, private val apiHolder: PixelfedAPIHolder,
private val db: AppDatabase, private val db: AppDatabase,
) : RemoteMediator<Int, HomeStatusDatabaseEntity>() { ) : RemoteMediator<Int, HomeStatusDatabaseEntity>() {
@ -47,7 +49,7 @@ class HomeFeedRemoteMediator @Inject constructor(
HomeStatusDatabaseEntity(user.user_id, user.instance_uri, it) HomeStatusDatabaseEntity(user.user_id, user.instance_uri, it)
} }
val endOfPaginationReached = apiResponse.isEmpty() val endOfPaginationReached = apiResponse.isEmpty() || maxId == apiResponse.sortedBy { it.created_at }.last().id
db.withTransaction { db.withTransaction {
// Clear table in the database // Clear table in the database

View File

@ -11,14 +11,14 @@ import androidx.paging.PagingDataAdapter
import androidx.paging.RemoteMediator import androidx.paging.RemoteMediator
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.pixeldroid.app.R
import org.pixeldroid.app.utils.db.dao.feedContent.FeedContentDao
import org.pixeldroid.app.posts.StatusViewHolder import org.pixeldroid.app.posts.StatusViewHolder
import org.pixeldroid.app.posts.feeds.cachedFeeds.FeedViewModel
import org.pixeldroid.app.posts.feeds.cachedFeeds.CachedFeedFragment import org.pixeldroid.app.posts.feeds.cachedFeeds.CachedFeedFragment
import org.pixeldroid.app.posts.feeds.cachedFeeds.FeedViewModel
import org.pixeldroid.app.posts.feeds.cachedFeeds.ViewModelFactory import org.pixeldroid.app.posts.feeds.cachedFeeds.ViewModelFactory
import org.pixeldroid.app.stories.StoriesAdapter
import org.pixeldroid.app.utils.api.objects.FeedContentDatabase import org.pixeldroid.app.utils.api.objects.FeedContentDatabase
import org.pixeldroid.app.utils.api.objects.Status import org.pixeldroid.app.utils.api.objects.Status
import org.pixeldroid.app.utils.db.dao.feedContent.FeedContentDao
import org.pixeldroid.app.utils.displayDimensionsInPx import org.pixeldroid.app.utils.displayDimensionsInPx
import kotlin.properties.Delegates import kotlin.properties.Delegates
@ -38,14 +38,18 @@ class PostFeedFragment<T: FeedContentDatabase>: CachedFeedFragment<T>() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
adapter = PostsAdapter(requireContext().displayDimensionsInPx()) home = requireArguments().getBoolean("home")
home = requireArguments().get("home") as Boolean adapter = PostsAdapter(requireContext().displayDimensionsInPx())
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
if (home){ if (home){
mediator = HomeFeedRemoteMediator(apiHolder, db) as RemoteMediator<Int, T> mediator = HomeFeedRemoteMediator(apiHolder, db) as RemoteMediator<Int, T>
dao = db.homePostDao() as FeedContentDao<T> dao = db.homePostDao() as FeedContentDao<T>
headerAdapter = StoriesAdapter(lifecycleScope, apiHolder)
headerAdapter?.showStories = false
headerAdapter?.refreshStories()
} }
else { else {
mediator = PublicFeedRemoteMediator(apiHolder, db) as RemoteMediator<Int, T> mediator = PublicFeedRemoteMediator(apiHolder, db) as RemoteMediator<Int, T>
@ -55,7 +59,7 @@ class PostFeedFragment<T: FeedContentDatabase>: CachedFeedFragment<T>() {
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?,
): View? { ): View? {
val view = super.onCreateView(inflater, container, savedInstanceState) val view = super.onCreateView(inflater, container, savedInstanceState)
@ -70,6 +74,7 @@ class PostFeedFragment<T: FeedContentDatabase>: CachedFeedFragment<T>() {
return view return view
} }
inner class PostsAdapter(private val displayDimensionsInPx: Pair<Int, Int>) : PagingDataAdapter<T, RecyclerView.ViewHolder>( inner class PostsAdapter(private val displayDimensionsInPx: Pair<Int, Int>) : PagingDataAdapter<T, RecyclerView.ViewHolder>(
object : DiffUtil.ItemCallback<T>() { object : DiffUtil.ItemCallback<T>() {
override fun areItemsTheSame (oldItem: T, newItem: T): Boolean = oldItem.id == newItem.id override fun areItemsTheSame (oldItem: T, newItem: T): Boolean = oldItem.id == newItem.id
@ -81,15 +86,19 @@ class PostFeedFragment<T: FeedContentDatabase>: CachedFeedFragment<T>() {
return StatusViewHolder.create(parent) return StatusViewHolder.create(parent)
} }
override fun getItemViewType(position: Int): Int {
return R.layout.post_fragment
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val uiModel = getItem(position) as Status? holder.itemView.visibility = View.VISIBLE
uiModel?.let { holder.itemView.layoutParams =
(holder as StatusViewHolder).bind(it, apiHolder, db, lifecycleScope, displayDimensionsInPx) RecyclerView.LayoutParams(
} ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
val uiModel = getItem(position) as Status?
uiModel?.let {
(holder as StatusViewHolder).bind(
it, apiHolder, db, lifecycleScope, displayDimensionsInPx, requestPermissionDownloadPic
)
}
} }
} }
} }

View File

@ -16,13 +16,15 @@
package org.pixeldroid.app.posts.feeds.cachedFeeds.postFeeds package org.pixeldroid.app.posts.feeds.cachedFeeds.postFeeds
import androidx.paging.* import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadType
import androidx.paging.PagingSource
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.room.withTransaction import androidx.room.withTransaction
import org.pixeldroid.app.utils.db.AppDatabase import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity
import org.pixeldroid.app.utils.di.PixelfedAPIHolder import org.pixeldroid.app.utils.di.PixelfedAPIHolder
import java.lang.NullPointerException
import javax.inject.Inject
/** /**
* RemoteMediator for the public feed. * RemoteMediator for the public feed.
@ -32,7 +34,7 @@ import javax.inject.Inject
* a local db cache. * a local db cache.
*/ */
@OptIn(ExperimentalPagingApi::class) @OptIn(ExperimentalPagingApi::class)
class PublicFeedRemoteMediator @Inject constructor( class PublicFeedRemoteMediator(
private val apiHolder: PixelfedAPIHolder, private val apiHolder: PixelfedAPIHolder,
private val db: AppDatabase private val db: AppDatabase
) : RemoteMediator<Int, PublicFeedStatusDatabaseEntity>() { ) : RemoteMediator<Int, PublicFeedStatusDatabaseEntity>() {
@ -62,7 +64,7 @@ class PublicFeedRemoteMediator @Inject constructor(
val dbObjects = apiResponse.map{ val dbObjects = apiResponse.map{
PublicFeedStatusDatabaseEntity(user.user_id, user.instance_uri, it) PublicFeedStatusDatabaseEntity(user.user_id, user.instance_uri, it)
} }
val endOfPaginationReached = apiResponse.isEmpty() val endOfPaginationReached = apiResponse.isEmpty() || maxId == apiResponse.sortedBy { it.created_at }.last().id
db.withTransaction { db.withTransaction {
// Clear table in the database // Clear table in the database

View File

@ -11,6 +11,7 @@ import androidx.paging.ExperimentalPagingApi
import androidx.paging.LoadState import androidx.paging.LoadState
import androidx.paging.PagingDataAdapter import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
@ -20,6 +21,7 @@ import org.pixeldroid.app.posts.feeds.initAdapter
import org.pixeldroid.app.posts.feeds.launch import org.pixeldroid.app.posts.feeds.launch
import org.pixeldroid.app.utils.BaseFragment import org.pixeldroid.app.utils.BaseFragment
import org.pixeldroid.app.utils.api.objects.FeedContent import org.pixeldroid.app.utils.api.objects.FeedContent
import org.pixeldroid.app.utils.limitedLengthSmoothScrollToPosition
/** /**
@ -30,8 +32,7 @@ open class UncachedFeedFragment<T: FeedContent> : BaseFragment() {
internal lateinit var viewModel: FeedViewModel<T> internal lateinit var viewModel: FeedViewModel<T>
internal lateinit var adapter: PagingDataAdapter<T, RecyclerView.ViewHolder> internal lateinit var adapter: PagingDataAdapter<T, RecyclerView.ViewHolder>
lateinit var binding: FragmentFeedBinding var binding: FragmentFeedBinding? = null
private var job: Job? = null private var job: Job? = null
@ -48,23 +49,35 @@ open class UncachedFeedFragment<T: FeedContent> : BaseFragment() {
.distinctUntilChangedBy { it.refresh } .distinctUntilChangedBy { it.refresh }
// Only react to cases where Remote REFRESH completes i.e., NotLoading. // Only react to cases where Remote REFRESH completes i.e., NotLoading.
.filter { it.refresh is LoadState.NotLoading } .filter { it.refresh is LoadState.NotLoading }
.collect { binding.list.scrollToPosition(0) } .collect { binding?.list?.scrollToPosition(0) }
} }
} }
override fun onCreateView( fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?, swipeRefreshLayout: SwipeRefreshLayout?
): View? { ): View {
super.onCreateView(inflater, container, savedInstanceState) super.onCreateView(inflater, container, savedInstanceState)
binding = FragmentFeedBinding.inflate(layoutInflater) binding = FragmentFeedBinding.inflate(layoutInflater)
initAdapter(binding.progressBar, binding.swipeRefreshLayout, binding.list, binding!!.let {
binding.motionLayout, binding.errorLayout, adapter) initAdapter(
it.progressBar, swipeRefreshLayout ?: it.swipeRefreshLayout, it.list,
it.motionLayout, it.errorLayout, adapter
)
return binding.root }
return binding!!.root
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return onCreateView(inflater, container, savedInstanceState, null)
}
fun onTabReClicked() {
binding?.list?.limitedLengthSmoothScrollToPosition(0)
} }
} }

View File

@ -85,7 +85,9 @@ class UncachedPostsFragment : UncachedFeedFragment<Status>() {
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
getItem(position)?.let { getItem(position)?.let {
(holder as StatusViewHolder).bind(it, apiHolder, db, lifecycleScope, displayDimensionsInPx) (holder as StatusViewHolder).bind(
it, apiHolder, db, lifecycleScope, displayDimensionsInPx, requestPermissionDownloadPic
)
} }
} }
} }

View File

@ -5,12 +5,15 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.NestedScrollingChild
import androidx.core.view.NestedScrollingChildHelper
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.paging.ExperimentalPagingApi import androidx.paging.ExperimentalPagingApi
import androidx.paging.PagingDataAdapter import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.CommentBinding import org.pixeldroid.app.databinding.CommentBinding
import org.pixeldroid.app.posts.PostActivity import org.pixeldroid.app.posts.PostActivity
@ -25,7 +28,7 @@ import org.pixeldroid.app.utils.setProfileImageFromURL
/** /**
* Fragment to show a list of [Status]s, in form of comments * Fragment to show a list of [Status]s, in form of comments
*/ */
class CommentFragment : UncachedFeedFragment<Status>() { class CommentFragment(val swipeRefreshLayout: SwipeRefreshLayout): UncachedFeedFragment<Status>() {
private lateinit var id: String private lateinit var id: String
private lateinit var domain: String private lateinit var domain: String
@ -42,11 +45,11 @@ class CommentFragment : UncachedFeedFragment<Status>() {
@OptIn(ExperimentalPagingApi::class) @OptIn(ExperimentalPagingApi::class)
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?,
): View? { ): View? {
val view = super.onCreateView(inflater, container, savedInstanceState) val view = super.onCreateView(inflater, container, savedInstanceState, swipeRefreshLayout)
// Get the view model // Get the view model
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@ -62,6 +65,7 @@ class CommentFragment : UncachedFeedFragment<Status>() {
launch() launch()
initSearch() initSearch()
binding?.swipeRefreshLayout?.isEnabled = false
return view return view
} }
companion object { companion object {

View File

@ -2,17 +2,23 @@ package org.pixeldroid.app.posts.feeds.uncachedFeeds.hashtags
import android.os.Bundle import android.os.Bundle
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityFollowersBinding
import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedPostsFragment import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedPostsFragment
import org.pixeldroid.app.utils.BaseThemedWithBarActivity import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.objects.Tag.Companion.HASHTAG_TAG import org.pixeldroid.app.utils.api.objects.Tag.Companion.HASHTAG_TAG
class HashTagActivity : BaseThemedWithBarActivity() { class HashTagActivity : BaseActivity() {
private var tagFragment = UncachedPostsFragment() private var tagFragment = UncachedPostsFragment()
private lateinit var binding: ActivityFollowersBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_followers) binding = ActivityFollowersBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.topBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
// Get hashtag tag // Get hashtag tag

View File

@ -13,12 +13,12 @@ import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityCollectionBinding import org.pixeldroid.app.databinding.ActivityCollectionBinding
import org.pixeldroid.app.profile.ProfileFeedFragment.Companion.COLLECTION import org.pixeldroid.app.profile.ProfileFeedFragment.Companion.COLLECTION
import org.pixeldroid.app.profile.ProfileFeedFragment.Companion.COLLECTION_ID import org.pixeldroid.app.profile.ProfileFeedFragment.Companion.COLLECTION_ID
import org.pixeldroid.app.utils.BaseThemedWithBarActivity import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.PixelfedAPI import org.pixeldroid.app.utils.api.PixelfedAPI
import org.pixeldroid.app.utils.api.objects.Collection import org.pixeldroid.app.utils.api.objects.Collection
import java.lang.Exception import java.lang.Exception
class CollectionActivity : BaseThemedWithBarActivity() { class CollectionActivity : BaseActivity() {
private lateinit var binding: ActivityCollectionBinding private lateinit var binding: ActivityCollectionBinding
private lateinit var collection: Collection private lateinit var collection: Collection
@ -37,6 +37,7 @@ class CollectionActivity : BaseThemedWithBarActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityCollectionBinding.inflate(layoutInflater) binding = ActivityCollectionBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
setSupportActionBar(binding.topBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)

View File

@ -6,6 +6,7 @@ import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doAfterTextChanged
@ -19,44 +20,60 @@ import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityEditProfileBinding import org.pixeldroid.app.databinding.ActivityEditProfileBinding
import org.pixeldroid.app.utils.BaseThemedWithBarActivity import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.openUrl import org.pixeldroid.app.utils.openUrl
class EditProfileActivity : BaseThemedWithBarActivity() { class EditProfileActivity : BaseActivity() {
private lateinit var model: EditProfileViewModel private val model: EditProfileViewModel by viewModels()
private lateinit var binding: ActivityEditProfileBinding private lateinit var binding: ActivityEditProfileBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityEditProfileBinding.inflate(layoutInflater) binding = ActivityEditProfileBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
setSupportActionBar(binding.topBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setTitle(R.string.edit_profile)
val _model: EditProfileViewModel by viewModels { EditProfileViewModelFactory(application) } onBackPressedDispatcher.addCallback(this) {
model = _model // Handle the back button event
if(model.madeChanges()){
MaterialAlertDialogBuilder(binding.root.context).apply {
setMessage(getString(R.string.profile_save_changes))
setNegativeButton(android.R.string.cancel) { _, _ -> }
setPositiveButton(android.R.string.ok) { _, _ ->
this@addCallback.isEnabled = false
super.onBackPressedDispatcher.onBackPressed()
}
}.show()
} else {
this.isEnabled = false
if (model.submittedChanges) setResult(RESULT_OK)
super.onBackPressedDispatcher.onBackPressed()
}
}
lifecycleScope.launch { lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { repeatOnLifecycle(Lifecycle.State.STARTED) {
model.uiState.collect { uiState -> model.uiState.collect { uiState ->
if(uiState.profileLoaded){ if(binding.bioEditText.text.toString() != uiState.bio) binding.bioEditText.setText(uiState.bio)
binding.bioEditText.setText(uiState.bio) if(binding.nameEditText.text.toString() != uiState.name) binding.nameEditText.setText(uiState.name)
binding.nameEditText.setText(uiState.name)
model.changesApplied() binding.progressCard.visibility = if(uiState.loadingProfile || uiState.sendingProfile || uiState.uploadingPicture || uiState.profileSent || uiState.error) View.VISIBLE else View.INVISIBLE
}
binding.progressCard.visibility = if(uiState.loadingProfile || uiState.sendingProfile || uiState.profileSent || uiState.error) View.VISIBLE else View.INVISIBLE
if(uiState.loadingProfile) binding.progressText.setText(R.string.fetching_profile) if(uiState.loadingProfile) binding.progressText.setText(R.string.fetching_profile)
else if(uiState.sendingProfile) binding.progressText.setText(R.string.saving_profile) else if(uiState.sendingProfile) binding.progressText.setText(R.string.saving_profile)
binding.privateSwitch.isChecked = uiState.privateAccount == true binding.privateSwitch.isChecked = uiState.privateAccount == true
Glide.with(binding.profilePic).load(uiState.profilePictureUri) Glide.with(binding.profilePic).load(uiState.profilePictureUri)
.apply(RequestOptions.circleCropTransform()) .apply(RequestOptions.circleCropTransform())
.into(binding.profilePic) .into(binding.profilePic)
binding.savingProgressBar.visibility = if(uiState.error || uiState.profileSent) View.GONE binding.savingProgressBar.visibility =
else View.VISIBLE if(uiState.error || (uiState.profileSent && !uiState.uploadingPicture)) View.GONE
else View.VISIBLE
if(uiState.profileSent){ if(uiState.profileSent && !uiState.uploadingPicture && !uiState.error){
binding.progressText.setText(R.string.profile_saved) binding.progressText.setText(R.string.profile_saved)
binding.done.visibility = View.VISIBLE binding.done.visibility = View.VISIBLE
} else { } else {
@ -94,18 +111,18 @@ class EditProfileActivity : BaseThemedWithBarActivity() {
} }
} }
// binding.changeImageButton.setOnClickListener { binding.profilePic.setOnClickListener {
// Intent(Intent.ACTION_GET_CONTENT).apply { Intent(Intent.ACTION_GET_CONTENT).apply {
// type = "*/*" type = "*/*"
// putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*")) putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*"))
// action = Intent.ACTION_GET_CONTENT action = Intent.ACTION_GET_CONTENT
// addCategory(Intent.CATEGORY_OPENABLE) addCategory(Intent.CATEGORY_OPENABLE)
// putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false) putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false)
// uploadImageResultContract.launch( uploadImageResultContract.launch(
// Intent.createChooser(this, null) Intent.createChooser(this, null)
// ) )
// } }
// } }
} }
private val uploadImageResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> private val uploadImageResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
@ -119,10 +136,10 @@ class EditProfileActivity : BaseThemedWithBarActivity() {
val imageUri: String = clipData.getItemAt(i).uri.toString() val imageUri: String = clipData.getItemAt(i).uri.toString()
images.add(imageUri) images.add(imageUri)
} }
model.uploadImage(images.first()) model.updateImage(images.first())
} else if (data.data != null) { } else if (data.data != null) {
images.add(data.data!!.toString()) images.add(data.data!!.toString())
model.uploadImage(images.first()) model.updateImage(images.first())
} }
} }
} }
@ -132,18 +149,6 @@ class EditProfileActivity : BaseThemedWithBarActivity() {
return true return true
} }
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
if(model.madeChanges()){
MaterialAlertDialogBuilder(binding.root.context).apply {
setMessage(getString(R.string.profile_save_changes))
setNegativeButton(android.R.string.cancel) { _, _ -> }
setPositiveButton(android.R.string.ok) { _, _ -> super.onBackPressed()}
}.show()
}
else super.onBackPressed()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId){ when (item.itemId){
R.id.action_apply -> { R.id.action_apply -> {

View File

@ -1,16 +1,16 @@
package org.pixeldroid.app.profile package org.pixeldroid.app.profile
import android.app.Application import android.content.Context
import android.net.Uri import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.text.Editable import android.text.Editable
import android.util.Log import android.util.Log
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
@ -21,23 +21,33 @@ import kotlinx.coroutines.launch
import okhttp3.MultipartBody import okhttp3.MultipartBody
import org.pixeldroid.app.postCreation.ProgressRequestBody import org.pixeldroid.app.postCreation.ProgressRequestBody
import org.pixeldroid.app.posts.fromHtml import org.pixeldroid.app.posts.fromHtml
import org.pixeldroid.app.utils.PixelDroidApplication
import org.pixeldroid.app.utils.api.objects.Account import org.pixeldroid.app.utils.api.objects.Account
import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.db.updateUserInfoDb
import org.pixeldroid.app.utils.di.PixelfedAPIHolder import org.pixeldroid.app.utils.di.PixelfedAPIHolder
import retrofit2.HttpException
import javax.inject.Inject import javax.inject.Inject
class EditProfileViewModel(application: Application) : AndroidViewModel(application) { @HiltViewModel
class EditProfileViewModel @Inject constructor(
@ApplicationContext private val applicationContext: Context
): ViewModel() {
@Inject @Inject
lateinit var apiHolder: PixelfedAPIHolder lateinit var apiHolder: PixelfedAPIHolder
@Inject
lateinit var db: AppDatabase
private val _uiState = MutableStateFlow(EditProfileActivityUiState()) private val _uiState = MutableStateFlow(EditProfileActivityUiState())
val uiState: StateFlow<EditProfileActivityUiState> = _uiState val uiState: StateFlow<EditProfileActivityUiState> = _uiState
var oldProfile: Account? = null private var oldProfile: Account? = null
var submittedChanges = false
private set
init { init {
(application as PixelDroidApplication).getAppComponent().inject(this)
loadProfile() loadProfile()
} }
@ -46,6 +56,7 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
val api = apiHolder.api ?: apiHolder.setToCurrentUser() val api = apiHolder.api ?: apiHolder.setToCurrentUser()
try { try {
val profile = api.verifyCredentials() val profile = api.verifyCredentials()
updateUserInfoDb(db, profile)
if (oldProfile == null) oldProfile = profile if (oldProfile == null) oldProfile = profile
_uiState.update { currentUiState -> _uiState.update { currentUiState ->
currentUiState.copy( currentUiState.copy(
@ -76,15 +87,10 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
fun sendProfile() { fun sendProfile() {
val api = apiHolder.api ?: apiHolder.setToCurrentUser() val api = apiHolder.api ?: apiHolder.setToCurrentUser()
val requestBody =
null //MultipartBody.Part.createFormData("avatar", System.currentTimeMillis().toString(), avatarBody)
_uiState.update { currentUiState -> _uiState.update { currentUiState ->
currentUiState.copy( currentUiState.copy(
sendingProfile = true, sendingProfile = true,
profileSent = false, profileSent = false,
loadingProfile = false,
profileLoaded = false,
error = false error = false
) )
} }
@ -97,12 +103,17 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
note = bio, note = bio,
locked = privateAccount, locked = privateAccount,
) )
if (madeChanges()) submittedChanges = true
oldProfile = account oldProfile = account
_uiState.update { currentUiState -> _uiState.update { currentUiState ->
currentUiState.copy( currentUiState.copy(
bio = account.source?.note ?: account.note?.let {fromHtml(it).toString()}, bio = account.source?.note
?: account.note?.let { fromHtml(it).toString() },
name = account.display_name, name = account.display_name,
profilePictureUri = account.anyAvatar()?.toUri(), profilePictureUri = if (profilePictureChanged) profilePictureUri
else account.anyAvatar()?.toUri(),
uploadProgress = 0,
uploadingPicture = profilePictureChanged,
privateAccount = account.locked, privateAccount = account.locked,
sendingProfile = false, sendingProfile = false,
profileSent = true, profileSent = true,
@ -111,14 +122,13 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
error = false error = false
) )
} }
if(profilePictureChanged) uploadImage()
} catch (exception: Exception) { } catch (exception: Exception) {
Log.e("TAG", exception.toString()) Log.e("TAG", exception.toString())
_uiState.update { currentUiState -> _uiState.update { currentUiState ->
currentUiState.copy( currentUiState.copy(
sendingProfile = false, sendingProfile = false,
profileSent = false, profileSent = false,
loadingProfile = false,
profileLoaded = false,
error = true error = true
) )
} }
@ -145,20 +155,16 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
} }
} }
fun changesApplied() {
_uiState.update { currentUiState ->
currentUiState.copy(profileLoaded = false)
}
}
fun madeChanges(): Boolean = fun madeChanges(): Boolean =
with(uiState.value) { with(uiState.value) {
val bioUnchanged: Boolean = oldProfile?.source?.note?.let { it != bio } val privateChanged = oldProfile?.locked != privateAccount
// If source note is null, check note val displayNameChanged = oldProfile?.display_name != name
val bioChanged: Boolean = oldProfile?.source?.note?.let { it != bio }
// If source note is null, check note
?: oldProfile?.note?.let { fromHtml(it).toString() != bio } ?: oldProfile?.note?.let { fromHtml(it).toString() != bio }
?: true ?: true
oldProfile?.locked != privateAccount || oldProfile?.display_name != name
|| bioUnchanged profilePictureChanged || privateChanged || displayNameChanged || bioChanged
} }
fun clickedCard() { fun clickedCard() {
@ -178,16 +184,27 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
} }
} }
fun uploadImage(image: String) { fun updateImage(image: String) {
//TODO fix _uiState.update { currentUiState ->
currentUiState.copy(
profilePictureUri = image.toUri(),
profilePictureChanged = true,
profileSent = false
)
}
}
private fun uploadImage() {
val image = uiState.value.profilePictureUri!!
val inputStream = val inputStream =
getApplication<PixelDroidApplication>().contentResolver.openInputStream(image.toUri()) applicationContext.contentResolver.openInputStream(image)
?: return ?: return
val size: Long = val size: Long =
if (image.toUri().scheme == "content") { if (image.scheme == "content") {
getApplication<PixelDroidApplication>().contentResolver.query( applicationContext.contentResolver.query(
image.toUri(), image,
null, null,
null, null,
null, null,
@ -203,7 +220,7 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
cursor.getLong(sizeIndex) cursor.getLong(sizeIndex)
} ?: 0 } ?: 0
} else { } else {
image.toUri().toFile().length() image.toFile().length()
} }
val imagePart = ProgressRequestBody(inputStream, size, "image/*") val imagePart = ProgressRequestBody(inputStream, size, "image/*")
@ -225,21 +242,32 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
var postSub: Disposable? = null var postSub: Disposable? = null
val api = apiHolder.api ?: apiHolder.setToCurrentUser() val api = apiHolder.api ?: apiHolder.setToCurrentUser()
val inter = api.updateProfilePicture(requestBody.parts[0])
val pixelfed = db.instanceDao().getActiveInstance().pixelfed
val inter =
if(pixelfed) api.updateProfilePicture(requestBody.parts[0])
else api.updateProfilePictureMastodon(requestBody.parts[0])
postSub = inter postSub = inter
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe( .subscribe(
{ it: Account -> /* onNext = */ { account: Account ->
Log.e("qsdfqsdfs", it.toString()) account.anyAvatar()?.let {
_uiState.update { currentUiState ->
currentUiState.copy(
profilePictureUri = it.toUri()
)
}
}
}, },
{ e: Throwable -> /* onError = */ { e: Throwable ->
Log.e("error", (e as? HttpException)?.message().orEmpty())
_uiState.update { currentUiState -> _uiState.update { currentUiState ->
currentUiState.copy( currentUiState.copy(
uploadProgress = 0, uploadProgress = 0,
uploadingPicture = true, uploadingPicture = false,
error = true error = true
) )
} }
@ -247,9 +275,10 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
postSub?.dispose() postSub?.dispose()
sub.dispose() sub.dispose()
}, },
{ /* onComplete = */ {
_uiState.update { currentUiState -> _uiState.update { currentUiState ->
currentUiState.copy( currentUiState.copy(
profilePictureChanged = false,
uploadProgress = 100, uploadProgress = 100,
uploadingPicture = false uploadingPicture = false
) )
@ -265,7 +294,8 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat
data class EditProfileActivityUiState( data class EditProfileActivityUiState(
val name: String? = null, val name: String? = null,
val bio: String? = null, val bio: String? = null,
val profilePictureUri: Uri?= null, val profilePictureUri: Uri? = null,
val profilePictureChanged: Boolean = false,
val privateAccount: Boolean? = null, val privateAccount: Boolean? = null,
val loadingProfile: Boolean = true, val loadingProfile: Boolean = true,
val profileLoaded: Boolean = false, val profileLoaded: Boolean = false,
@ -274,10 +304,4 @@ data class EditProfileActivityUiState(
val error: Boolean = false, val error: Boolean = false,
val uploadingPicture: Boolean = false, val uploadingPicture: Boolean = false,
val uploadProgress: Int = 0, val uploadProgress: Int = 0,
) )
class EditProfileViewModelFactory(val application: Application) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.getConstructor(Application::class.java).newInstance(application)
}
}

View File

@ -2,20 +2,25 @@ package org.pixeldroid.app.profile
import android.os.Bundle import android.os.Bundle
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityFollowersBinding
import org.pixeldroid.app.posts.feeds.uncachedFeeds.accountLists.AccountListFragment import org.pixeldroid.app.posts.feeds.uncachedFeeds.accountLists.AccountListFragment
import org.pixeldroid.app.utils.BaseThemedWithBarActivity import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.objects.Account import org.pixeldroid.app.utils.api.objects.Account
import org.pixeldroid.app.utils.api.objects.Account.Companion.ACCOUNT_ID_TAG import org.pixeldroid.app.utils.api.objects.Account.Companion.ACCOUNT_ID_TAG
import org.pixeldroid.app.utils.api.objects.Account.Companion.ACCOUNT_TAG import org.pixeldroid.app.utils.api.objects.Account.Companion.ACCOUNT_TAG
import org.pixeldroid.app.utils.api.objects.Account.Companion.FOLLOWERS_TAG import org.pixeldroid.app.utils.api.objects.Account.Companion.FOLLOWERS_TAG
class FollowsActivity : BaseThemedWithBarActivity() { class FollowsActivity : BaseActivity() {
private var followsFragment = AccountListFragment() private var followsFragment = AccountListFragment()
private lateinit var binding: ActivityFollowersBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_followers) binding = ActivityFollowersBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.topBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)

View File

@ -6,26 +6,33 @@ import android.text.method.LinkMovementMethod
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityProfileBinding import org.pixeldroid.app.databinding.ActivityProfileBinding
import org.pixeldroid.app.posts.feeds.cachedFeeds.CachedFeedFragment
import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedFeedFragment
import org.pixeldroid.app.posts.parseHTMLText import org.pixeldroid.app.posts.parseHTMLText
import org.pixeldroid.app.utils.BaseThemedWithBarActivity import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.PixelfedAPI import org.pixeldroid.app.utils.api.PixelfedAPI
import org.pixeldroid.app.utils.api.objects.Account import org.pixeldroid.app.utils.api.objects.Account
import org.pixeldroid.app.utils.api.objects.FeedContent
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import org.pixeldroid.app.utils.db.updateUserInfoDb
import org.pixeldroid.app.utils.setProfileImageFromURL import org.pixeldroid.app.utils.setProfileImageFromURL
import retrofit2.HttpException import retrofit2.HttpException
import java.io.IOException import java.io.IOException
class ProfileActivity : BaseThemedWithBarActivity() { class ProfileActivity : BaseActivity() {
private lateinit var domain : String private lateinit var domain : String
private lateinit var accountId : String private lateinit var accountId : String
@ -36,7 +43,10 @@ class ProfileActivity : BaseThemedWithBarActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityProfileBinding.inflate(layoutInflater) binding = ActivityProfileBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
setSupportActionBar(binding.topBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
@ -51,9 +61,32 @@ class ProfileActivity : BaseThemedWithBarActivity() {
val tabs = createProfileTabs(account) val tabs = createProfileTabs(account)
setupTabs(tabs) setupTabs(tabs)
setContent(account) setContent(account)
binding.profileMotion.setTransitionListener(
object : MotionLayout.TransitionListener {
override fun onTransitionStarted(
motionLayout: MotionLayout?, startId: Int, endId: Int,
) {}
override fun onTransitionChange(p0: MotionLayout?, p1: Int, p2: Int, p3: Float) {}
override fun onTransitionCompleted(motionLayout: MotionLayout?, currentId: Int) {
if (currentId == R.id.hideProfile && motionLayout?.startState == R.id.start) {
// If the 1st transition has been made go to the second one
motionLayout.setTransition(R.id.second)
} else if(currentId == R.id.hideProfile && motionLayout?.startState == R.id.hideProfile){
motionLayout.setTransition(R.id.first)
}
}
override fun onTransitionTrigger(
motionLayout: MotionLayout?, triggerId: Int, positive: Boolean, progress: Float,
) {}
}
)
} }
private fun createProfileTabs(account: Account?): Array<Fragment>{ private fun createProfileTabs(account: Account?): Array<UncachedFeedFragment<FeedContent>> {
val profileFeedFragment = ProfileFeedFragment() val profileFeedFragment = ProfileFeedFragment()
profileFeedFragment.arguments = Bundle().apply { profileFeedFragment.arguments = Bundle().apply {
@ -77,7 +110,7 @@ class ProfileActivity : BaseThemedWithBarActivity() {
putSerializable(ProfileFeedFragment.COLLECTIONS, true) putSerializable(ProfileFeedFragment.COLLECTIONS, true)
} }
val returnArray: Array<Fragment> = arrayOf( val returnArray: Array<UncachedFeedFragment<FeedContent>> = arrayOf(
profileGridFragment, profileGridFragment,
profileFeedFragment, profileFeedFragment,
profileCollectionsFragment profileCollectionsFragment
@ -97,7 +130,7 @@ class ProfileActivity : BaseThemedWithBarActivity() {
} }
private fun setupTabs( private fun setupTabs(
tabs: Array<Fragment> tabs: Array<UncachedFeedFragment<FeedContent>>,
){ ){
binding.viewPager.adapter = object : FragmentStateAdapter(this) { binding.viewPager.adapter = object : FragmentStateAdapter(this) {
override fun createFragment(position: Int): Fragment { override fun createFragment(position: Int): Fragment {
@ -129,8 +162,15 @@ class ProfileActivity : BaseThemedWithBarActivity() {
} }
} }
}.attach() }.attach()
}
binding.profileTabs.addOnTabSelectedListener(object : OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {}
override fun onTabUnselected(tab: TabLayout.Tab) {}
override fun onTabReselected(tab: TabLayout.Tab) {
tabs[tab.position].onTabReClicked()
}
})
}
private fun setContent(account: Account?) { private fun setContent(account: Account?) {
if(account != null) { if(account != null) {
@ -149,6 +189,9 @@ class ProfileActivity : BaseThemedWithBarActivity() {
).show() ).show()
return@launchWhenResumed return@launchWhenResumed
} }
updateUserInfoDb(db, myAccount)
setViews(myAccount) setViews(myAccount)
} }
} }
@ -214,9 +257,15 @@ class ProfileActivity : BaseThemedWithBarActivity() {
) )
} }
private val editResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == RESULT_OK) {
// Profile was edited, reload
setContent(null)
}
}
private fun onClickEditButton() { private fun onClickEditButton() {
val intent = Intent(this, EditProfileActivity::class.java) editResult.launch(Intent(this, EditProfileActivity::class.java))
ContextCompat.startActivity(this, intent, null)
} }
private fun onClickFollowers(account: Account?) { private fun onClickFollowers(account: Account?) {

View File

@ -101,7 +101,7 @@ class ProfileFeedFragment : UncachedFeedFragment<FeedContent>() {
val view = super.onCreateView(inflater, container, savedInstanceState) val view = super.onCreateView(inflater, container, savedInstanceState)
if(grid || bookmarks || collections || addCollection) { if(grid || bookmarks || collections || addCollection) {
binding.list.layoutManager = GridLayoutManager(context, 3) binding?.list?.layoutManager = GridLayoutManager(context, 3)
} }
// Get the view model // Get the view model
@ -178,8 +178,10 @@ class ProfileFeedFragment : UncachedFeedFragment<FeedContent>() {
deleteFromCollection deleteFromCollection
) )
} else { } else {
(holder as StatusViewHolder).bind(it as Status, apiHolder, db, (holder as StatusViewHolder).bind(
lifecycleScope, requireContext().displayDimensionsInPx()) it as Status, apiHolder, db, lifecycleScope,
requireContext().displayDimensionsInPx(), requestPermissionDownloadPic
)
} }
} }
@ -189,8 +191,11 @@ class ProfileFeedFragment : UncachedFeedFragment<FeedContent>() {
val url = "$domain/i/collections/create" val url = "$domain/i/collections/create"
if(domain.isNullOrEmpty() || !requireContext().openUrl(url)) { if(domain.isNullOrEmpty() || !requireContext().openUrl(url)) {
Snackbar.make(binding.root, getString(R.string.new_collection_link_failed), binding?.let { binding ->
Snackbar.LENGTH_LONG).show() Snackbar.make(
binding.root, getString(R.string.new_collection_link_failed),
Snackbar.LENGTH_LONG).show()
}
} }
} }

View File

@ -9,17 +9,21 @@ import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivitySearchBinding
import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedPostsFragment import org.pixeldroid.app.posts.feeds.uncachedFeeds.UncachedPostsFragment
import org.pixeldroid.app.posts.feeds.uncachedFeeds.search.SearchAccountFragment import org.pixeldroid.app.posts.feeds.uncachedFeeds.search.SearchAccountFragment
import org.pixeldroid.app.posts.feeds.uncachedFeeds.search.SearchHashtagFragment import org.pixeldroid.app.posts.feeds.uncachedFeeds.search.SearchHashtagFragment
import org.pixeldroid.app.utils.BaseThemedWithBarActivity import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.objects.Results import org.pixeldroid.app.utils.api.objects.Results
class SearchActivity : BaseThemedWithBarActivity() { class SearchActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search) val binding = ActivitySearchBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.topBar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
var query = "" var query = ""

View File

@ -11,22 +11,24 @@ import androidx.core.content.ContextCompat
import org.pixeldroid.app.databinding.FragmentSearchBinding import org.pixeldroid.app.databinding.FragmentSearchBinding
import org.pixeldroid.app.searchDiscover.TrendingActivity.Companion.TRENDING_TAG import org.pixeldroid.app.searchDiscover.TrendingActivity.Companion.TRENDING_TAG
import org.pixeldroid.app.searchDiscover.TrendingActivity.Companion.TrendingType import org.pixeldroid.app.searchDiscover.TrendingActivity.Companion.TrendingType
import org.pixeldroid.app.utils.api.PixelfedAPI
import org.pixeldroid.app.utils.BaseFragment import org.pixeldroid.app.utils.BaseFragment
import org.pixeldroid.app.utils.api.PixelfedAPI
import org.pixeldroid.app.utils.bindingLifecycleAware import org.pixeldroid.app.utils.bindingLifecycleAware
/** /**
* This fragment lets you search and use Pixelfed's Discover feature * This fragment lets you search and use Pixelfed's Discover feature
*/ */
class SearchDiscoverFragment : BaseFragment() { class SearchDiscoverFragment : BaseFragment() {
private lateinit var api: PixelfedAPI private lateinit var api: PixelfedAPI
var binding: FragmentSearchBinding by bindingLifecycleAware() var binding: FragmentSearchBinding by bindingLifecycleAware()
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?,
): View { ): View {
binding = FragmentSearchBinding.inflate(inflater, container, false) binding = FragmentSearchBinding.inflate(inflater, container, false)
@ -56,4 +58,5 @@ class SearchDiscoverFragment : BaseFragment() {
intent.putExtra(TRENDING_TAG, type) intent.putExtra(TRENDING_TAG, type)
ContextCompat.startActivity(binding.root.context, intent, null) ContextCompat.startActivity(binding.root.context, intent, null)
} }
} }

View File

@ -15,7 +15,7 @@ import org.pixeldroid.app.posts.PostActivity
import org.pixeldroid.app.posts.feeds.uncachedFeeds.accountLists.AccountViewHolder import org.pixeldroid.app.posts.feeds.uncachedFeeds.accountLists.AccountViewHolder
import org.pixeldroid.app.posts.feeds.uncachedFeeds.search.HashTagViewHolder import org.pixeldroid.app.posts.feeds.uncachedFeeds.search.HashTagViewHolder
import org.pixeldroid.app.profile.ProfilePostViewHolder import org.pixeldroid.app.profile.ProfilePostViewHolder
import org.pixeldroid.app.utils.BaseThemedWithBarActivity import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.PixelfedAPI import org.pixeldroid.app.utils.api.PixelfedAPI
import org.pixeldroid.app.utils.api.objects.Account import org.pixeldroid.app.utils.api.objects.Account
import org.pixeldroid.app.utils.api.objects.Attachment import org.pixeldroid.app.utils.api.objects.Attachment
@ -24,7 +24,7 @@ import org.pixeldroid.app.utils.api.objects.Status
import org.pixeldroid.app.utils.api.objects.Tag import org.pixeldroid.app.utils.api.objects.Tag
import org.pixeldroid.app.utils.setSquareImageFromURL import org.pixeldroid.app.utils.setSquareImageFromURL
class TrendingActivity : BaseThemedWithBarActivity() { class TrendingActivity : BaseActivity() {
private lateinit var binding: ActivityTrendingBinding private lateinit var binding: ActivityTrendingBinding
private lateinit var trendingAdapter : TrendingRecyclerViewAdapter private lateinit var trendingAdapter : TrendingRecyclerViewAdapter
@ -33,6 +33,7 @@ class TrendingActivity : BaseThemedWithBarActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityTrendingBinding.inflate(layoutInflater) binding = ActivityTrendingBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
setSupportActionBar(binding.topBar)
val recycler = binding.list val recycler = binding.list
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)

View File

@ -1,26 +0,0 @@
package org.pixeldroid.app.settings
import android.content.Intent
import android.os.Bundle
import org.pixeldroid.app.BuildConfig
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityAboutBinding
import org.pixeldroid.app.utils.BaseThemedWithBarActivity
class AboutActivity : BaseThemedWithBarActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityAboutBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setTitle(R.string.about_pixeldroid)
binding.aboutVersionNumber.text = BuildConfig.VERSION_NAME
binding.licensesButton.setOnClickListener{
val intent = Intent(this, LicenseActivity::class.java)
startActivity(intent)
}
}
}

View File

@ -1,41 +0,0 @@
package org.pixeldroid.app.settings
import android.os.Bundle
import com.mikepenz.aboutlibraries.Libs
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.OpenSourceLicenseBinding
import org.pixeldroid.app.utils.BaseThemedWithBarActivity
/**
* Displays licenses for all app dependencies. JSON is
* generated by the plugin https://github.com/cookpad/LicenseToolsPlugin.
*/
class LicenseActivity: BaseThemedWithBarActivity() {
private lateinit var binding: OpenSourceLicenseBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = OpenSourceLicenseBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.setTitle(R.string.dependencies_licenses)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setHomeButtonEnabled(true)
setupRecyclerView()
}
private fun setupRecyclerView() {
val aboutLibsJson: String = applicationContext.resources.openRawResource(R.raw.aboutlibraries)
.bufferedReader().use { it.readText() }
val libs = Libs.Builder()
.withJson(aboutLibsJson)
.build()
val adapter = OpenSourceLicenseAdapter(libs)
binding.openSourceLicenseRecyclerView.adapter = adapter
}
}

View File

@ -1,56 +0,0 @@
package org.pixeldroid.app.settings
import android.annotation.SuppressLint
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.entity.Library
import org.pixeldroid.app.databinding.OpenSourceItemBinding
class OpenSourceLicenseAdapter(private val openSourceItems: Libs) :
RecyclerView.Adapter<OpenSourceLicenseAdapter.OpenSourceLicenceViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OpenSourceLicenceViewHolder
{
val itemBinding = OpenSourceItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return OpenSourceLicenceViewHolder(itemBinding)
}
override fun onBindViewHolder(holder: OpenSourceLicenceViewHolder, position: Int) {
val item = openSourceItems.libraries[position]
holder.bind(item)
}
override fun getItemCount(): Int = openSourceItems.libraries.size
class OpenSourceLicenceViewHolder(val binding: OpenSourceItemBinding) :
RecyclerView.ViewHolder(binding.root) {
@SuppressLint("SetTextI18n")
fun bind(item: Library) {
with(binding) {
if (item.name.isNotEmpty()) {
title.isVisible = true
title.text = item.name
} else {
title.isVisible = false
}
val license = item.licenses.firstOrNull()
val licenseName = license?.name ?: ""
val licenseUrl = license?.url?.let { " (${it} )" } ?: ""
copyright.isVisible = true
copyright.apply {
text = "$licenseName$licenseUrl"
movementMethod = LinkMovementMethod.getInstance()
}
url.isVisible = true
url.apply {
text = "${item.developers.firstOrNull()?.name ?: ""} ${item.website}"
movementMethod = LinkMovementMethod.getInstance()
}
}
}
}
}

View File

@ -6,6 +6,7 @@ import android.content.SharedPreferences
import android.content.res.XmlResourceParser import android.content.res.XmlResourceParser
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
@ -16,23 +17,39 @@ import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.pixeldroid.app.MainActivity import org.pixeldroid.app.MainActivity
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.utils.BaseThemedWithBarActivity import org.pixeldroid.app.databinding.SettingsBinding
import org.pixeldroid.common.ThemedActivity
import org.pixeldroid.app.utils.setThemeFromPreferences import org.pixeldroid.app.utils.setThemeFromPreferences
class SettingsActivity : BaseThemedWithBarActivity(), SharedPreferences.OnSharedPreferenceChangeListener { class SettingsActivity : ThemedActivity(), SharedPreferences.OnSharedPreferenceChangeListener {
private var restartMainOnExit = false private var restartMainOnExit = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val binding = SettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.topBar)
setContentView(R.layout.settings)
supportFragmentManager supportFragmentManager
.beginTransaction() .beginTransaction()
.replace(R.id.settings, SettingsFragment()) .replace(R.id.settings, SettingsFragment())
.commit() .commit()
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setTitle(R.string.menu_settings)
onBackPressedDispatcher.addCallback(this /* lifecycle owner */) {
// Handle the back button event
// If a setting (for example language or theme) was changed, the main activity should be
// started without history so that the change is applied to the whole back stack
if (restartMainOnExit) {
val intent = Intent(this@SettingsActivity, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
super@SettingsActivity.startActivity(intent)
} else {
finish()
}
}
restartMainOnExit = intent.getBooleanExtra("restartMain", false) restartMainOnExit = intent.getBooleanExtra("restartMain", false)
} }
@ -51,25 +68,17 @@ class SettingsActivity : BaseThemedWithBarActivity(), SharedPreferences.OnShared
) )
} }
override fun onBackPressed() { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
// If a setting (for example language or theme) was changed, the main activity should be sharedPreferences?.let {
// started without history so that the change is applied to the whole back stack when (key) {
if (restartMainOnExit) { "theme" -> {
val intent = Intent(this, MainActivity::class.java) setThemeFromPreferences(it, resources)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK recreateWithRestartStatus()
super.startActivity(intent) }
} else {
super.onBackPressed() "themeColor" -> {
} recreateWithRestartStatus()
} }
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
when (key) {
"theme" -> {
setThemeFromPreferences(sharedPreferences, resources)
recreateWithRestartStatus()
}
"themeColor" -> {
recreateWithRestartStatus()
} }
} }
} }
@ -125,7 +134,8 @@ class SettingsActivity : BaseThemedWithBarActivity(), SharedPreferences.OnShared
class LanguageSettingFragment : DialogFragment() { class LanguageSettingFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val list: MutableList<String> = mutableListOf() val list: MutableList<String> = mutableListOf()
resources.getXml(R.xml.locales_config).use { // IDE doesn't find it, but compiling works apparently?
resources.getXml(R.xml._generated_res_locale_config).use {
var eventType = it.eventType var eventType = it.eventType
while (eventType != XmlResourceParser.END_DOCUMENT) { while (eventType != XmlResourceParser.END_DOCUMENT) {
when (eventType) { when (eventType) {

View File

@ -0,0 +1,215 @@
package org.pixeldroid.app.stories
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.MotionEvent
import android.view.View.OnClickListener
import android.view.View.OnTouchListener
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.view.isVisible
import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.Target
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityStoriesBinding
import org.pixeldroid.app.posts.setTextViewFromISO8601
import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.api.objects.Account
class StoriesActivity: BaseActivity() {
companion object {
const val STORY_CAROUSEL = "LaunchStoryCarousel"
const val STORY_CAROUSEL_SELF = "LaunchStoryCarouselSelf"
const val STORY_CAROUSEL_USER_ID = "LaunchStoryUserId"
}
private lateinit var binding: ActivityStoriesBinding
private lateinit var storyProgress: StoryProgress
private val model: StoriesViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
//force night mode always
delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES
super.onCreate(savedInstanceState)
binding = ActivityStoriesBinding.inflate(layoutInflater)
setContentView(binding.root)
storyProgress = StoryProgress(model.uiState.value.imageList.size)
binding.storyProgressImage.setImageDrawable(storyProgress)
lifecycleScope.launch {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
model.uiState.collect { uiState ->
binding.pause.isSelected = uiState.paused
uiState.age?.let { setTextViewFromISO8601(it, binding.storyAge, false) }
if (uiState.errorMessage != null) {
binding.storyErrorText.setText(uiState.errorMessage)
binding.storyErrorCard.isVisible = true
} else binding.storyErrorCard.isVisible = false
if (uiState.snackBar != null) {
Snackbar.make(
binding.root, uiState.snackBar,
Snackbar.LENGTH_SHORT
).setAnchorView(binding.storyReplyField).show()
model.shownSnackbar()
}
if (uiState.username != null) {
binding.storyReplyField.hint = getString(R.string.replyToStory).format(uiState.username)
} else binding.storyReplyField.hint = null
uiState.profilePicture?.let {
Glide.with(binding.storyAuthorProfilePicture)
.load(it)
.apply(RequestOptions.circleCropTransform())
.into(binding.storyAuthorProfilePicture)
}
binding.storyAuthor.text = uiState.username
storyProgress.currentStory = uiState.currentImage
uiState.imageList.getOrNull(uiState.currentImage)?.let {
Glide.with(binding.storyImage)
.load(it)
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>,
isFirstResource: Boolean,
): Boolean = false
override fun onResourceReady(
resource: Drawable,
model: Any,
target: Target<Drawable>?,
dataSource: DataSource,
isFirstResource: Boolean,
): Boolean {
Glide.with(binding.storyImage)
.load(uiState.imageList.getOrNull(uiState.currentImage + 1))
.preload()
return false
}
})
.into(binding.storyImage)
}
}
}
}
//Pause when clicked on text field
binding.storyReplyField.editText?.setOnFocusChangeListener { view, isFocused ->
if (view.isInTouchMode && isFocused) {
view.performClick() // picks up first tap
}
}
binding.storyReplyField.editText?.setOnClickListener {
if (!model.uiState.value.paused) {
model.pause()
}
}
binding.storyReplyField.editText?.doAfterTextChanged {
it?.let { text ->
val string = text.toString()
if(string != model.uiState.value.reply) model.replyChanged(string)
}
}
binding.storyReplyField.setEndIconOnClickListener {
binding.storyReplyField.editText?.text?.let { text ->
model.sendReply(text)
}
}
binding.storyErrorCard.setOnClickListener{
model.dismissError()
}
model.count.observe(this) { state ->
// Render state in UI
model.uiState.value.durationList.getOrNull(model.uiState.value.currentImage)?.let {
storyProgress.progress = 1 - (state/it.toFloat())
binding.storyProgressImage.postInvalidate()
}
}
binding.pause.setOnClickListener {
//Set the button's appearance
it.isSelected = !it.isSelected
model.pause()
}
val authorOnClickListener = OnClickListener {
if (!model.uiState.value.paused) {
model.pause()
}
model.currentProfileId()?.let {
lifecycleScope.launch {
Account.openAccountFromId(
it,
apiHolder.api ?: apiHolder.setToCurrentUser(),
this@StoriesActivity
)
}
}
}
binding.storyAuthorProfilePicture.setOnClickListener(authorOnClickListener)
binding.storyAuthor.setOnClickListener(authorOnClickListener)
val onTouchListener = OnTouchListener { v, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> if (!model.uiState.value.paused) {
model.pause()
}
MotionEvent.ACTION_UP -> if(event.eventTime - event.downTime < 500) {
v.performClick()
return@OnTouchListener false
} else model.pause()
}
true
}
binding.viewMiddle.setOnTouchListener{ v, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> model.pause()
MotionEvent.ACTION_UP -> if(event.eventTime - event.downTime < 500) {
v.performClick()
return@setOnTouchListener false
} else model.pause()
}
true
}
binding.viewLeft.setOnTouchListener(onTouchListener)
binding.viewRight.setOnTouchListener(onTouchListener)
binding.viewRight.setOnClickListener {
model.goToNext()
}
binding.viewLeft.setOnClickListener {
model.goToPrevious()
}
}
}

View File

@ -0,0 +1,210 @@
package org.pixeldroid.app.stories
import android.os.CountDownTimer
import android.text.Editable
import androidx.annotation.StringRes
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.pixeldroid.app.R
import org.pixeldroid.app.utils.api.objects.CarouselUserContainer
import org.pixeldroid.app.utils.api.objects.Story
import org.pixeldroid.app.utils.api.objects.StoryCarousel
import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
import java.time.Instant
import javax.inject.Inject
data class StoriesUiState(
val profilePicture: String? = null,
val username: String? = null,
val age: Instant? = null,
val currentImage: Int = 0,
val imageList: List<String> = emptyList(),
val durationList: List<Int> = emptyList(),
val paused: Boolean = false,
@StringRes
val errorMessage: Int? = null,
@StringRes
val snackBar: Int? = null,
val reply: String = ""
)
@HiltViewModel
class StoriesViewModel @Inject constructor(state: SavedStateHandle,
db: AppDatabase,
private val apiHolder: PixelfedAPIHolder) : ViewModel() {
private val carousel: StoryCarousel? = state[StoriesActivity.STORY_CAROUSEL]
private val userId: String? = state[StoriesActivity.STORY_CAROUSEL_USER_ID]
private val selfCarousel: Array<Story>? = state[StoriesActivity.STORY_CAROUSEL_SELF]
private var currentAccount: CarouselUserContainer?
private val _uiState: MutableStateFlow<StoriesUiState>
val uiState: StateFlow<StoriesUiState>
val count = MutableLiveData<Float>()
private var timer: CountDownTimer? = null
init {
currentAccount =
if (selfCarousel != null) {
db.userDao().getActiveUser()?.let { CarouselUserContainer(it, selfCarousel.toList()) }
} else carousel?.nodes?.firstOrNull { it?.user?.id == userId }
_uiState = MutableStateFlow(newUiStateFromCurrentAccount())
uiState = _uiState
startTimerForCurrent()
}
private fun setTimer(timerLength: Float) {
count.value = timerLength
timer = object: CountDownTimer((timerLength * 1000).toLong(), 50){
override fun onTick(millisUntilFinished: Long) {
count.value = millisUntilFinished.toFloat() / 1000
}
override fun onFinish() {
goToNext()
}
}
}
private fun newUiStateFromCurrentAccount(): StoriesUiState = StoriesUiState(
profilePicture = currentAccount?.user?.avatar,
age = currentAccount?.nodes?.getOrNull(0)?.created_at,
username = currentAccount?.user?.username, //TODO check if not username_acct, think about falling back on other option?
errorMessage = null,
currentImage = 0,
imageList = currentAccount?.nodes?.mapNotNull { it?.src } ?: emptyList(),
durationList = currentAccount?.nodes?.mapNotNull { it?.duration } ?: emptyList()
)
private fun goTo(index: Int){
if((0 until uiState.value.imageList.size).contains(index)) {
_uiState.update { currentUiState ->
currentUiState.copy(
currentImage = index,
age = currentAccount?.nodes?.getOrNull(index)?.created_at,
paused = false
)
}
} else {
if(selfCarousel != null) return
val currentUserId = currentAccount?.user?.id
val currentAccountIndex = carousel?.nodes?.indexOfFirst { it?.user?.id == currentUserId } ?: return
currentAccount = when (index) {
uiState.value.imageList.size -> {
// Go to next user
if(currentAccountIndex + 1 >= carousel.nodes.size) return
carousel.nodes.getOrNull(currentAccountIndex + 1)
}
-1 -> {
// Go to previous user
if(currentAccountIndex <= 0) return
carousel.nodes.getOrNull(currentAccountIndex - 1)
}
else -> return // Do nothing, given index does not make sense
}
_uiState.update { newUiStateFromCurrentAccount() }
}
timer?.cancel()
startTimerForCurrent()
}
fun goToNext() {
viewModelScope.launch {
try {
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
val story = currentAccount?.nodes?.getOrNull(uiState.value.currentImage)
if (story?.seen == true){
//TODO update seen when marked successfully as seen?
story.id?.let { api.storySeen(it) }
}
} catch (exception: Exception){
_uiState.update { currentUiState ->
currentUiState.copy(errorMessage = R.string.story_could_not_see)
}
}
}
goTo(uiState.value.currentImage + 1)
}
fun goToPrevious() = goTo(uiState.value.currentImage - 1)
private fun startTimerForCurrent(){
uiState.value.let {
it.durationList.getOrNull(it.currentImage)?.toLong()?.let { time ->
setTimer(time.toFloat())
timer?.start()
}
}
}
fun pause() {
if(_uiState.value.paused){
timer?.start()
} else {
timer?.cancel()
count.value?.let { setTimer(it) }
}
_uiState.update { currentUiState ->
currentUiState.copy(paused = !currentUiState.paused)
}
}
fun sendReply(text: Editable) {
viewModelScope.launch {
try {
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
currentStoryId()?.let { api.storyComment(it, text.toString()) }
_uiState.update { currentUiState ->
currentUiState.copy(snackBar = R.string.sent_reply_story)
}
} catch (exception: Exception){
_uiState.update { currentUiState ->
currentUiState.copy(errorMessage = R.string.story_reply_error)
}
}
}
}
private fun currentStoryId(): String? = currentAccount?.nodes?.getOrNull(uiState.value.currentImage)?.id
fun replyChanged(text: String) {
_uiState.update { currentUiState ->
currentUiState.copy(reply = text)
}
}
fun dismissError() {
_uiState.update { currentUiState ->
currentUiState.copy(errorMessage = null)
}
}
fun shownSnackbar() {
_uiState.update { currentUiState ->
currentUiState.copy(snackBar = null)
}
}
fun currentProfileId(): String? = currentAccount?.user?.id
}

View File

@ -0,0 +1,210 @@
package org.pixeldroid.app.stories
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.RenderEffect
import android.graphics.Shader
import android.os.Build
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import kotlinx.coroutines.launch
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.StoryCarouselBinding
import org.pixeldroid.app.databinding.StoryCarouselItemBinding
import org.pixeldroid.app.databinding.StoryCarouselSelfBinding
import org.pixeldroid.app.postCreation.camera.CameraActivity
import org.pixeldroid.app.postCreation.camera.CameraFragment
import org.pixeldroid.app.utils.api.objects.CarouselUserContainer
import org.pixeldroid.app.utils.api.objects.Story
import org.pixeldroid.app.utils.api.objects.StoryCarousel
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
/**
* Adapter that has either 1 or 0 items, to show stories widget or not
*/
class StoriesAdapter(val lifecycleScope: LifecycleCoroutineScope, val apiHolder: PixelfedAPIHolder) : RecyclerView.Adapter<StoryCarouselViewHolder>() {
var carousel: StoryCarousel? = null
/**
* Whether to show stories or not.
*
* Changing this property will immediately notify the Adapter to change the item it's
* presenting.
*/
var showStories: Boolean = false
set(newValue) {
val oldValue = field
if (oldValue && !newValue) {
notifyItemRemoved(0)
} else if (newValue && !oldValue) {
notifyItemInserted(0)
} else if (oldValue && newValue) {
notifyItemChanged(0)
}
field = newValue
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StoryCarouselViewHolder {
return StoryCarouselViewHolder.create(parent, ::noStories)
}
override fun onBindViewHolder(holder: StoryCarouselViewHolder, position: Int) {
holder.bind(carousel)
}
override fun getItemViewType(position: Int): Int = 0
override fun getItemCount(): Int = if (showStories) 1 else 0
private fun noStories(){
showStories = false
}
private fun gotStories(newCarousel: StoryCarousel) {
carousel = newCarousel
showStories = true
}
fun refreshStories(){
lifecycleScope.launch {
try{
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
val carousel = api.carousel()
// If there are stories from someone else or our stories to show, show them
if (carousel.nodes?.isEmpty() == false || carousel.self?.nodes?.isEmpty() == false) {
// Pass carousel to adapter
gotStories(carousel)
} else {
noStories()
}
} catch (exception: Exception){
noStories()
}
}
}
}
class StoryCarouselViewHolder(val binding: StoryCarouselBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(carousel: StoryCarousel?) {
val adapter = StoriesListAdapter()
binding.storyCarousel.adapter = adapter
carousel?.let { adapter.initCarousel(it) }
}
companion object {
fun create(parent: ViewGroup, noStories: () -> Unit): StoryCarouselViewHolder {
val itemBinding = StoryCarouselBinding.inflate(
LayoutInflater.from(parent.context), parent, false
)
return StoryCarouselViewHolder(itemBinding)
}
}
}
class StoriesListAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var storyCarousel: StoryCarousel? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if(viewType == R.layout.story_carousel_self){
val v = StoryCarouselSelfBinding.inflate(LayoutInflater.from(parent.context), parent, false)
v.myStory.visibility =
if (storyCarousel?.self?.nodes?.isEmpty() == false) View.VISIBLE
else View.GONE
AddViewHolder(v)
}
else {
val v = StoryCarouselItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
ViewHolder(v)
}
}
override fun getItemViewType(position: Int): Int {
return if(position == 0) R.layout.story_carousel_self
else R.layout.story_carousel_item
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if(position > 0) {
val carouselPosition = position - 1
storyCarousel?.nodes?.get(carouselPosition)?.let { (holder as ViewHolder).bindItem(it) }
holder.itemView.setOnClickListener {
storyCarousel?.nodes?.get(carouselPosition)?.user?.id?.let { userId ->
val intent = Intent(holder.itemView.context, StoriesActivity::class.java)
intent.putExtra(StoriesActivity.STORY_CAROUSEL, storyCarousel)
intent.putExtra(StoriesActivity.STORY_CAROUSEL_USER_ID, userId)
holder.itemView.context.startActivity(intent)
}
}
} else {
storyCarousel?.self?.nodes?.let { (holder as? AddViewHolder)?.bindItem(it.filterNotNull()) }
}
}
override fun getItemCount(): Int {
// If the storyCarousel is not set, the carousel is not shown, so itemCount of 0
return (storyCarousel?.nodes?.size?.plus(1)) ?: 0
}
@SuppressLint("NotifyDataSetChanged")
fun initCarousel(carousel: StoryCarousel){
storyCarousel = carousel
notifyDataSetChanged()
}
class AddViewHolder(private val itemBinding: StoryCarouselSelfBinding) : RecyclerView.ViewHolder(itemBinding.root) {
fun bindItem(nodes: List<Story>) {
itemBinding.addStory.setOnClickListener {
val intent = Intent(itemView.context, CameraActivity::class.java)
intent.putExtra(CameraFragment.CAMERA_ACTIVITY_STORY, true)
itemView.context.startActivity(intent)
}
itemBinding.myStory.setOnClickListener {
val intent = Intent(itemView.context, StoriesActivity::class.java)
intent.putExtra(StoriesActivity.STORY_CAROUSEL_SELF, nodes.toTypedArray())
itemView.context.startActivity(intent)
}
// Only show image on new Android versions, because the transformations need it and the
// text is not legible without the transformations
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
Glide.with(itemBinding.root).load(nodes.firstOrNull()?.src).into(itemBinding.carouselImageView)
val value = 70 * 255 / 100
val darkFilterRenderEffect = PorterDuffColorFilter(Color.argb(value, 0, 0, 0), PorterDuff.Mode.SRC_ATOP)
val blurRenderEffect =
RenderEffect.createBlurEffect(
4f, 4f, Shader.TileMode.MIRROR
)
val combinedEffect = RenderEffect.createColorFilterEffect(darkFilterRenderEffect, blurRenderEffect)
itemBinding.carouselImageView.setRenderEffect(combinedEffect)
}
}
}
class ViewHolder(private val itemBinding: StoryCarouselItemBinding) :
RecyclerView.ViewHolder(itemBinding.root) {
fun bindItem(user: CarouselUserContainer) {
Glide.with(itemBinding.root).load(user.nodes?.firstOrNull()?.src).into(itemBinding.carouselImageView)
Glide.with(itemBinding.root).load(user.user?.avatar).circleCrop().into(itemBinding.storyAuthorProfilePicture)
itemBinding.username.text = user.user?.username ?: "" //TODO check which one to use here!
}
}
}

View File

@ -0,0 +1,72 @@
package org.pixeldroid.app.stories
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.drawable.Drawable
/**
* Copied & adapted from AntennaPod's EchoProgress class because it looked great and is very simple
* AntennaPod/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/EchoProgress.java
*/
class StoryProgress(private val numStories: Int) : Drawable() {
private val paint: Paint = Paint().apply {
flags = Paint.ANTI_ALIAS_FLAG
style = Paint.Style.STROKE
strokeJoin = Paint.Join.ROUND
strokeCap = Paint.Cap.ROUND
color = -0x1
}
var progress = 0f
var currentStory: Int = 0
override fun draw(canvas: Canvas) {
paint.strokeWidth = 0.5f * bounds.height()
val y = 0.5f * bounds.height()
val sectionWidth = 1.0f * bounds.width() / numStories
val sectionPadding = 0.03f * sectionWidth
// Iterate over stories
for (i in 0 until numStories) {
if (i < currentStory) {
// If current drawing position is smaller than current story, the paint we will use
// should be opaque: this story is already "seen"
paint.alpha = 255
} else {
// Otherwise it should be somewhat transparent, denoting it is not yet seen
paint.alpha = 100
}
// Draw an entire line with the paint, for now ignoring partial progress within the
// current story
canvas.drawLine(
i * sectionWidth + sectionPadding,
y,
(i + 1) * sectionWidth - sectionPadding,
y,
paint
)
// If current position is equal to progress, we are drawing the current story. Thus we
// should account for partial progress and paint the beginning of the line opaquely
if (i == currentStory) {
paint.alpha = 255
canvas.drawLine(
currentStory * sectionWidth + sectionPadding,
y,
currentStory * sectionWidth + sectionPadding + progress * (sectionWidth - 2 * sectionPadding),
y,
paint
)
}
}
}
@Deprecated("Deprecated in Java")
override fun getOpacity(): Int {
return PixelFormat.TRANSLUCENT
}
override fun setAlpha(alpha: Int) {}
override fun setColorFilter(cf: ColorFilter?) {}
}

View File

@ -1,25 +1,20 @@
package org.pixeldroid.app.utils package org.pixeldroid.app.utils
import android.os.Bundle import dagger.hilt.android.AndroidEntryPoint
import androidx.appcompat.app.AppCompatActivity
import org.pixeldroid.app.utils.db.AppDatabase import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.di.PixelfedAPIHolder import org.pixeldroid.app.utils.di.PixelfedAPIHolder
import javax.inject.Inject import javax.inject.Inject
open class BaseActivity : AppCompatActivity() { @AndroidEntryPoint
open class BaseActivity : org.pixeldroid.common.ThemedActivity() {
@Inject @Inject
lateinit var db: AppDatabase lateinit var db: AppDatabase
@Inject @Inject
lateinit var apiHolder: PixelfedAPIHolder lateinit var apiHolder: PixelfedAPIHolder
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
(this.application as PixelDroidApplication).getAppComponent().inject(this)
}
override fun onSupportNavigateUp(): Boolean { override fun onSupportNavigateUp(): Boolean {
onBackPressed() onBackPressedDispatcher.onBackPressed()
return true return true
} }
} }

View File

@ -1,7 +1,10 @@
package org.pixeldroid.app.utils package org.pixeldroid.app.utils
import android.os.Bundle import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import org.pixeldroid.app.R
import org.pixeldroid.app.utils.db.AppDatabase import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.di.PixelfedAPIHolder import org.pixeldroid.app.utils.di.PixelfedAPIHolder
import javax.inject.Inject import javax.inject.Inject
@ -9,6 +12,7 @@ import javax.inject.Inject
/** /**
* Base Fragment, for dependency injection and other things common to a lot of the fragments * Base Fragment, for dependency injection and other things common to a lot of the fragments
*/ */
@AndroidEntryPoint
open class BaseFragment: Fragment() { open class BaseFragment: Fragment() {
@Inject @Inject
@ -17,9 +21,18 @@ open class BaseFragment: Fragment() {
@Inject @Inject
lateinit var db: AppDatabase lateinit var db: AppDatabase
override fun onCreate(savedInstanceState: Bundle?) { internal val requestPermissionDownloadPic =
super.onCreate(savedInstanceState) registerForActivityResult(
(requireActivity().application as PixelDroidApplication).getAppComponent().inject(this) ActivityResultContracts.RequestPermission()
} ) { isGranted: Boolean ->
if (!isGranted) {
context?.let {
MaterialAlertDialogBuilder(it)
.setMessage(R.string.write_permission_download_pic)
.setNegativeButton(android.R.string.ok) { _, _ -> }
.show()
}
}
}
} }

View File

@ -1,11 +0,0 @@
package org.pixeldroid.app.utils
import android.os.Bundle
open class BaseThemedWithBarActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Set theme when we chose one
themeActionBar()?.let { setTheme(it) }
super.onCreate(savedInstanceState)
}
}

View File

@ -1,11 +0,0 @@
package org.pixeldroid.app.utils
import android.os.Bundle
open class BaseThemedWithoutBarActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Set theme when we chose one
themeNoActionBar()?.let { setTheme(it) }
super.onCreate(savedInstanceState)
}
}

View File

@ -24,6 +24,7 @@ fun setProfileImageFromURL(view : View, url : String?, image : ImageView) {
* @param image, the imageView into which we will load the image * @param image, the imageView into which we will load the image
*/ */
fun setSquareImageFromURL(view : View, url : String?, image : ImageView, blurhash: String? = null) { fun setSquareImageFromURL(view : View, url : String?, image : ImageView, blurhash: String? = null) {
//TODO performance: placeholder here takes a lot of time to compute and this is not async!
Glide.with(view).load(url).placeholder( Glide.with(view).load(url).placeholder(
blurhash?.let { BlurHashDecoder.blurHashBitmap(view.resources, it, 32, 32) } blurhash?.let { BlurHashDecoder.blurHashBitmap(view.resources, it, 32, 32) }
).apply(RequestOptions().centerCrop()).into(image) ).apply(RequestOptions().centerCrop()).into(image)

View File

@ -3,14 +3,12 @@ package org.pixeldroid.app.utils
import android.app.Application import android.app.Application
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColors
import dagger.hilt.android.HiltAndroidApp
import org.ligi.tracedroid.TraceDroid import org.ligi.tracedroid.TraceDroid
import org.pixeldroid.app.utils.di.*
@HiltAndroidApp
class PixelDroidApplication: Application() { class PixelDroidApplication: Application() {
private lateinit var mApplicationComponent: ApplicationComponent
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -19,18 +17,7 @@ class PixelDroidApplication: Application() {
val sharedPreferences = val sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(this) PreferenceManager.getDefaultSharedPreferences(this)
setThemeFromPreferences(sharedPreferences, resources) setThemeFromPreferences(sharedPreferences, resources)
mApplicationComponent = DaggerApplicationComponent
.builder()
.applicationModule(ApplicationModule(this))
.databaseModule(DatabaseModule(applicationContext))
.aPIModule(APIModule())
.build()
mApplicationComponent.inject(this)
DynamicColors.applyToActivitiesIfAvailable(this) DynamicColors.applyToActivitiesIfAvailable(this)
} }
fun getAppComponent(): ApplicationComponent {
return mApplicationComponent
}
} }

View File

@ -1,28 +1,25 @@
package org.pixeldroid.app.utils package org.pixeldroid.app.utils
import android.content.* import android.content.ActivityNotFoundException
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import android.graphics.ImageDecoder
import android.graphics.Matrix
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.MediaStore
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.WindowManager import android.view.WindowManager
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.StyleRes
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.exifinterface.media.ExifInterface
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
@ -34,7 +31,7 @@ import okhttp3.HttpUrl
import org.pixeldroid.app.R import org.pixeldroid.app.R
import java.time.Instant import java.time.Instant
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.* import java.util.Locale
import kotlin.properties.ReadWriteProperty import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
@ -161,30 +158,6 @@ fun setThemeFromPreferences(preferences: SharedPreferences, resources: Resources
} }
} }
@StyleRes
fun Context.themeNoActionBar(): Int? {
return when(PreferenceManager.getDefaultSharedPreferences(this).getInt("themeColor", 0)) {
// No theme was chosen: the user wants to use the system dynamic color (from wallpaper for example)
-1 -> null
1 -> R.style.AppTheme2_NoActionBar
2 -> R.style.AppTheme3_NoActionBar
3 -> R.style.AppTheme4_NoActionBar
else -> R.style.AppTheme5_NoActionBar
}
}
@StyleRes
fun Context.themeActionBar(): Int? {
return when(PreferenceManager.getDefaultSharedPreferences(this).getInt("themeColor", 0)) {
// No theme was chosen: the user wants to use the system dynamic color (from wallpaper for example)
-1 -> null
1 -> R.style.AppTheme2
2 -> R.style.AppTheme3
3 -> R.style.AppTheme4
else -> R.style.AppTheme5
}
}
@ColorInt @ColorInt
fun Context.getColorFromAttr(@AttrRes attrColor: Int): Int = MaterialColors.getColor(this, attrColor, Color.BLACK) fun Context.getColorFromAttr(@AttrRes attrColor: Int): Int = MaterialColors.getColor(this, attrColor, Color.BLACK)

View File

@ -23,6 +23,7 @@ import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.* import retrofit2.http.*
import retrofit2.http.Field import retrofit2.http.Field
import java.time.Instant import java.time.Instant
import java.util.concurrent.TimeUnit
/* /*
@ -51,7 +52,9 @@ interface PixelfedAPI {
.client( .client(
OkHttpClient().newBuilder().addNetworkInterceptor(headerInterceptor) OkHttpClient().newBuilder().addNetworkInterceptor(headerInterceptor)
// Only do secure-ish TLS connections (no HTTP or very old SSL/TLS) // Only do secure-ish TLS connections (no HTTP or very old SSL/TLS)
.connectionSpecs(listOf(ConnectionSpec.MODERN_TLS)).build() .connectionSpecs(listOf(ConnectionSpec.MODERN_TLS))
.readTimeout(20, TimeUnit.SECONDS)
.build()
) )
.build().create(PixelfedAPI::class.java) .build().create(PixelfedAPI::class.java)
} }
@ -74,6 +77,7 @@ interface PixelfedAPI {
OkHttpClient().newBuilder().addNetworkInterceptor(headerInterceptor) OkHttpClient().newBuilder().addNetworkInterceptor(headerInterceptor)
// Only do secure-ish TLS connections (no HTTP or very old SSL/TLS) // Only do secure-ish TLS connections (no HTTP or very old SSL/TLS)
.connectionSpecs(listOf(ConnectionSpec.MODERN_TLS)) .connectionSpecs(listOf(ConnectionSpec.MODERN_TLS))
.readTimeout(20, TimeUnit.SECONDS)
.authenticator(TokenAuthenticator(user, db, pixelfedAPIHolder)) .authenticator(TokenAuthenticator(user, db, pixelfedAPIHolder))
.addInterceptor { .addInterceptor {
it.request().newBuilder().run { it.request().newBuilder().run {
@ -161,6 +165,7 @@ interface PixelfedAPI {
@Field("poll[expires_in]") poll_expires: List<String>? = null, @Field("poll[expires_in]") poll_expires: List<String>? = null,
@Field("poll[multiple]") poll_multiple: List<String>? = null, @Field("poll[multiple]") poll_multiple: List<String>? = null,
@Field("poll[hide_totals]") poll_hideTotals: List<String>? = null, @Field("poll[hide_totals]") poll_hideTotals: List<String>? = null,
//FIXME this should be able to take a boolean or at least "true"/"false" but only "0"/"1" works
@Field("sensitive") sensitive: Int? = null, @Field("sensitive") sensitive: Int? = null,
@Field("spoiler_text") spoiler_text: String? = null, @Field("spoiler_text") spoiler_text: String? = null,
@Field("visibility") visibility: String = "public", @Field("visibility") visibility: String = "public",
@ -231,6 +236,43 @@ interface PixelfedAPI {
@Query("post_id") post_id: String, @Query("post_id") post_id: String,
) )
@GET("/api/pixelfed/v1/stories/self-carousel")
suspend fun carousel(): StoryCarousel
@POST("/api/v1.1/stories/seen")
suspend fun storySeen(
@Query("id") id: String
)
@POST("/api/v1.1/stories/comment")
suspend fun storyComment(
@Query("sid") sid: String,
@Query("caption") caption: String
)
@Multipart
@POST("/api/v1.1/stories/add")
fun storyUpload(
@Part file: MultipartBody.Part,
// The API takes this value but then overwrites it in /api/v1.1/stories/publish, so ignore this
@Part duration: MultipartBody.Part? = null,
): Observable<Attachment>
@POST("/api/v1.1/stories/publish")
suspend fun storyPublish(
@Query("media_id") media_id: String,
//From 0 to 30, duration in seconds of the story
@Query("duration") duration: Int = 10,
//FIXME this should be able to take a boolean or at least "true"/"false" but only "0"/"1" works. Same issue as sensitive boolean in postStatus
@Query("can_reply") can_reply: String,
@Query("can_react") can_react: String,
)
@POST("/api/v1.1/stories/self-expire/{id}")
suspend fun deleteCarousel(
@Path("id") storyId: String
)
//Used in our case to retrieve comments for a given status //Used in our case to retrieve comments for a given status
@GET("/api/v1/statuses/{id}/context") @GET("/api/v1/statuses/{id}/context")
suspend fun statusComments( suspend fun statusComments(
@ -296,18 +338,31 @@ interface PixelfedAPI {
@Header("Authorization") authorization: String? = null @Header("Authorization") authorization: String? = null
): Account ): Account
//@Multipart
@PATCH("/api/v1/accounts/update_credentials") @PATCH("/api/v1/accounts/update_credentials")
suspend fun updateCredentials( suspend fun updateCredentials(
@Query(value = "display_name") displayName: String?, @Query(value = "display_name") displayName: String?,
@Query(value = "note") note: String?, @Query(value = "note") note: String?,
@Query(value = "locked") locked: Boolean?, @Query(value = "locked") locked: Boolean?,
// @Part avatar: MultipartBody.Part?,
): Account ): Account
/**
* Pixelfed uses PHP, multipart uploads don't work through PATCH so we use POST as suggested
* here: https://github.com/pixelfed/pixelfed/issues/4250
* However, changing to POST breaks the upload on Mastodon.
*
* To have this work on Pixelfed and Mastodon without special logic to distinguish the two,
* we'll have to wait for PHP 8.4 and https://wiki.php.net/rfc/rfc1867-non-post
* which should come out end of 2024
*/
@Multipart
@POST("/api/v1/accounts/update_credentials")
fun updateProfilePicture(
@Part avatar: MultipartBody.Part?
): Observable<Account>
@Multipart @Multipart
@PATCH("/api/v1/accounts/update_credentials") @PATCH("/api/v1/accounts/update_credentials")
fun updateProfilePicture( fun updateProfilePictureMastodon(
@Part avatar: MultipartBody.Part? @Part avatar: MultipartBody.Part?
): Observable<Account> ): Observable<Account>

View File

@ -57,11 +57,13 @@ data class Account(
suspend fun openAccountFromId(id: String, api : PixelfedAPI, context: Context) { suspend fun openAccountFromId(id: String, api : PixelfedAPI, context: Context) {
val account = try { val account = try {
api.getAccount(id) api.getAccount(id)
} catch (exception: IOException) { } catch (exception: Exception) {
Log.e("GET ACCOUNT ERROR", exception.toString()) val toLog = if (exception is HttpException) {
return exception.code().toString()
} catch (exception: HttpException) { } else {
Log.e("ERROR CODE", exception.code().toString()) exception.toString()
}
Log.e("GET ACCOUNT ERROR", toLog)
return return
} }
//Open the account page in a separate activity //Open the account page in a separate activity

View File

@ -18,6 +18,12 @@ data class Attachment(
//Deprecated attributes //Deprecated attributes
val text_url: String? = null, //URL val text_url: String? = null, //URL
//Pixelfed's Story upload response... TODO make the server return a regular Attachment?
val msg: String? = null,
val media_id: String? = null,
val media_url: String? = null,
val media_type: String? = null,
) : Serializable { ) : Serializable {
enum class AttachmentType: Serializable { enum class AttachmentType: Serializable {
unknown, image, gifv, video, audio unknown, image, gifv, video, audio

View File

@ -1,8 +1,10 @@
package org.pixeldroid.app.utils.api.objects package org.pixeldroid.app.utils.api.objects
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.app.DownloadManager import android.app.DownloadManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.database.Cursor import android.database.Cursor
import android.net.Uri import android.net.Uri
import android.os.Environment import android.os.Environment
@ -11,6 +13,7 @@ import androidx.core.net.toUri
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.posts.getDomain import org.pixeldroid.app.posts.getDomain
import org.pixeldroid.app.utils.getMimeType
import java.io.File import java.io.File
import java.io.Serializable import java.io.Serializable
import java.time.Instant import java.time.Instant
@ -148,11 +151,13 @@ open class Status(
) )
val file = path.toUri() val file = path.toUri()
val shareIntent: Intent = Intent.createChooser(Intent().apply { val shareIntent: Intent = Intent.createChooser(Intent().apply {
action = Intent.ACTION_SEND action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, file) putExtra(Intent.EXTRA_STREAM, file)
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
type = "image/$ext" type = file.getMimeType(context.contentResolver)
}, null) }, null)
context.startActivity(shareIntent) context.startActivity(shareIntent)

View File

@ -0,0 +1,43 @@
package org.pixeldroid.app.utils.api.objects
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import java.io.Serializable
import java.time.Instant
data class StoryCarousel(
val self: CarouselUserContainer?,
val nodes: List<CarouselUserContainer?>?
): Serializable
data class CarouselUser(
val id: String?,
val username: String?,
val username_acct: String?,
val avatar: String?, // URL to account avatar
val local: Boolean?, // Is this story from the local instance?
val is_author: Boolean?, // Is this me? (seems redundant with id)
): Serializable
/**
* Container with a description of the [user] and a list of stories ([nodes])
*/
data class CarouselUserContainer(
val user: CarouselUser?,
val nodes: List<Story?>?,
): Serializable {
constructor(user: UserDatabaseEntity, nodes: List<Story?>?) : this(
CarouselUser(user.user_id, user.username, null, user.avatar_static,
local = true,
is_author = true
), nodes)
}
data class Story(
val id: String?,
val pid: String?, // id of author
val type: String?, //TODO make enum of this? examples: "photo", ???
val src: String?, // URL to photo of story
val duration: Int?, //Time in seconds that the Story should be shown
val seen: Boolean?, //Indication of whether this story has been seen. Set to true using carouselSeen
val created_at: Instant?, //ISO 8601 Datetime
): Serializable

View File

@ -22,7 +22,7 @@ import org.pixeldroid.app.utils.api.objects.Notification
PublicFeedStatusDatabaseEntity::class, PublicFeedStatusDatabaseEntity::class,
Notification::class Notification::class
], ],
version = 5 version = 6
) )
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
@ -44,4 +44,9 @@ val MIGRATION_4_5 = object : Migration(4, 5) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE instances ADD COLUMN videoEnabled INTEGER NOT NULL DEFAULT 1") database.execSQL("ALTER TABLE instances ADD COLUMN videoEnabled INTEGER NOT NULL DEFAULT 1")
} }
}
val MIGRATION_5_6 = object : Migration(5, 6) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE instances ADD COLUMN pixelfed INTEGER NOT NULL DEFAULT 1")
}
} }

View File

@ -13,41 +13,58 @@ import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity.Companion.DEF
import org.pixeldroid.app.utils.normalizeDomain import org.pixeldroid.app.utils.normalizeDomain
import java.lang.IllegalArgumentException import java.lang.IllegalArgumentException
fun addUser(db: AppDatabase, account: Account, instance_uri: String, activeUser: Boolean = true, suspend fun addUser(
accessToken: String, refreshToken: String?, clientId: String, clientSecret: String) { db: AppDatabase, account: Account, instance_uri: String, activeUser: Boolean = true,
accessToken: String, refreshToken: String?, clientId: String, clientSecret: String,
) {
db.userDao().insertOrUpdate( db.userDao().insertOrUpdate(
UserDatabaseEntity( UserDatabaseEntity(
user_id = account.id!!, user_id = account.id!!,
instance_uri = normalizeDomain(instance_uri), instance_uri = normalizeDomain(instance_uri),
username = account.username!!, username = account.username!!,
display_name = account.getDisplayName(), display_name = account.getDisplayName(),
avatar_static = account.anyAvatar().orEmpty(), avatar_static = account.anyAvatar().orEmpty(),
isActive = activeUser, isActive = activeUser,
accessToken = accessToken, accessToken = accessToken,
refreshToken = refreshToken, refreshToken = refreshToken,
clientId = clientId, clientId = clientId,
clientSecret = clientSecret clientSecret = clientSecret
) )
) )
} }
fun storeInstance(db: AppDatabase, nodeInfo: NodeInfo?, instance: Instance? = null) { suspend fun updateUserInfoDb(db: AppDatabase, account: Account) {
val user = db.userDao().getActiveUser()!!
db.userDao().updateUserAccountDetails(
account.username.orEmpty(),
account.display_name.orEmpty(),
account.anyAvatar().orEmpty(),
user.user_id,
user.instance_uri
)
}
suspend fun storeInstance(db: AppDatabase, nodeInfo: NodeInfo?, instance: Instance? = null) {
val dbInstance: InstanceDatabaseEntity = nodeInfo?.run { val dbInstance: InstanceDatabaseEntity = nodeInfo?.run {
InstanceDatabaseEntity( InstanceDatabaseEntity(
uri = normalizeDomain(metadata?.config?.site?.url!!), uri = normalizeDomain(metadata?.config?.site?.url!!),
title = metadata.config.site.name!!, title = metadata.config.site.name!!,
maxStatusChars = metadata.config.uploader?.max_caption_length!!.toInt(), maxStatusChars = metadata.config.uploader?.max_caption_length!!.toInt(),
maxPhotoSize = metadata.config.uploader.max_photo_size?.toIntOrNull() ?: DEFAULT_MAX_PHOTO_SIZE, maxPhotoSize = metadata.config.uploader.max_photo_size?.toIntOrNull()
?: DEFAULT_MAX_PHOTO_SIZE,
// Pixelfed doesn't distinguish between max photo and video size // Pixelfed doesn't distinguish between max photo and video size
maxVideoSize = metadata.config.uploader.max_photo_size?.toIntOrNull() ?: DEFAULT_MAX_VIDEO_SIZE, maxVideoSize = metadata.config.uploader.max_photo_size?.toIntOrNull()
?: DEFAULT_MAX_VIDEO_SIZE,
albumLimit = metadata.config.uploader.album_limit?.toIntOrNull() ?: DEFAULT_ALBUM_LIMIT, albumLimit = metadata.config.uploader.album_limit?.toIntOrNull() ?: DEFAULT_ALBUM_LIMIT,
videoEnabled = metadata.config.features?.video ?: DEFAULT_VIDEO_ENABLED videoEnabled = metadata.config.features?.video ?: DEFAULT_VIDEO_ENABLED,
pixelfed = metadata.software?.repo?.contains("pixelfed", ignoreCase = true) == true
) )
} ?: instance?.run { } ?: instance?.run {
InstanceDatabaseEntity( InstanceDatabaseEntity(
uri = normalizeDomain(uri.orEmpty()), uri = normalizeDomain(uri.orEmpty()),
title = title.orEmpty(), title = title.orEmpty(),
maxStatusChars = max_toot_chars?.toInt() ?: DEFAULT_MAX_TOOT_CHARS, maxStatusChars = max_toot_chars?.toInt() ?: DEFAULT_MAX_TOOT_CHARS,
pixelfed = false
) )
} ?: throw IllegalArgumentException("Cannot store instance where both are null") } ?: throw IllegalArgumentException("Cannot store instance where both are null")

View File

@ -1,27 +1,33 @@
package org.pixeldroid.app.utils.db.dao package org.pixeldroid.app.utils.db.dao
import androidx.room.* import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
@Dao @Dao
interface InstanceDao { interface InstanceDao {
@Query("SELECT * FROM instances")
fun getAll(): List<InstanceDatabaseEntity>
@Query("SELECT * FROM instances WHERE uri=:instanceUri") @Query("SELECT * FROM instances WHERE uri=:instanceUri")
fun getInstance(instanceUri: String): InstanceDatabaseEntity fun getInstance(instanceUri: String): InstanceDatabaseEntity
@Query("SELECT * FROM instances WHERE uri=(SELECT users.instance_uri FROM users WHERE isActive=1)")
fun getActiveInstance(): InstanceDatabaseEntity
/** /**
* Insert an instance, if it already exists return -1 * Insert an instance, if it already exists return -1
*/ */
@Insert(onConflict = OnConflictStrategy.IGNORE) @Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertInstance(instance: InstanceDatabaseEntity): Long suspend fun insertInstance(instance: InstanceDatabaseEntity): Long
@Update @Update
fun updateInstance(instance: InstanceDatabaseEntity) suspend fun updateInstance(instance: InstanceDatabaseEntity)
@Transaction @Transaction
fun insertOrUpdate(instance: InstanceDatabaseEntity) { suspend fun insertOrUpdate(instance: InstanceDatabaseEntity) {
if (insertInstance(instance) == -1L) { if (insertInstance(instance) == -1L) {
updateInstance(instance) updateInstance(instance)
} }

View File

@ -1,6 +1,12 @@
package org.pixeldroid.app.utils.db.dao package org.pixeldroid.app.utils.db.dao
import androidx.room.* import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
@Dao @Dao
@ -9,17 +15,21 @@ interface UserDao {
* Insert a user, if it already exists return -1 * Insert a user, if it already exists return -1
*/ */
@Insert(onConflict = OnConflictStrategy.IGNORE) @Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertUser(user: UserDatabaseEntity): Long suspend fun insertUser(user: UserDatabaseEntity): Long
@Transaction @Transaction
fun insertOrUpdate(user: UserDatabaseEntity) { suspend fun insertOrUpdate(user: UserDatabaseEntity) {
if (insertUser(user) == -1L) { if (insertUser(user) == -1L) {
updateUser(user) updateUser(user)
} }
} }
@Update @Update
fun updateUser(user: UserDatabaseEntity) suspend fun updateUser(user: UserDatabaseEntity)
@Query("UPDATE users SET username = :username, display_name = :displayName, avatar_static = :avatarStatic WHERE user_id = :id and instance_uri = :instanceUri")
suspend fun updateUserAccountDetails(username: String, displayName: String, avatarStatic: String, id: String, instanceUri: String)
@Query("UPDATE users SET accessToken = :accessToken, refreshToken = :refreshToken WHERE user_id = :id and instance_uri = :instanceUri") @Query("UPDATE users SET accessToken = :accessToken, refreshToken = :refreshToken WHERE user_id = :id and instance_uri = :instanceUri")
fun updateAccessToken(accessToken: String, refreshToken: String, id: String, instanceUri: String) fun updateAccessToken(accessToken: String, refreshToken: String, id: String, instanceUri: String)
@ -27,6 +37,9 @@ interface UserDao {
@Query("SELECT * FROM users") @Query("SELECT * FROM users")
fun getAll(): List<UserDatabaseEntity> fun getAll(): List<UserDatabaseEntity>
@Query("SELECT * FROM users")
fun getAllFlow(): Flow<List<UserDatabaseEntity>>
@Query("SELECT * FROM users WHERE isActive=1") @Query("SELECT * FROM users WHERE isActive=1")
fun getActiveUser(): UserDatabaseEntity? fun getActiveUser(): UserDatabaseEntity?

View File

@ -4,20 +4,22 @@ import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
@Entity(tableName = "instances") @Entity(tableName = "instances")
data class InstanceDatabaseEntity ( data class InstanceDatabaseEntity(
@PrimaryKey var uri: String, @PrimaryKey var uri: String,
var title: String, var title: String,
var maxStatusChars: Int = DEFAULT_MAX_TOOT_CHARS, var maxStatusChars: Int = DEFAULT_MAX_TOOT_CHARS,
// Per-file file-size limit in KB. Defaults to 15000 (15MB). Default limit for Mastodon is 8MB // Per-file file-size limit in KB. Defaults to 15000 (15MB). Default limit for Mastodon is 8MB
var maxPhotoSize: Int = DEFAULT_MAX_PHOTO_SIZE, var maxPhotoSize: Int = DEFAULT_MAX_PHOTO_SIZE,
// Mastodon has different file limits for videos, default of 40MB // Mastodon has different file limits for videos, default of 40MB
var maxVideoSize: Int = DEFAULT_MAX_VIDEO_SIZE, var maxVideoSize: Int = DEFAULT_MAX_VIDEO_SIZE,
// How many photos can go into an album. Default limit for Pixelfed and Mastodon is 4 // How many photos can go into an album. Default limit for Pixelfed and Mastodon is 4
var albumLimit: Int = DEFAULT_ALBUM_LIMIT, var albumLimit: Int = DEFAULT_ALBUM_LIMIT,
// Is video functionality enabled on this instance? // Is video functionality enabled on this instance?
var videoEnabled: Boolean = DEFAULT_VIDEO_ENABLED, var videoEnabled: Boolean = DEFAULT_VIDEO_ENABLED,
// Is this Pixelfed instance?
var pixelfed: Boolean = true,
) { ) {
companion object{ companion object {
// Default max number of chars for Mastodon: used when their is no other value supplied by // Default max number of chars for Mastodon: used when their is no other value supplied by
// either NodeInfo or the instance endpoint // either NodeInfo or the instance endpoint
const val DEFAULT_MAX_TOOT_CHARS = 500 const val DEFAULT_MAX_TOOT_CHARS = 500

View File

@ -6,13 +6,16 @@ import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import okhttp3.* import okhttp3.*
import org.pixeldroid.app.utils.api.PixelfedAPI.Companion.apiForUser import org.pixeldroid.app.utils.api.PixelfedAPI.Companion.apiForUser
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
class APIModule{ @InstallIn(SingletonComponent::class)
class APIModule {
@Provides @Provides
@Singleton @Singleton
@ -54,7 +57,7 @@ class TokenAuthenticator(val user: UserDatabaseEntity, val db: AppDatabase, val
client_secret = user.clientSecret client_secret = user.clientSecret
) )
} }
}catch (e: Exception){ } catch (e: Exception){
return null return null
} }

View File

@ -1,29 +0,0 @@
package org.pixeldroid.app.utils.di
import android.app.Application
import android.content.Context
import org.pixeldroid.app.utils.BaseActivity
import org.pixeldroid.app.utils.PixelDroidApplication
import org.pixeldroid.app.utils.db.AppDatabase
import org.pixeldroid.app.utils.BaseFragment
import dagger.Component
import org.pixeldroid.app.postCreation.PostCreationViewModel
import org.pixeldroid.app.profile.EditProfileViewModel
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker
import javax.inject.Singleton
@Singleton
@Component(modules = [ApplicationModule::class, DatabaseModule::class, APIModule::class])
interface ApplicationComponent {
fun inject(application: PixelDroidApplication?)
fun inject(activity: BaseActivity?)
fun inject(feedFragment: BaseFragment)
fun inject(notificationsWorker: NotificationsWorker)
fun inject(postCreationViewModel: PostCreationViewModel)
fun inject(editProfileViewModel: EditProfileViewModel)
val context: Context?
val application: Application?
val database: AppDatabase
}

View File

@ -1,27 +0,0 @@
package org.pixeldroid.app.utils.di
import android.app.Application
import android.content.Context
import dagger.Module
import dagger.Provides
import javax.inject.Singleton
@Module
class ApplicationModule(app: Application) {
private val mApplication: Application = app
@Singleton
@Provides
fun provideContext(): Context {
return mApplication
}
@Singleton
@Provides
fun provideApplication(): Application {
return mApplication
}
}

View File

@ -5,20 +5,27 @@ import androidx.room.Room
import org.pixeldroid.app.utils.db.AppDatabase import org.pixeldroid.app.utils.db.AppDatabase
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import org.pixeldroid.app.utils.db.MIGRATION_3_4 import org.pixeldroid.app.utils.db.MIGRATION_3_4
import org.pixeldroid.app.utils.db.MIGRATION_4_5 import org.pixeldroid.app.utils.db.MIGRATION_4_5
import org.pixeldroid.app.utils.db.MIGRATION_5_6
import javax.inject.Singleton import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module @Module
class DatabaseModule(private val context: Context) { class DatabaseModule {
@Provides @Provides
@Singleton @Singleton
fun providesDatabase(): AppDatabase { fun providesDatabase(
@ApplicationContext applicationContext: Context
): AppDatabase {
return Room.databaseBuilder( return Room.databaseBuilder(
context, applicationContext,
AppDatabase::class.java, "pixeldroid" AppDatabase::class.java, "pixeldroid"
).addMigrations(MIGRATION_3_4).addMigrations(MIGRATION_4_5) ).addMigrations(MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6)
.allowMainThreadQueries().build() .allowMainThreadQueries().build()
} }
} }

View File

@ -32,9 +32,6 @@ import java.io.IOException
import java.time.Instant import java.time.Instant
import javax.inject.Inject import javax.inject.Inject
class NotificationsWorker( class NotificationsWorker(
context: Context, context: Context,
params: WorkerParameters params: WorkerParameters
@ -46,9 +43,6 @@ class NotificationsWorker(
lateinit var apiHolder: PixelfedAPIHolder lateinit var apiHolder: PixelfedAPIHolder
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
(applicationContext as PixelDroidApplication).getAppComponent().inject(this)
val users: List<UserDatabaseEntity> = db.userDao().getAll() val users: List<UserDatabaseEntity> = db.userDao().getAll()
for (user in users){ for (user in users){
@ -306,8 +300,7 @@ fun removeNotificationChannelsFromAccount(context: Context, user: UserDatabaseEn
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
notificationManager.deleteNotificationChannelGroup(channelGroupId.hashCode().toString()) notificationManager.deleteNotificationChannelGroup(channelGroupId.hashCode().toString())
} else { } else {
val types: MutableList<Notification.NotificationType?> = val types: MutableList<Notification.NotificationType?> = entries.toMutableList()
Notification.NotificationType.values().toMutableList()
types += null types += null
types.forEach { types.forEach {

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2020 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
~
~ https://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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true" android:color="?attr/colorSecondary"/>
</selector>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2020 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
~
~ https://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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true" android:color="?attr/colorOnSecondary"/>
<item android:state_checked="false" android:color="?attr/colorOnSecondaryContainer"/>
</selector>

View File

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="#FFFFFF" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z"/>
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z"
android:fillColor="?attr/colorOnBackground"/>
</vector>

View File

@ -1,8 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt" <vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
android:viewportWidth="201.8771" android:viewportWidth="403.75"
android:viewportHeight="218.8104" android:viewportHeight="437.6"
android:width="254dp" android:width="100dp"
android:height="275dp"> android:height="108dp">
<group android:translateX="100"
android:translateY="115">
<group <group
android:translateX="-1.41459" android:translateX="-1.41459"
android:translateY="-24.00768"> android:translateY="-24.00768">
@ -808,4 +811,5 @@
android:strokeColor="#000000" android:strokeColor="#000000"
android:strokeWidth="1.32292" android:strokeWidth="1.32292"
android:strokeLineCap="round" /> android:strokeLineCap="round" />
</group>
</vector> </vector>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M8,5v14l11,-7z"/>
</vector>

View File

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

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M4,6L2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6zM20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM12,14.5v-9l6,4.5 -6,4.5z"/>
</vector>

View File

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12.87,15.07l-2.54,-2.51 0.03,-0.03c1.74,-1.94 2.98,-4.17 3.71,-6.53L17,6L17,4h-7L10,2L8,2v2L1,4v1.99h11.17C11.5,7.92 10.44,9.75 9,11.35 8.07,10.32 7.3,9.19 6.69,8h-2c0.73,1.63 1.73,3.17 2.98,4.56l-5.09,5.02L4,19l5,-5 3.11,3.11 0.76,-2.04zM18.5,10h-2L12,22h2l1.12,-3h4.75L21,22h2l-4.5,-12zM15.88,17l1.62,-4.33L19.12,17h-3.24z"
android:fillColor="?attr/colorOnBackground"/>
</vector>

View File

@ -1,136 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fadeScrollbars="false">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:context=".settings.AboutActivity">
<ImageView
android:importantForAccessibility="no"
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/mascot" />
<TextView
android:id="@+id/aboutAppName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="11dp"
android:text="@string/app_name"
android:textAppearance="@style/TextAppearance.AppCompat.Large"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView" />
<TextView
android:id="@+id/aboutVersionNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/aboutAppName"
tools:text="v1.0.realversion" />
<TextView
android:id="@+id/aboutAppDescription"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:text="@string/license_info"
android:textAlignment="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/aboutVersionNumber" />
<TextView
android:id="@+id/aboutWebsite"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:autoLink="web"
android:textAlignment="center"
android:text="@string/project_website"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/aboutAppDescription" />
<TextView
android:id="@+id/contributeTranslationsText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:autoLink="web"
android:drawablePadding="6dp"
android:textAlignment="center"
android:text="@string/help_translate"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/aboutWebsite"
app:drawableLeftCompat="@drawable/translate_black_24dp" />
<TextView
android:id="@+id/contributeTranslationsUrl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="web"
android:textAlignment="center"
android:text="https://weblate.pixeldroid.org"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/contributeTranslationsText"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/contributeForgeText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:autoLink="web"
android:drawablePadding="6dp"
android:textAlignment="center"
android:text="@string/issues_contribute"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/contributeTranslationsUrl"
app:drawableLeftCompat="@drawable/bug_report_black_24dp" />
<TextView
android:id="@+id/contributeForgeUrl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="web"
android:textAlignment="center"
android:text="https://gitlab.shinice.net/pixeldroid/PixelDroid"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/contributeForgeText"
tools:ignore="HardcodedText" />
<Button
android:id="@+id/licensesButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/dependencies_licenses"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/contributeForgeUrl" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -1,13 +1,34 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:fitsSystemWindows="true"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorSecondaryContainer"
tools:context=".postCreation.camera.CameraActivity"> tools:context=".postCreation.camera.CameraActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/top_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView
android:id="@+id/camera_activity_fragment" android:id="@+id/camera_activity_fragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/> android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/top_bar" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,14 +1,30 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:fitsSystemWindows="true"
tools:context=".searchDiscover.TrendingActivity"> tools:context=".searchDiscover.TrendingActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSecondaryContainer"
android:fitsSystemWindows="true"
android:theme="@style/ThemeOverlay.AppCompat.ActionBar">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/top_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize" />
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout <FrameLayout
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:id = "@+id/collectionFragment" android:id = "@+id/collectionFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/> android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,184 +1,211 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:fitsSystemWindows="true">
<ImageView <com.google.android.material.appbar.AppBarLayout
android:id="@+id/profilePic" android:layout_width="match_parent"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
app:layout_constraintBottom_toTopOf="@+id/textInputLayoutName"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars"
android:contentDescription="@string/profile_picture" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayoutName"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:background="?attr/colorSecondaryContainer"
android:layout_marginTop="8dp" android:fitsSystemWindows="true"
android:layout_marginEnd="8dp" android:theme="@style/ThemeOverlay.AppCompat.ActionBar">
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/profilePic">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/nameEditText" android:id="@+id/top_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/your_name" android:minHeight="?attr/actionBarSize"
android:ems="10" app:title="@string/edit_profile" />
android:imeOptions="actionDone" /> </com.google.android.material.appbar.AppBarLayout>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/textInputLayoutBio" android:layout_width="match_parent"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" android:layout_height="match_parent"
android:layout_width="0dp" app:layout_behavior="@string/appbar_scrolling_view_behavior">
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textInputLayoutName">
<com.google.android.material.textfield.TextInputEditText <ImageView
android:id="@+id/bioEditText" android:id="@+id/profilePic"
android:layout_width="match_parent" android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:contentDescription="@string/profile_picture"
app:layout_constraintBottom_toTopOf="@+id/textInputLayoutName"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayoutName"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/your_bio" /> android:layout_marginStart="8dp"
</com.google.android.material.textfield.TextInputLayout> android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/profilePic">
<com.google.android.material.materialswitch.MaterialSwitch <com.google.android.material.textfield.TextInputEditText
android:id="@+id/privateSwitch" android:id="@+id/nameEditText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toStartOf="@+id/privateText"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textInputLayoutBio" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/privateText"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/privateSwitch"
app:layout_constraintTop_toTopOf="@+id/privateSwitch"
app:layout_constraintBottom_toBottomOf="@+id/privateSwitch">
<TextView
android:id="@+id/privateTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/private_account"
android:textStyle="bold"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/private_account_explanation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/privateTitle"
app:layout_constraintTop_toBottomOf="@+id/privateTitle" />
</androidx.constraintlayout.widget.ConstraintLayout>
<Button
android:id="@+id/editButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/more_profile_settings"
android:layout_marginTop="24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:icon="@drawable/ic_baseline_open_in_browser_24"
app:layout_constraintTop_toBottomOf="@+id/privateText" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/progressCard"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
style="?attr/materialCardViewElevatedStyle"
app:cardBackgroundColor="?attr/colorSecondaryContainer"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" >
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_margin="8dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/progressIcon"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent" android:ems="10"
app:layout_constraintStart_toStartOf="parent" android:hint="@string/your_name"
app:layout_constraintTop_toTopOf="parent"> android:imeOptions="actionDone" />
<ProgressBar </com.google.android.material.textfield.TextInputLayout>
android:id="@+id/savingProgressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<ImageView <com.google.android.material.textfield.TextInputLayout
android:id="@+id/error" android:id="@+id/textInputLayoutBio"
app:tint="?attr/colorOnSecondaryContainer" style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:src="@drawable/error" android:layout_width="0dp"
android:visibility="gone" android:layout_height="wrap_content"
android:layout_width="wrap_content" android:layout_marginStart="8dp"
android:layout_height="wrap_content" android:layout_marginTop="24dp"
app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="8dp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent"
android:contentDescription="@string/profile_saved" /> app:layout_constraintTop_toBottomOf="@+id/textInputLayoutName">
<ImageView <com.google.android.material.textfield.TextInputEditText
android:id="@+id/done" android:id="@+id/bioEditText"
android:src="@drawable/check_circle_24" android:layout_width="match_parent"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent" android:hint="@string/your_bio" />
app:layout_constraintStart_toStartOf="parent" </com.google.android.material.textfield.TextInputLayout>
app:layout_constraintTop_toTopOf="parent"
android:contentDescription="@string/profile_saved" /> <com.google.android.material.materialswitch.MaterialSwitch
</androidx.constraintlayout.widget.ConstraintLayout> android:id="@+id/privateSwitch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toStartOf="@+id/privateText"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textInputLayoutBio" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/privateText"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="@+id/privateSwitch"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/privateSwitch"
app:layout_constraintTop_toTopOf="@+id/privateSwitch">
<TextView <TextView
android:id="@+id/progressText" android:id="@+id/privateTitle"
tools:text="@string/fetching_profile"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" android:text="@string/private_account"
app:layout_constraintEnd_toEndOf="parent" android:textStyle="bold"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/progressIcon"/> app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/private_account_explanation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/privateTitle"
app:layout_constraintTop_toBottomOf="@+id/privateTitle" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout> <Button
android:id="@+id/editButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@string/more_profile_settings"
app:icon="@drawable/ic_baseline_open_in_browser_24"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/privateText" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/progressCard"
style="?attr/materialCardViewElevatedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardBackgroundColor="?attr/colorSecondaryContainer"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="8dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/progressIcon"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ProgressBar
android:id="@+id/savingProgressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/profile_saved"
android:src="@drawable/error"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?attr/colorOnSecondaryContainer" />
<ImageView
android:id="@+id/done"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/profile_saved"
android:src="@drawable/check_circle_24"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/progressText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/progressIcon"
tools:text="@string/fetching_profile" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,5 +1,30 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/followsFragment" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"/> android:fitsSystemWindows="true"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:fitsSystemWindows="true"
android:layout_height="wrap_content"
android:background="?attr/colorSecondaryContainer"
android:theme="@style/ThemeOverlay.AppCompat.ActionBar">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/top_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/followsFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/top_bar" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -21,7 +21,9 @@
<ImageView <ImageView
android:id="@+id/mascotImage" android:id="@+id/mascotImage"
android:layout_width="match_parent" android:layout_width="508dp"
android:layout_marginTop="-130dp"
android:adjustViewBounds="true"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="20dp" android:layout_marginBottom="20dp"
android:contentDescription="@string/mascot_description" android:contentDescription="@string/mascot_description"
@ -30,6 +32,7 @@
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/login_activity_instance_input_layout" android:id="@+id/login_activity_instance_input_layout"
android:layout_width="250dp" android:layout_width="250dp"
android:layout_marginTop="-130dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center" android:gravity="center"
android:hint="@string/domain_of_your_instance" android:hint="@string/domain_of_your_instance"

View File

@ -27,7 +27,7 @@
android:id="@+id/main_drawer_button" android:id="@+id/main_drawer_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorSurface" android:background="?attr/colorSurfaceContainer"
android:contentDescription="@string/open_drawer_menu" android:contentDescription="@string/open_drawer_menu"
android:padding="12dp" android:padding="12dp"
android:src="@drawable/ic_baseline_menu_24" /> android:src="@drawable/ic_baseline_menu_24" />

View File

@ -5,75 +5,102 @@
android:id="@+id/scrollview" android:id="@+id/scrollview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorSecondaryContainer"
android:fitsSystemWindows="true"
tools:context=".posts.PostActivity"> tools:context=".posts.PostActivity">
<com.google.android.material.appbar.AppBarLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/app_bar_layout" android:id="@+id/swipeRefreshLayout"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="match_parent">
<com.google.android.material.appbar.CollapsingToolbarLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/collapsing_toolbar_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed"> android:background="?attr/colorSurface">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.core.widget.NestedScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="match_parent"
android:fillViewport="true"
<include app:layout_constraintBottom_toTopOf="parent"
android:id="@+id/postFragmentSingle" app:layout_constraintBottom_toBottomOf="parent">
layout="@layout/post_fragment" />
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/commentIn"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
app:layout_constraintTop_toBottomOf="@+id/postFragmentSingle"
tools:layout_editor_absoluteX="10dp">
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/textInputLayout2" android:id="@+id/top_bar"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" android:background="?attr/colorSecondaryContainer"
app:layout_constraintEnd_toStartOf="@+id/submitComment" android:minHeight="?attr/actionBarSize"
app:layout_constraintStart_toStartOf="parent">
<EditText
android:id="@+id/editComment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/comment"
android:importantForAutofill="no"
android:inputType="text|textCapSentences|textMultiLine" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/submitComment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:contentDescription="@string/submit_comment"
android:text="@string/comment"
app:layout_constraintBottom_toBottomOf="@+id/textInputLayout2"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/textInputLayout2" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/textInputLayout2" /> app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraintPost"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/top_bar">
<include
android:id="@+id/postFragmentSingle"
layout="@layout/post_fragment" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/commentIn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/constraintPost"
tools:layout_editor_absoluteX="10dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout2"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/submitComment"
app:layout_constraintStart_toStartOf="parent">
<EditText
android:id="@+id/editComment"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/comment_noun"
android:importantForAutofill="no"
android:inputType="text|textCapSentences|textMultiLine" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/submitComment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:contentDescription="@string/submit_comment"
android:text="@string/comment_verb"
app:layout_constraintBottom_toBottomOf="@+id/textInputLayout2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/textInputLayout2"
app:layout_constraintTop_toTopOf="@+id/textInputLayout2" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/commentFragment"
android:layout_width="match_parent"
android:layout_height="500dp"
android:fillViewport="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/commentIn" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</com.google.android.material.appbar.CollapsingToolbarLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:id="@+id/commentFragment"
/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,9 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorSecondaryContainer"
android:fitsSystemWindows="true"
tools:context=".postCreation.PostCreationActivity"> tools:context=".postCreation.PostCreationActivity">
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView
@ -11,6 +13,7 @@
android:name="androidx.navigation.fragment.NavHostFragment" android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/colorSurface"
app:defaultNavHost="true" app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintLeft_toLeftOf="parent"
@ -18,4 +21,4 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/post_creation_graph" /> app:navGraph="@navigation/post_creation_graph" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -4,142 +4,154 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".profile.ProfileActivity"> tools:context=".profile.ProfileActivity">
<com.google.android.material.appbar.AppBarLayout <androidx.constraintlayout.motion.widget.MotionLayout
android:id="@+id/app_bar_layout" android:id="@+id/profileMotion"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:background="?attr/colorSecondaryContainer"
app:layoutDescription="@xml/collapsing_motion_layout_scene">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/top_bar"
android:background="?attr/colorSecondaryContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:fitsSystemWindows="true"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.appbar.CollapsingToolbarLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/collapsing_toolbar_layout" android:id="@+id/profile"
android:elevation="-1dp"
android:background="?attr/colorSurface"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/top_bar">
<ImageView
android:id="@+id/profilePictureImageView"
android:layout_width="88dp"
android:layout_height="88dp"
android:clickable="false"
android:layout_marginStart="20dp"
android:layout_marginTop="6dp"
android:contentDescription="@string/profile_picture"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars" />
<TextView
android:id="@+id/nbPostsTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="10dp"
android:gravity="center"
android:clickable="false"
android:text="@string/default_nposts"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/descriptionTextView" />
<TextView
android:id="@+id/nbFollowersTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:gravity="center"
android:text="@string/default_nfollowers"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@+id/nbPostsTextView"
app:layout_constraintEnd_toStartOf="@+id/nbFollowingTextView"
app:layout_constraintStart_toEndOf="@+id/nbPostsTextView"
app:layout_constraintTop_toTopOf="@+id/nbPostsTextView" />
<TextView
android:id="@+id/nbFollowingTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:gravity="center"
android:text="@string/default_nfollowing"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@+id/nbFollowersTextView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/nbFollowersTextView" />
<TextView
android:id="@+id/accountNameTextView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:clickable="false"
app:layout_scrollFlags="scroll|exitUntilCollapsed"> android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="20dp"
android:text="@string/no_username"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/profilePictureImageView" />
<androidx.constraintlayout.widget.ConstraintLayout <TextView
android:layout_width="match_parent" android:id="@+id/descriptionTextView"
android:layout_height="match_parent" android:layout_width="match_parent"
android:layout_marginBottom="12dp" android:clickable="false"
android:visibility="visible" android:layout_height="wrap_content"
app:layout_collapseMode="parallax" android:layout_marginLeft="20dp"
app:layout_constraintTop_toBottomOf="@id/nbFollowersTextView" android:layout_marginTop="5dp"
tools:visibility="visible"> android:layout_marginRight="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/accountNameTextView" />
<ImageView <Button
android:id="@+id/profilePictureImageView" android:id="@+id/followButton"
android:layout_width="88dp" android:layout_width="wrap_content"
android:layout_height="88dp" android:layout_height="wrap_content"
android:layout_marginStart="20dp" android:layout_marginEnd="20dp"
android:layout_marginTop="6dp" android:text="@string/follow"
android:contentDescription="@string/profile_picture" android:visibility="invisible"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintBottom_toBottomOf="@+id/profilePictureImageView"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent"
tools:srcCompat="@tools:sample/avatars" /> app:layout_constraintTop_toTopOf="@+id/profilePictureImageView" />
<TextView <Button
android:id="@+id/nbPostsTextView" android:id="@+id/editButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="20dp" android:text="@string/edit_profile"
android:layout_marginTop="10dp" android:visibility="gone"
android:gravity="center" app:layout_constraintBottom_toBottomOf="@+id/profilePictureImageView"
android:text="@string/default_nposts" app:layout_constraintEnd_toEndOf="parent"
android:textStyle="bold" app:layout_constraintStart_toEndOf="@+id/profilePictureImageView"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/profilePictureImageView" />
app:layout_constraintTop_toBottomOf="@+id/descriptionTextView" /> </androidx.constraintlayout.widget.ConstraintLayout>
<TextView <com.google.android.material.tabs.TabLayout
android:id="@+id/nbFollowersTextView" android:id="@+id/profileTabs"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" app:layout_constraintEnd_toEndOf="parent"
android:background="?attr/selectableItemBackgroundBorderless" app:layout_constraintStart_toStartOf="parent"
android:gravity="center" app:layout_constraintTop_toBottomOf="@id/profile"
android:text="@string/default_nfollowers" android:layout_height="wrap_content"
android:textStyle="bold" app:layout_scrollFlags="scroll|enterAlways" />
app:layout_constraintBottom_toBottomOf="@+id/nbPostsTextView"
app:layout_constraintEnd_toStartOf="@+id/nbFollowingTextView"
app:layout_constraintStart_toEndOf="@+id/nbPostsTextView"
app:layout_constraintTop_toTopOf="@+id/nbPostsTextView" />
<TextView
android:id="@+id/nbFollowingTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:gravity="center"
android:text="@string/default_nfollowing"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@+id/nbFollowersTextView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/nbFollowersTextView" />
<TextView
android:id="@+id/accountNameTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="20dp"
android:text="@string/no_username"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/profilePictureImageView" />
<TextView
android:id="@+id/descriptionTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="5dp"
android:layout_marginRight="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/accountNameTextView" />
<Button
android:id="@+id/followButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:text="@string/follow"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@+id/profilePictureImageView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/profilePictureImageView" />
<Button
android:id="@+id/editButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/edit_profile"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/profilePictureImageView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/profilePictureImageView"
app:layout_constraintTop_toTopOf="@+id/profilePictureImageView" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/profileTabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/nbPostsTextView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2 <androidx.viewpager2.widget.ViewPager2
android:id="@+id/view_pager" android:id="@+id/view_pager"
android:background="?attr/colorSurface"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="0dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior" /> app:layout_constraintTop_toBottomOf="@id/profileTabs"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.motion.widget.MotionLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,11 +1,29 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/colorSecondaryContainer"
tools:context=".posts.ReportActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:background="?attr/colorSurface"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".posts.ReportActivity"> tools:context=".posts.ReportActivity">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/top_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
app:title="@string/report"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView <TextView
android:id="@+id/report_target_textview" android:id="@+id/report_target_textview"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -14,8 +32,8 @@
app:layout_constraintBottom_toTopOf="@+id/textInputLayout" app:layout_constraintBottom_toTopOf="@+id/textInputLayout"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toBottomOf="@+id/top_bar"
tools:text="Reporting @user's post:" /> tools:text="Report @user's post" />
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout" android:id="@+id/textInputLayout"
@ -95,4 +113,5 @@
app:layout_constraintTop_toTopOf="@+id/reportButton" /> app:layout_constraintTop_toTopOf="@+id/reportButton" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,13 +1,29 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSecondaryContainer"
android:fitsSystemWindows="true"
android:theme="@style/ThemeOverlay.AppCompat.ActionBar">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/top_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize"
app:title="@string/menu_settings" />
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:orientation="vertical"> android:orientation="vertical">
@ -26,5 +42,4 @@
</LinearLayout> </LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,168 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black">
<com.google.android.material.card.MaterialCardView
android:id="@+id/storyErrorCard"
style="?attr/materialCardViewElevatedStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:visibility="invisible"
app:cardBackgroundColor="?attr/colorSecondaryContainer"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/storyAuthorProfilePicture"
tools:visibility="visible">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginEnd="8dp"
android:minHeight="48dp">
<ImageView
android:id="@+id/storyErrorIcon"
android:layout_width="50dp"
android:layout_height="match_parent"
android:importantForAccessibility="no"
android:src="@drawable/error"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?attr/colorOnSecondaryContainer" />
<TextView
android:id="@+id/storyErrorText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="@id/storyErrorIcon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/storyErrorIcon"
app:layout_constraintTop_toTopOf="@id/storyErrorIcon"
tools:text="Something is wrong with stories" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<ImageView
android:id="@+id/storyImage"
android:layout_width="match_parent"
android:layout_height="0dp"
android:contentDescription="@string/story_image"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/story_progress_image"
app:layout_constraintVertical_bias="1.0"
tools:scaleType="centerCrop"
tools:srcCompat="@tools:sample/backgrounds/scenic[10]" />
<ImageButton
android:id="@+id/pause"
android:layout_marginEnd="12dp"
android:layout_width="24dp"
android:layout_height="24dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/story_pause"
android:src="@drawable/play_pause"
app:layout_constraintBottom_toBottomOf="@+id/storyAuthor"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/storyAge"
tools:visibility="visible" />
<ImageView
android:id="@+id/story_progress_image"
android:layout_width="match_parent"
android:layout_height="4dp"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
android:importantForAccessibility="no"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/storyAuthorProfilePicture"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_margin="12dp"
android:contentDescription="@string/profile_picture"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/story_progress_image"
tools:srcCompat="@tools:sample/avatars" />
<TextView
android:id="@+id/storyAuthor"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:textStyle="bold"
app:layout_constraintStart_toEndOf="@+id/storyAuthorProfilePicture"
app:layout_constraintTop_toTopOf="@+id/storyAuthorProfilePicture"
tools:text="username" />
<TextView
android:id="@+id/storyAge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
app:layout_constraintBottom_toBottomOf="@+id/storyAuthor"
app:layout_constraintStart_toEndOf="@+id/storyAuthor"
app:layout_constraintTop_toTopOf="@+id/storyAuthorProfilePicture"
tools:text="48m" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/storyReplyField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:endIconContentDescription="TODO"
app:endIconDrawable="@drawable/ic_send_blue"
app:endIconMode="custom"
app:layout_constraintBottom_toBottomOf="parent"
tools:hint="Reply to PixelDroid">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
<View
android:id="@+id/viewRight"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/storyReplyField"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/viewMiddle"
app:layout_constraintTop_toBottomOf="@+id/storyAuthorProfilePicture" />
<View
android:id="@+id/viewMiddle"
android:layout_width="80dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/storyReplyField"
app:layout_constraintEnd_toStartOf="@id/viewRight"
app:layout_constraintStart_toEndOf="@id/viewLeft"
app:layout_constraintTop_toBottomOf="@+id/storyAuthorProfilePicture" />
<View
android:id="@+id/viewLeft"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/storyReplyField"
app:layout_constraintEnd_toStartOf="@id/viewMiddle"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/storyAuthorProfilePicture" />
</androidx.constraintlayout.widget.ConstraintLayout>

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