Compare commits

..

624 Commits

Author SHA1 Message Date
rebel onion
59784de727 Merge pull request #269 from rebelonion/dev
Dev
2024-03-20 14:54:01 -05:00
aayush262
42f23e4345 dix: many small changes 2024-03-21 01:18:36 +05:30
rebelonion
adb304f138 fix: manga/anime page noti icon updating 2024-03-20 14:29:52 -05:00
rebelonion
3bbf9efe63 dix: comment scroll deadspace 2024-03-20 13:54:25 -05:00
rebelonion
b454a2e3d9 fix: comment notification at bottom 2024-03-20 13:08:46 -05:00
rebelonion
23e6323f92 fix: comment reply dead scrolling space 2024-03-20 04:29:14 -05:00
rebelonion
b0dbd7a348 fix: add a check for minimum poll time 2024-03-20 04:10:12 -05:00
rebel onion
0f9bf3c5b1 chore: Update stable.md 2024-03-20 01:33:54 -05:00
rebel onion
4035aee1f9 Merge pull request #264 from rebelonion/dev
Dev
2024-03-20 01:32:15 -05:00
rebelonion
f707f8cc33 fix: activity color tweaks 2024-03-20 01:23:48 -05:00
aayush262
fa7126d80d fix: better gradiant color 2024-03-20 11:13:09 +05:30
aayush262
7d5f69888a fix(profile): double usernames 2024-03-20 10:57:50 +05:30
rebelonion
51841cf05f feat: error message snack -> toast 2024-03-20 00:23:42 -05:00
rebelonion
6d2c01ff2b chore: version bump 2024-03-20 00:19:39 -05:00
rebelonion
0bd4755814 fix: remove snack spam 2024-03-20 00:17:27 -05:00
rebelonion
927ba5ac86 fix: AAChartCore library not found tempfix 2024-03-19 20:18:39 -05:00
rebelonion
808d4e6bf5 feat: move subscriptions to new notification method 2024-03-19 19:30:12 -05:00
rebelonion
a39db5ea93 fix: cleaner spoiler text in comments 2024-03-19 17:09:34 -05:00
rebelonion
ca2409ef91 fix: fav workaround for broken anilist api 2024-03-19 16:50:52 -05:00
rebelonion
7b1f1a1357 fix: more robust notification loading 2024-03-19 16:02:52 -05:00
rebelonion
9471683501 feat: AlarmManager option for notifications 2024-03-18 23:51:00 -05:00
rebelonion
deeefb8e35 fix: don't show 500 error code 2024-03-18 17:55:12 -05:00
rebelonion
c777888fdb Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-18 17:45:36 -05:00
rebelonion
ce50627989 fix: add missing ui SharedPreference 2024-03-18 17:45:17 -05:00
TwistedUmbrellaX
9f84845ada fix: login and navigation < API 23 (#258)
* fix: compensate for old nav (48dp)

* fix: allow login to complete < API 23
2024-03-18 17:42:33 -05:00
rebelonion
6a8e422a30 fix: webview loading crash 2024-03-18 17:27:32 -05:00
rebelonion
39d6f0fbd6 fix: don't open links in webview 2024-03-18 10:40:39 -05:00
rebelonion
c240664fda fix: links in apps always open externally 2024-03-18 10:38:19 -05:00
rebelonion
a0f6320eee fix: what file links dantotsu opens 2024-03-18 10:12:32 -05:00
rebelonion
3434aa9744 fix: sticky profile fragment 2024-03-18 09:53:54 -05:00
rebelonion
3e84cfe09a fix: profile fragment scrolling 2024-03-18 00:23:40 -05:00
aayush262
22dccaa24b Merge remote-tracking branch 'origin/dev' into dev 2024-03-18 10:39:42 +05:30
rebelonion
ffe921a223 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-17 23:14:07 -05:00
rebelonion
1fd91b9ec6 Merge branch 'pr/257' into dev 2024-03-17 23:12:40 -05:00
TwistedUmbrellaX
cf10229574 fix: address deprecated code (#256)
* fix: address deprecated code

Build.RADIO has been deprecated since API 15, which means it hasn't worked since before the lowest target API of the app, and versioncode is deprecated in API 28.

* fix: use the convenience method

This takes the unused convenience method and the individually declared uses and merges them.

* fix: simplify compat switch
2024-03-17 23:10:38 -05:00
rebelonion
5c2ae57d77 feat: open user links in dantotsu 2024-03-17 23:05:05 -05:00
rebelonion
353452dd21 feat: open settings files directly 2024-03-17 22:15:20 -05:00
rebelonion
92fa0c117d fix: stats hardware acceleration glitch 2024-03-17 21:59:21 -05:00
rebelonion
9f4cd0ba0d fix: z fighting in viewpager2 potential? 2024-03-17 21:21:17 -05:00
rebelonion
385198e69a fix: text | reply bar not hiding 2024-03-17 21:07:54 -05:00
TwistedUmbrellaX
89fe3b82a3 fix: excess scope and redundancy 2024-03-17 22:05:14 -04:00
rebelonion
af1bc944d8 feat: sort comments in api 2024-03-17 20:52:39 -05:00
rebelonion
a0b22e8d56 fix: setdub out of bounds 2024-03-17 20:15:21 -05:00
rebelonion
c47d1afa1a feat: comment notifications in notification section 2024-03-17 20:05:38 -05:00
TwistedUmbrellaX
12a5b602e9 feat: getColor compatibility changes 2024-03-17 20:03:21 -04:00
rebelonion
25b85569fe fix: fragment IllegalStateException 2024-03-17 18:25:38 -05:00
rebelonion
b373a52218 fix: search for Spanish "episode" 2024-03-17 18:14:00 -05:00
rebelonion
b0e46cd904 fix: all notifications going to the same activity 2024-03-17 17:56:53 -05:00
rebelonion
89a54b4509 fix: recycled stat item 2024-03-17 17:35:59 -05:00
TwistedUmbrellaX
56aefef693 feat: move theme to API 23 res 2024-03-17 18:06:28 -04:00
TwistedUmbrellaX
a8ad018c44 feat: support API 21 with compat 2024-03-17 14:36:07 -04:00
aayush262
726f461ff6 fix(profile): Buggy animation 2024-03-17 21:06:20 +05:30
rebelonion
9c0861a8e4 feat: character fav 2024-03-17 01:39:21 -05:00
rebelonion
ca0162fb9c Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-17 01:01:09 -05:00
rebelonion
cfb8c3c733 feat: toggle fav 2024-03-17 01:00:08 -05:00
aayush262
fea448f850 feat: fav character (WIP) 2024-03-17 11:29:58 +05:30
rebelonion
c033bb0445 fix: use hardware acceleration for bio 2024-03-17 00:35:51 -05:00
rebelonion
bb110be9ab fix: bio color cleanup 2024-03-17 00:10:39 -05:00
rebelonion
fd39c4f391 fix: some color logging 2024-03-16 23:22:37 -05:00
rebelonion
9a3f9c6de2 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-16 23:11:42 -05:00
rebelonion
cf9799da7c chore: kotlin version bump 2024-03-16 23:11:30 -05:00
TwistedUmbrellaX
c054e2f2ac feat: progress for starting manga (#245)
The caveat is that the user must have disabled updating each title individually, along with the other standard checks. This will only apply when a chapter has not been completed.
2024-03-16 23:00:58 -05:00
TwistedUmbrellaX
8177dfdcef feat: delete item from context menu (#251)
* feat: delete item from context menu

* fix: follow the naming convention
2024-03-16 22:56:38 -05:00
rebelonion
813b64980d Merge branch 'pr/247' into dev 2024-03-16 22:53:25 -05:00
rebelonion
fda809bc8a fix: more strategic refresh in comments 2024-03-16 22:44:45 -05:00
rebelonion
5d1b220105 version bump 2024-03-16 19:58:17 -05:00
rebelonion
de21365c90 feat: tell user if not logged in 2024-03-16 19:51:40 -05:00
rebelonion
a24d1515b3 fix: more descriptive string 2024-03-16 19:24:38 -05:00
rebelonion
a3d6f841c6 feat: use markwon builder 2024-03-16 19:10:37 -05:00
rebelonion
b770bca6ba fix: RPC image 2024-03-16 18:56:56 -05:00
rebelonion
eaefbc13f9 feat: logout check 2024-03-16 17:04:39 -05:00
rebelonion
7fcc23c5bf fix: refresh stats every page load 2024-03-16 16:56:24 -05:00
rebelonion
29364bf30a fix: chart load | background of bio? 2024-03-16 16:50:58 -05:00
rebelonion
a9b4916dd8 fix: combine profile query 2024-03-16 15:43:09 -05:00
TwistedUmbrellaX
d4ab0ad57d fix: hide the skip button if hidden (#252)
If using the option to hide the skip button after a delay, setting 0 results in a generic +85 button with no click action.
2024-03-16 11:54:29 -05:00
aayush262
e3f8096749 feat: better profile page 2024-03-16 22:04:57 +05:30
rebelonion
94aae33d10 feat: animations for comment/activity/notification 2024-03-15 21:21:14 -05:00
rebelonion
96e29a8c59 fix: dismiss after extractors loaded 2024-03-15 21:13:03 -05:00
rebelonion
34a9a55d4f fix: comment bar not visible (solution is so cursed) 2024-03-15 20:57:36 -05:00
rebelonion
cf93f6d657 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-15 18:05:31 -05:00
rebelonion
91bcacc978 feat: swipe refresh activity/notifications 2024-03-15 18:05:23 -05:00
TwistedUmbrellaX
e79a824a04 fix: remove landscape buffer (#250)
Portrait compensates for system navigation, which bleeds into landscape (where system nav is on the side) and places the bar 1/3 into the screen.
2024-03-15 17:53:52 -05:00
rebelonion
e00bbb2d8e fix: notification list blank on click 2024-03-15 17:38:02 -05:00
rebelonion
12c77604f1 fix: typo 2024-03-15 17:16:27 -05:00
TwistedUmbrellaX
9a1ec8567c fix: error when streams are empty (#249)
The current design simply presents an empty server list and leaves the user to click away. No action can be taken without leaving the dialog.
2024-03-15 17:15:43 -05:00
aayush262
5dbc01dba3 feat: long tap like button to see users 2024-03-15 18:32:51 +05:30
rebelonion
c5abfa15e0 feat: activity clicking 2024-03-15 05:55:46 -05:00
rebelonion
b69e466853 feat: notification to activity click 2024-03-14 16:25:59 -05:00
rebelonion
9e371778b7 feat: filter lists by genre 2024-03-14 15:51:40 -05:00
rebelonion
ff036165df fix: don't delete global 2024-03-14 15:09:07 -05:00
rebelonion
4ed74b664b feat: notification clicking 2024-03-14 14:40:48 -05:00
rebelonion
ddd59643c5 fix: stop re-sending anilist notifications 2024-03-14 14:17:15 -05:00
rebelonion
6122eb3669 feat: global notification 2024-03-14 14:16:25 -05:00
TwistedUmbrellaX
1b5149f143 fix: update gradle dependancies 2024-03-14 10:35:20 -04:00
rebelonion
b654824eb7 fix: home page not loading 2024-03-14 06:30:04 -05:00
rebelonion
4d2a08c258 feat: anilist notifications (real) 2024-03-14 06:00:48 -05:00
rebelonion
19697f4f39 feat: view profile on anilist 2024-03-14 02:59:59 -05:00
TwistedUmbrellaX
41eea667e5 fix: forgotten uncle onRestart (#244)
* fix: forgotten uncle onRestart

It functions a lot like onResume, but assumes that onCreate ran and the user navigated away from the activity completely.

* fix: don't change to the current tab
2024-03-14 02:46:37 -05:00
TwistedUmbrellaX
f0040b8392 feat: add an option to revert bar hide (#242)
* feat: add an option to revert bar hide

* fix: clarify the bars being hidden
2024-03-13 08:15:13 -05:00
ibo
291f61551a feat: hide scrollBar toggle (#238) 2024-03-13 07:57:40 -05:00
꧁𝓜𝓸𝓱𝓪𝓶𝓶𝓮𝓭 𝓞𝓽𝓪𝓴𝓾꧂
6e8bd08828 Update ExoplayerView.kt (#237)
* feat (player): added portrait mode

Co-authored-by: MohammedOtaku <121404638+MohammedOtaku@users.noreply.github.com>
2024-03-13 07:56:33 -05:00
tutel
e915dd619d Made the skip button dissappear after 5 seconds with a setting to turn it off (#224)
* Made the skip button dissappear after 5 seconds with a setting to turn it off

* Resolved Merge Conflicts and Removed Unnecessary Imports

* Resolved Merge Conflicts

* Resolved Merge Conflicts

* Resolved Merge Conflicts

* Resolved problems

* Fixed a little mistake

* Made Requested Changes

* Removed println I forgot
2024-03-13 07:56:00 -05:00
aayush262
8fb6357fb5 feat: Blur toggle 2024-03-12 20:43:20 +05:30
aayush262
07662a91f4 Merge remote-tracking branch 'origin/dev' into dev 2024-03-12 10:23:12 +05:30
aayush262
37c618cb28 feat: blur function 2024-03-12 10:22:25 +05:30
rebelonion
5536f3b994 fix: logging home page 2024-03-11 11:53:35 -05:00
rebelonion
bdbbe62570 fix: genre sorting 2024-03-11 04:36:51 -05:00
rebelonion
4838e69aea Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-11 04:28:15 -05:00
rebelonion
408737d510 feat: activity replies 2024-03-11 04:28:13 -05:00
TwistedUmbrellaX
a0f05928e0 fix: reapply theme to each init call (#235) 2024-03-11 03:51:55 -05:00
rebelonion
a35887d4ac fix: tiny ui changes 2024-03-11 03:38:32 -05:00
rebelonion
dbce7c5b29 feat: logging to file 2024-03-11 03:01:08 -05:00
rebelonion
1028ac66cb fix: some anilist markdown 2024-03-11 00:05:07 -05:00
rebelonion
eb5e2623a0 feat: combine profile queries 2024-03-10 05:00:23 -05:00
rebelonion
867a4f36b3 fix: popup spam 2024-03-10 03:59:24 -05:00
rebelonion
913d74b285 feat: message activities 2024-03-10 03:59:15 -05:00
rebelonion
eb2eae7d6c fix: broken function name 2024-03-10 03:42:56 -05:00
rebelonion
8e5e548e16 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-10 03:21:22 -05:00
TwistedUmbrellaX
af1a481bdb Cleaning up navigation (#234)
* fix: align bottom to top with RTL

* fix: clean up the overlapping decor

* feat: match theme color with navbar

* fix: measure view on return to view
2024-03-10 03:21:14 -05:00
rebelonion
92089067f1 fix: activity pagination 2024-03-10 03:20:05 -05:00
rebelonion
d04ced94ea fix: comment pagination 2024-03-10 00:02:40 -06:00
TwistedUmbrellaX
14115ada4c A few build and navigation bar improvements (#231)
* fix: match project root to repo name

* feat: hide navigation bar until swiped

* fix: limit announcements to official

* feat: keep navigation visible for back

* fix: remove a duplicate permission
2024-03-09 15:06:48 -06:00
ibo
7f36eba709 feat: longclicklistener for AL profile now accessible everywhere (#228)
* feat: added longclicklistener for AL profile in AnimePageAdapter and MangaPageAdapter

* feat: add delete to smaller media bottom sheet
2024-03-09 15:02:23 -06:00
TwistedUmbrellaX
7504bb9081 fix: optimize querying download uri (#232) 2024-03-09 15:00:00 -06:00
TwistedUmbrellaX
64df08f91c fix: swap chapter names and nav on RTL (#230)
* fix: swap chapter names and nav on RTL

* fix: swipe RTL no longer needs invert
2024-03-09 14:58:15 -06:00
rebelonion
98f4d4f30b feat: global/personal feed | like posts | pagination 2024-03-09 04:33:06 -06:00
rebelonion
a9b03c45c6 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-08 22:05:17 -06:00
rebelonion
3af7926d20 feat: text activity 2024-03-08 22:04:25 -06:00
aayush262
e0cd43c63c fix: some UI changes pt:2 2024-03-08 15:24:35 +05:30
ibo
2742f58af5 feat(): fixed the UI changes 🦍 + notificationIcon logic and long press userAvatar 🐒 (#226) 2024-03-08 14:04:32 +05:30
rebelonion
49175a962a feat: (wip) user activities 2024-03-08 00:30:11 -06:00
rebelonion
46d8248ffd fix: profile recyclerViews 2024-03-07 18:45:02 -06:00
rebelonion
4ba1408f0f fix: notification size 2024-03-07 18:13:07 -06:00
Sadwhy
95fa5dcd9b Feet(profiles): update textviews (#221)
* nothing

* feet: Attached strings

* feet(fix)

---------

Co-authored-by: rebel onion <87634197+rebelonion@users.noreply.github.com>
2024-03-07 17:36:18 -06:00
aayush262
a2ca16355a fix: some UI changes (for better or worse) 2024-03-08 00:45:13 +05:30
rebelonion
7ac679f927 feat: anilist notifications 2024-03-07 02:51:04 -06:00
aayush262
e2eae6250b feat: WIP activity and notification page 2024-03-07 01:02:27 +05:30
ibo
2855093f5f feat: Inbox WIP(#222) 2024-03-06 20:36:56 +05:30
rebelonion
e50a65571f fix: follow activity crash 2024-03-06 08:38:55 -06:00
Finnley Somdahl
acef7c3d5e fix: headerAdaptor crash 2024-03-06 08:11:13 -06:00
aayush262
18778f3c5a fix(profile): remove progress in fav media 2024-03-06 15:58:55 +05:30
aayush262
03dae8c1b0 Merge remote-tracking branch 'origin/dev' into dev 2024-03-06 14:15:04 +05:30
aayush262
c862c072b5 feat: author and staff stuff 2024-03-06 13:43:37 +05:30
rebelonion
251f1e89cf Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-05 23:53:40 -06:00
rebelonion
3632055081 fix: correctly set banner 2024-03-05 23:53:38 -06:00
aayush262
bd64454c15 fix(profile): padding and stuff 2024-03-06 10:33:43 +05:30
rebelonion
31afbd547e feat: following / followers page 2024-03-05 19:33:42 -06:00
aayush262
8da0092561 WTF: rebel pls fix it 2024-03-06 02:19:30 +05:30
aayush262
36c64951c7 fix: someshit 2024-03-06 00:19:36 +05:30
aayush262
120e63ea8a fix(comments): hide "search by image" when chips are active 2024-03-05 20:07:50 +05:30
aayush262
ad82faba3f fix(comments): fix comment bar color 2024-03-05 20:03:53 +05:30
aayush262
4c4bbe3214 feat(YT): moved back to watch section 2024-03-05 20:03:18 +05:30
aayush262
ecbc7efebc feat(search): moved search by image 2024-03-05 17:22:40 +05:30
aayush262
89b6f28b9f feat(profile): added fav characters and staff 2024-03-05 17:10:04 +05:30
aayush262
8a1097cd35 Merge remote-tracking branch 'origin/dev' into dev 2024-03-05 14:36:46 +05:30
rebelonion
47d74de7ce fix: normal link colors 2024-03-05 02:33:39 -06:00
rebelonion
f3c89b3ac5 chore: remove debug webview 2024-03-05 02:30:56 -06:00
rebelonion
a2ecc5e30e fix: most profiles 2024-03-05 02:29:00 -06:00
rebelonion
db50975174 fix: navbar color in media details 2024-03-05 01:11:49 -06:00
rebelonion
ab14c4815f fix: home list sorting 2024-03-05 00:53:56 -06:00
rebelonion
7ad586c994 feat: brighten pink theme 2024-03-05 00:29:48 -06:00
rebelonion
db979de829 feat: normalize genres 2024-03-05 00:25:40 -06:00
rebelonion
5218d5cd28 fix: for different count types 2024-03-05 00:00:10 -06:00
aayush262
9e4684e61c Merge remote-tracking branch 'origin/dev' into dev 2024-03-05 11:13:38 +05:30
rebelonion
9b408e7520 feat: follow button 2024-03-04 23:38:05 -06:00
aayush262
10bd7d0918 fix(font): replaced Century Gothic Bold with Levenim MT Bold 2024-03-05 11:02:32 +05:30
rebelonion
5279b0cd65 feat: compare user stats 2024-03-04 22:55:29 -06:00
rebelonion
d181dcf837 feat: open stat in new window 2024-03-04 18:26:12 -06:00
rebelonion
49dc9d55b5 fix: cast color 2024-03-04 17:49:28 -06:00
rebelonion
852e9d0d29 fix: vote indent | serialize error 2024-03-04 00:56:35 -06:00
rebelonion
7a1ed4f83e feat: more charts | code cleanup 2024-03-04 00:02:41 -06:00
aayush262
2673b7d9bc fix(profile):formatting 2024-03-04 01:22:02 +05:30
aayush262
1587aff433 fix(list): bottom padding 2024-03-04 01:12:57 +05:30
aayush262
26b3f50fe0 fix(comments): force scroll 2024-03-03 23:05:19 +05:30
aayush262
286297aa38 Merge remote-tracking branch 'origin/dev' into dev 2024-03-03 22:41:17 +05:30
aayush262
99a805826d fix(profile page): Fav manga not showing 2024-03-03 22:41:02 +05:30
ibo
be1711b51e feat: scanlation bulk ticker (#218)
* feat: scanlation mass tick (WIP)

* feat: scanlation mass tick

* fix: togglebutton on scanlation scrollview

* fix: fix ImageButton padding + overlay

* fix: minor padding adjustment
2024-03-03 11:09:07 -06:00
aayush262
51beea1dc8 fix(profile page): Full bio not showing 2024-03-03 20:49:01 +05:30
aayush262
297e9dd756 feat(profile page): Better charts view ig 2024-03-03 20:36:24 +05:30
aayush262
03b8e7dab6 feat(profile page): added fav anime and manga 2024-03-03 16:24:36 +05:30
rebelonion
dbe837be28 feat: switch some graph styles 2024-03-03 01:29:43 -06:00
rebelonion
945d5886ea feat: genre graph 2024-03-03 01:25:45 -06:00
rebelonion
93fa29829f feat: length / year graph 2024-03-03 00:01:56 -06:00
rebelonion
a9f8d223e9 feat: score graph 2024-03-02 22:32:40 -06:00
Finnley Somdahl
d2876d04f5 feat: (wip) graph theming 2024-03-02 20:12:50 -06:00
rebelonion
fcd5c621de Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-02 14:57:22 -06:00
rebelonion
5fb0204376 feat: multidimensional charts 2024-03-02 14:56:43 -06:00
aayush262
790ab1a343 feat(profile page): something 2024-03-02 23:30:32 +05:30
aayush262
86ed721796 feat(profile page): Stats, Banner animation 2024-03-02 23:01:58 +05:30
rebel onion
2837cad762 feat: break markdown 2024-03-02 05:27:44 -06:00
rebelonion
500de4e45e feat: statistics (wip) 2024-03-02 04:54:02 -06:00
rebelonion
533148069f Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-02 01:15:54 -06:00
rebelonion
42b0a3b62b feat: statistics page (wip) 2024-03-02 01:15:46 -06:00
aayush262
c720aed4fc feat(profile page): WIP 2024-03-02 12:41:45 +05:30
rebelonion
00dad2ad48 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-01 22:38:26 -06:00
rebelonion
ce62a9c645 chore: gradle update 2024-03-01 22:38:25 -06:00
Sadwhy
1e4e2fd701 Remove Arca (#212)
* Removed from gradle

* Removed arca from BasePreferences.kt
2024-03-01 19:46:56 -06:00
rebelonion
103be31a43 fix: separate nullable statistics class 2024-03-01 18:15:09 -06:00
rebelonion
63fa3c829e fix: missing bracket 2024-03-01 18:08:56 -06:00
rebelonion
ab5c623e53 feat: send media ids in profile query 2024-03-01 18:07:46 -06:00
rebelonion
5e307bb796 fix: discord status code cleanup 2024-03-01 17:47:41 -06:00
ibo
a5567ef909 feat: discord status switcher (#211) 2024-03-01 17:40:13 -06:00
rebelonion
da22347267 feat: user profile data 2024-03-01 17:35:52 -06:00
rebelonion
a401ab89f3 feat: warnings 2024-02-29 23:03:57 -06:00
rebelonion
6e6429db82 fix: keep user data up to date 2024-02-29 18:52:27 -06:00
rebelonion
05fc97a933 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-02-29 15:13:34 -06:00
rebelonion
b9eb9d82f1 fix: navbar squish after loading settings 2024-02-29 15:13:31 -06:00
aayush262
976acd4af2 feat(manga dates): Better time formatting 2024-03-01 00:23:25 +05:30
ibo
c5cbe408c1 Update UserInterfaceSettingsActivity.kt (#207)
feat(UI setting): restart option after changing default tabs
2024-03-01 00:12:40 +05:30
aayush262
1316d5a698 feat(manga): Date and Scanlator in description 2024-02-29 20:43:30 +05:30
rebelonion
89aaef8355 feat: smooth navbar indicator 2024-02-29 03:54:56 -06:00
rebelonion
94e3dff909 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-02-29 03:29:22 -06:00
rebelonion
6240c61a11 fix: comment item spacing 2024-02-29 03:29:14 -06:00
Sadwhy
f226614980 Using different colours values on ep watched count (#205) 2024-02-29 03:21:08 -06:00
rebelonion
0ab283b254 fix: navbar behind system navbar 2024-02-29 03:20:08 -06:00
rebelonion
449485f06a fix: search bars 2024-02-29 03:10:21 -06:00
rebelonion
60752e83ed fix: no external links 2024-02-28 12:41:41 -06:00
rebelonion
dfd8b597cd fix: limit comment size 2024-02-28 12:41:31 -06:00
rebelonion
e256fb1560 feat: better notification 2024-02-28 01:27:48 -06:00
aayush262
2f7c6e734e feat(comments): UI tweaks
fix(comments): top padding
fix: removed self report
feat: better colors in color picker
2024-02-27 23:24:59 +05:30
rebelonion
efe5f546a2 feat: reply notifications 2024-02-27 02:13:06 -06:00
rebelonion
a8bd9ef97b fix: reply bar not showing 2024-02-26 19:35:12 -06:00
rebelonion
e93ca5d86e fix: failed to parse comment when commenting 2024-02-26 03:10:52 -06:00
rebelonion
7f943d34ac feat: comment placement | tagging 2024-02-26 03:01:11 -06:00
rebelonion
8a922bd083 feat: token lifetime stored 2024-02-25 18:35:45 -06:00
rebelonion
d5c87c46aa fix: replying message hide correctly 2024-02-25 01:18:44 -06:00
rebelonion
f128dee3e4 fix: clear status bar for custom themes 2024-02-25 00:36:53 -06:00
rebelonion
9de129a35b fix: block some tags 2024-02-25 00:29:06 -06:00
rebelonion
6d6b0b975a feat: alert dialog for deleting 2024-02-24 23:54:54 -06:00
rebelonion
bff8983b23 fix: comment replies visibility 2024-02-24 23:50:20 -06:00
rebelonion
55e156579b feat: comment reporting 2024-02-24 22:43:55 -06:00
rebelonion
a251dd4ffb feat: limit comment depth to 4 2024-02-24 19:05:17 -06:00
rebelonion
526098f2bf feat: (wip) limit comment depth to 4 2024-02-23 19:24:17 -06:00
rebelonion
6ccdc10208 feat: add user level to comments 2024-02-23 18:55:53 -06:00
rebelonion
ce355c108e fix: right-side padding on nested comments 2024-02-23 18:43:01 -06:00
rebelonion
612936476d Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-02-23 18:13:29 -06:00
rebelonion
7ba117ec25 feat: set comment name color above WCAG Guidelines 2024-02-23 18:13:00 -06:00
aayush262
8ea6bf85b8 feat(comments): "reply to" above text input 2024-02-23 20:47:59 +05:30
Sadwhy
70c87b4067 A few ui changes (#204) 2024-02-22 21:32:46 -06:00
rebelonion
4628282715 fix: crash on comments api connection failure 2024-02-22 21:13:12 -06:00
rebelonion
6c14a2eccf feat: colored names in comments 2024-02-22 20:49:36 -06:00
rebelonion
8944941d80 fix: comments api accepts total votes 2024-02-22 19:13:22 -06:00
rebelonion
78da98bd1d fix: keep text state when off screen (commentItem) 2024-02-22 17:36:59 -06:00
rebelonion
57833be7df fix: tag sort 2024-02-21 23:47:48 -06:00
rebelonion
506a0576df fix: subscription icon 2024-02-21 23:40:41 -06:00
rebelonion
458f4d1ff9 fix: most recent watch at beginning of list 2024-02-21 23:32:53 -06:00
rebelonion
21b9d51a35 fix: image saving on api > Q 2024-02-21 23:09:31 -06:00
rebelonion
82922b9792 fix re-add anilist link 2024-02-21 23:01:50 -06:00
rebelonion
160f783c6d Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-02-21 23:01:11 -06:00
rebelonion
ba7c665a9d fix: code cleanup | reply/edit stability 2024-02-21 22:59:45 -06:00
aayush262
b2f01a24b2 feat(comments): re-added Divider for reply 2024-02-20 14:11:38 +05:30
aayush262
84ae520f93 feat(comments): click on username to open in anilist 2024-02-20 13:59:50 +05:30
aayush262
4f61f4cf2c fix: Github Actions 2024-02-20 10:53:57 +05:30
Yutatsu
eba774618e added default load control (#202) 2024-02-19 22:09:21 -06:00
rebelonion
a74b9e985d fix: remove properties file dependency 2024-02-19 01:31:15 -06:00
rebelonion
0c2e2db1dc feat: basic replying 2024-02-19 01:28:11 -06:00
ibo
98b227876b New Theme :) preview in discord (#200)
* Update ThemeManager.kt

added oriax in picker + applier

* Update colors.xml

Added new color seed

* Update themes.xml

Added Oriax Lightmode

* Update themes.xml

Added Oriax Darkmode + OLED
2024-02-17 22:46:54 -06:00
aayush262
1fe50d2cca feat: reply in comments(WIP) 2024-02-17 12:29:39 +05:30
aayush262
420c0348f9 fix: mod cant ban themself now 2024-02-17 12:25:18 +05:30
aayush262
9be81aa4a9 fix: paddings of badges 2024-02-16 19:49:19 +05:30
aayush262
64c8f4225c fix: paddings in comments 2024-02-16 16:37:31 +05:30
rebelonion
a7c9604c43 fix: first comment message appear every time 2024-02-15 18:45:57 -06:00
rebelonion
68cc81e56c fix: correct config import 2024-02-15 18:39:46 -06:00
rebelonion
c9a64b1638 feat: server-side auth 2024-02-15 18:28:03 -06:00
rebelonion
ee7cff0fea feat: remember comment sort order 2024-02-15 14:17:45 -06:00
rebelonion
4c35f9a0cf fix: don't show 404 if no comments 2024-02-15 13:32:05 -06:00
rebelonion
9d9c4f026d fix: better error message 2024-02-15 13:24:34 -06:00
rebelonion
b4c7ea5f26 fix: round image on comment bar 2024-02-15 13:01:04 -06:00
rebelonion
093bee94c6 fix: update timestamp without reloading the page 2024-02-15 13:00:08 -06:00
rebelonion
fb99429dd7 fix: better attempt to get anilist username 2024-02-15 12:53:06 -06:00
rebelonion
a73c4cd678 feat: comments targeted at database 2024-02-15 12:44:52 -06:00
aayush262
1694a1cb24 feat: comments sorter popup 2024-02-15 15:25:18 +05:30
rebelonion
aaf9bdd00c feat: creating, deleting comments | markdown, spoiler comments 2024-02-14 06:41:24 -06:00
aayush262
129adc5825 chore: upgrade download-artifact-v3 -> v4 2024-02-13 22:39:04 +05:30
aayush262
07e7456ed8 Merge remote-tracking branch 'origin/dev' into dev 2024-02-13 13:36:31 +05:30
aayush262
7168e08587 feat: something idr 2024-02-13 13:35:46 +05:30
rebel onion
efb3b27317 Merge pull request #195 from Sadwhy/patch-6
A well recognized font
2024-02-12 17:34:33 -06:00
Sadwhy
2c3247c194 fixed unfortunate licence issues 2024-02-12 22:24:18 +06:00
aayush262
d37ebf8cdd fix: typo 2024-02-12 21:44:54 +05:30
aayush262
a73b049fd4 feat: moved "Play on youtube" to info page 2024-02-12 21:44:22 +05:30
aayush262
b3de2f805f wip: "send comments" interface 2024-02-12 21:43:29 +05:30
Sadwhy
0bec4f4d61 Mojangles font 2024-02-12 14:38:27 +06:00
aayush262
4be4a0968d Merge remote-tracking branch 'origin/dev' into dev 2024-02-12 01:45:16 +05:30
aayush262
97b957a0ab wip: UI for comments 2024-02-12 01:44:36 +05:30
Sadwhy
a22083dfcd fix: Updated discord links 2024-02-11 23:00:16 +05:30
rebel onion
9dbc3db1b8 Merge pull request #188 from Sadwhy/patch-6
Offline padding and faq update
2024-02-11 05:16:27 -06:00
Sadwhy
7af71ba217 Faq Rewrite P.1 2024-02-11 16:59:48 +06:00
rebelonion
80f3523f2e Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-02-11 04:16:23 -06:00
rebelonion
0afad1d9ae feat: comment authorization 2024-02-11 04:16:22 -06:00
aayush262
915c6c1dfe feat: better format for change logs 2024-02-10 23:11:20 +05:30
aayush262
e319aeb342 feat: monet icon for alpha 2024-02-10 23:08:13 +05:30
Sadwhy
ac20426689 Added proper padding to offline text/buttons 2024-02-09 09:02:53 +06:00
rebel onion
d778cd4350 Merge pull request #187 from rebelonion/dev
Dev
2024-02-08 09:56:12 -06:00
rebelonion
83c07467a9 fix: disable some buttons on fdroid build 2024-02-08 08:54:10 -06:00
rebelonion
0225b28fea fix: hitting enter on password input continues 2024-02-08 08:38:41 -06:00
rebelonion
1e2a740dae Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-02-08 08:15:53 -06:00
rebelonion
22e687b9d8 feat: add more helpers/developers 2024-02-08 08:15:51 -06:00
rebelonion
f088b90964 fix: ignore fdroid builds in beta 2024-02-08 08:15:29 -06:00
rebel onion
ab199a3502 Merge pull request #186 from rebelonion/dev
Dev
2024-02-08 07:35:44 -06:00
rebel onion
2493935349 Merge branch 'main' into dev 2024-02-08 07:34:23 -06:00
rebelonion
c9699ba1fc version bump 2024-02-08 07:26:14 -06:00
rebelonion
051012be2d feat: weeb filter in search results 2024-02-08 06:57:42 -06:00
rebelonion
a38f1a2b2b fix: hide system bars in reader by default 2024-02-08 06:47:15 -06:00
rebelonion
0b66275995 fix: default A10 to legacy storage 2024-02-08 06:32:22 -06:00
rebelonion
7879fe4355 fix: initial protected save not saving with correct filetype 2024-02-08 06:31:09 -06:00
rebelonion
4aa88244ed fix: default to old cast method for now 2024-02-08 06:23:51 -06:00
rebelonion
bfe4d69b8a fix: change default user agent 2024-02-08 05:52:55 -06:00
rebelonion
69fead70d1 fix: crash on softsub download 2024-02-08 05:24:51 -06:00
rebelonion
92c663cd38 chore: cleanup 2024-02-08 05:11:28 -06:00
rebelonion
b829ed26f3 fix: combine anilist init queries into one query 2024-02-08 05:06:09 -06:00
rebelonion
3d4834507d fix: image cropping on some dpi levels 2024-02-08 01:20:57 -06:00
rebelonion
0758241e06 fix: download button orientation 2024-02-08 00:56:14 -06:00
rebelonion
b093b5f979 fix networking on some extensions 2024-02-08 00:38:09 -06:00
rebelonion
d86481a0f7 fix: anime extension image url 2024-02-07 23:55:27 -06:00
rebel onion
de1788950f Merge pull request #185 from rebelonion/dev
Dev
2024-02-07 08:12:43 -06:00
rebelonion
1e7b546b75 fix: extension dragging 2024-02-07 01:29:08 -06:00
rebelonion
d53781e75a fix: text alignment issues 2024-02-07 00:05:14 -06:00
rebelonion
aa1830f12c fix: search history not clickable 2024-02-06 23:58:09 -06:00
rebelonion
17a87aa8c8 bump versionCode 2024-02-06 23:30:13 -06:00
rebelonion
2662017fb7 fix: favorites list not showing progress 2024-02-06 22:41:10 -06:00
rebelonion
f2b9cc3b3e fix: swap button colors in login fragment 2024-02-06 21:56:52 -06:00
rebelonion
2912ee5f73 fix: popups backgrounds to black when in OLED mode 2024-02-06 21:55:14 -06:00
rebelonion
9bb4c702f3 feat: ask permission for files on API <29 2024-02-06 21:40:38 -06:00
rebelonion
c4630f9243 fix: bottom sheet alignment 2024-02-06 21:23:29 -06:00
rebelonion
57581d6f54 feat: transition for mediainfofragment items 2024-02-06 20:51:41 -06:00
rebelonion
b5cfb5d567 fix: extension page sorting order bug 2024-02-06 19:46:12 -06:00
rebelonion
04306a981f fix: inconsistent search history order 2024-02-06 19:07:44 -06:00
rebelonion
95409f7eda fix: remove some overhead when storing sets 2024-02-06 18:58:21 -06:00
rebelonion
8741d820ad fix: start anime download icon rotate when pressed 2024-02-06 18:16:11 -06:00
rebelonion
6020636a66 fix: update .gitignore 2024-02-06 17:26:42 -06:00
rebelonion
d15ffe7708 fix: firebase initialization 2024-02-06 03:11:31 -06:00
rebelonion
f77269e468 fix: update workflow for new flavor 2024-02-06 02:33:01 -06:00
rebelonion
a2e44da99d chore: code refactor 2024-02-06 02:16:10 -06:00
rebelonion
8d7b86a667 fix: crash on incorrect password 2024-02-06 01:53:49 -06:00
rebelonion
0c6fad91ca fix: #184 constraint layout 2024-02-06 01:39:57 -06:00
rebelonion
c0f3fed142 feat: F-Droid flavor 2024-02-06 01:10:12 -06:00
Sadwhy
21f5d503cd Changed launch animation <:kys:960143876184739880> (#181)
* Branch

* Changed Launch Animation

* Update strings.xml

---------

Co-authored-by: rebel onion <87634197+rebelonion@users.noreply.github.com>
2024-02-06 00:58:43 -06:00
Sheby
18b4f858d9 style: Centre align chips 2024-02-06 04:40:41 +05:30
aayush262
a101cac503 fix: send apk to telegram 2024-02-05 23:05:39 +05:30
aayush262
3f4c1953f8 feat: send apk to telegram(WIP) 2024-02-05 20:27:57 +05:30
aayush262
a599b5b632 test 2024-02-05 15:16:56 +05:30
aayush262
9fad3597bb test 2024-02-05 15:09:54 +05:30
rebelonion
f4b9889d67 last shot 2024-02-05 02:33:51 -06:00
rebelonion
d27b4f6905 feat: commits start on a new line 2024-02-05 02:26:35 -06:00
rebelonion
855c7e623a fix: gradlew line endings
fix: beta.yml

fix: multiline beta.yml

Update beta.yml

Update beta.yml

Update beta.yml

Update beta.yml

fix: beta.yml newline

Update beta.yml

fix: finding last sha

fix: beta .yml
2024-02-05 02:10:30 -06:00
rebelonion
a7e7bd0230 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-02-04 22:47:14 -06:00
rebelonion
1d46897086 fix: alpha icon reversed 2024-02-04 22:47:09 -06:00
aayush262
7af0721a75 fix: big fonts 2024-02-04 20:22:13 +05:30
Sadwhy
884d738de9 :) (#179)
* This is war

* <@520373269979988000>
2024-02-04 18:29:16 +05:30
rebelonion
ef71ca8a76 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-02-04 04:27:37 -06:00
rebelonion
d4df12505b fix: handle multiline message 2024-02-04 04:27:12 -06:00
aayush262
670d9b3913 fix: mono icon in alpha version 2024-02-04 15:51:49 +05:30
rebelonion
19df5355ba add fetch depth 2024-02-04 04:17:24 -06:00
rebelonion
8ca1c3be1f show more commit messages in discord message 2024-02-04 04:14:19 -06:00
rebelonion
ed19aac553 feat: add rotation viewable setting 2024-02-04 04:07:56 -06:00
rebelonion
7a67fbb980 feat: import settings on login page 2024-02-04 03:59:52 -06:00
rebelonion
d80b250650 fix offline crash 2024-02-04 03:13:15 -06:00
rebelonion
300f2c2fb0 fix: added preference theme 2024-02-04 01:56:11 -06:00
rebelonion
c2f108bf44 fix: sorting extensions order 2024-02-04 01:19:34 -06:00
rebelonion
15abcd77d0 feat: better anilist api failure info 2024-02-04 00:29:41 -06:00
rebelonion
462f82e3fb fix: change download icon 2024-02-04 00:29:16 -06:00
rebelonion
eade3ce341 fix!: MAL token serialization 2024-02-03 23:36:49 -06:00
rebelonion
b4487159ed fix: webhook 2024-02-03 21:14:40 -06:00
rebelonion
d12022266e feat: alpha version 2024-02-03 21:05:01 -06:00
rebel onion
13074a0f72 fix: semantic versioning in workflow 2024-02-03 19:38:43 -06:00
aayush262
dc2c0c1027 something 2024-02-03 17:38:38 +05:30
rebelonion
97ed84127e extension filtering 2024-02-03 04:27:16 -06:00
rebelonion
d3f097f675 sortable sources 2024-02-03 04:06:41 -06:00
rebelonion
402e0576c8 fix for progress dialog 2024-02-03 01:52:39 -06:00
rebelonion
aa8d41eecf better setting export 2024-02-03 00:43:20 -06:00
rebelonion
54b53dbe56 remove less-used preferences 2024-02-02 16:48:46 -06:00
rebelonion
2214c47c0c exporting credentials now requires a password (encrypted) 2024-02-02 16:45:07 -06:00
Finnley Somdahl
ed6275b0e8 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-02-02 14:49:22 -06:00
Finnley Somdahl
97d062ffc2 remove unnecessary context (build failing) 2024-02-02 14:49:19 -06:00
Sadwhy
c57c33c088 Small delay to offline toggle (#171) 2024-02-02 22:25:01 +05:30
Finnley Somdahl
025d31102e move anilist to protected 2024-02-02 09:50:06 -06:00
Finnley Somdahl
cd96b6ab06 attempt to not overwrite settings file 2024-02-02 09:33:55 -06:00
Finnley Somdahl
9358f86d43 filter exportable extensions 2024-02-02 09:18:47 -06:00
Finnley Somdahl
7e51e067cd fix create file view to open file view 2024-02-02 09:12:02 -06:00
Finnley Somdahl
10ae66d1d0 fix deserialzeClass storing in the wrong preference 2024-02-02 08:36:20 -06:00
aayush262
cbdd1a2538 Fix dark theme 2024-02-02 19:05:32 +05:30
rebelonion
829292399b set default export before click 2024-02-02 02:47:08 -06:00
rebelonion
f7c7b8050f fixes 2024-02-02 02:43:29 -06:00
rebelonion
a3be59bd02 version bump 2024-02-02 02:05:05 -06:00
rebelonion
49e90a27b8 import/export settings 2024-02-02 02:04:46 -06:00
rebelonion
b559a13bab Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-02-01 14:54:27 -06:00
rebelonion
4d682f014d remove old setting saving 2024-02-01 14:54:25 -06:00
aayush262
1a346e6b5a no changes 2024-02-01 23:37:34 +05:30
aayush262
37a53748a2 crash fix 2024-01-31 13:25:00 +05:30
aayush262
9c2c932e75 Rearranged settings page 2024-01-31 12:26:11 +05:30
aayush262
c853d5bdf8 no rpc in offline mode 2024-01-31 10:41:21 +05:30
Finnley Somdahl
1f98e349dc automatically update downloads to new system 2024-01-30 20:37:13 -06:00
Finnley Somdahl
883c14bf0d more cleanup 2024-01-30 20:23:39 -06:00
Finnley Somdahl
178abf0f83 fix nullable string 2024-01-30 20:13:51 -06:00
aayush262
c242fedfee imported more settings 2024-01-30 17:34:01 +05:30
aayush262
922ccdfb27 fixed mis-match default values 2024-01-30 14:30:42 +05:30
rebelonion
965adddf8d Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-01-30 00:39:19 -06:00
rebelonion
8020b32541 backend preference wrapper 2024-01-30 00:39:12 -06:00
aayush262
33d798727c fix switch color 2024-01-29 13:37:13 +05:30
rebel onion
a0949c4e36 no 2024-01-28 22:34:09 -06:00
Sadwhy
f00a0367cf Fixed md3 switches in sub/dub (#165)
* Fixed md3 switches in sub/dub

* :)
2024-01-28 20:42:30 +05:30
rebelonion
eb5b83564f some small bugfixes 2024-01-28 04:29:18 -06:00
rebelonion
6b9dce10cf code 2024-01-28 04:24:20 -06:00
rebelonion
5d789bf96c SEARCH HISTORY OR SMTH IDK ANY MORE 2024-01-28 04:13:16 -06:00
rebelonion
17431734fb update string for better desc 2024-01-27 23:12:40 -06:00
rebelonion
63c80fa526 version bump 2024-01-27 13:52:39 -06:00
rebelonion
2b79d437fc Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-01-27 13:33:52 -06:00
rebelonion
1e6041f99e remove default anime/manga sources 2024-01-27 13:33:50 -06:00
rebel onion
fad4032a78 Merge pull request #163 from Yutatsu1/dev
added episode duration formatting
2024-01-26 15:57:51 -06:00
Yutatsu
36ad006021 add episode duration formatting 2024-01-26 17:42:34 +00:00
aayush262
f6db690454 quality in exoplayer 2024-01-26 19:36:46 +05:30
aayush262
26575cfa0d old switch for sub/dub toggle 2024-01-26 18:56:37 +05:30
rebelonion
73be639397 cleanup 2024-01-25 12:00:00 +00:00
rebelonion
0ebd067bc2 cleanup 2024-01-26 00:29:19 -06:00
rebelonion
49b3c33fbc subdub toggle | regex fix (yomiroll) | idk I forgot 2024-01-26 00:17:33 -06:00
aayush262
4a5eab13c9 removed quality selector 2024-01-26 10:32:02 +05:30
aayush262
00ce6ce755 Merge remote-tracking branch 'origin/dev' into dev 2024-01-26 09:29:22 +05:30
aayush262
0aa95889aa scroll to top padding fixed 2024-01-26 09:29:12 +05:30
Adolar0042
3fdec074c6 feat: made year filter dynamic (#159)
* feat: made year filter dynamic
2024-01-25 21:54:48 +05:30
aayush262
29c6863b00 Merge remote-tracking branch 'origin/dev' into dev 2024-01-25 00:31:33 +05:30
aayush262
97eacb58a6 wont show progress window if incognito is on 2024-01-25 00:30:28 +05:30
aayush262
67bb28d027 scroll to top 2024-01-25 00:23:19 +05:30
aayush262
513ed31b08 filler fix 2024-01-25 00:20:19 +05:30
rebel onion
da5d95c7e5 Merge pull request #158 from Yutatsu0/dev
let jerry sleep in peace
2024-01-24 09:37:38 -06:00
Yutatsu
7b9450807b let jerry sleep in peace 2024-01-24 20:24:42 +06:00
Finnley Somdahl
8bc3631964 subdub regex function 2024-01-23 16:20:50 -06:00
Finnley Somdahl
ab038983e5 sub/dub regex 2024-01-23 15:26:37 -06:00
Finnley Somdahl
79cff1ec9d fix heart thing 2024-01-23 14:44:50 -06:00
Finnley Somdahl
4893cd0b03 check for initialization 2024-01-23 14:18:36 -06:00
Finnley Somdahl
b8fbeed785 check for activity context 2024-01-23 14:15:04 -06:00
Finnley Somdahl
8a9668bc79 switch icon in service 2024-01-23 14:06:12 -06:00
Finnley Somdahl
d02d542207 cast fix 2024-01-23 13:56:59 -06:00
Finnley Somdahl
4c797c5eb1 index fix 2024-01-23 13:54:15 -06:00
Finnley Somdahl
3fa2690277 20% bleh 2024-01-23 13:37:52 -06:00
Finnley Somdahl
67d482bad6 Revert "removed sub dub toggle(useless)"
This reverts commit 60981ba224.
2024-01-23 13:35:28 -06:00
aayush262
60981ba224 removed sub dub toggle(useless) 2024-01-23 23:42:11 +05:30
aayush262
cb8ebfccb6 Removed useless Quality selector in exoplayer 2024-01-23 22:32:08 +05:30
aayush262
e5f0b71cf0 fixed broken transition in offline anime page 2024-01-23 18:38:30 +05:30
aayush262
4218d81c49 Download manager fixed
now no need to long tap download button to download externally (select external downloader to download from external app)
2024-01-23 17:38:11 +05:30
aayush262
9fa422ebf3 fixed anime/chapter list theme for OLED 2024-01-23 16:18:20 +05:30
aayush262
3ec488675f 20% Chance of getting Update extension 2024-01-23 15:53:41 +05:30
rebelonion
78b7d07500 version bump 2024-01-23 01:58:43 -06:00
rebel onion
a316de3957 Update stable.md 2024-01-23 01:43:02 -06:00
rebel onion
c3f5a820e4 Merge pull request #155 from rebelonion/dev
Dev
2024-01-23 01:39:07 -06:00
rebelonion
daa5ec7bed extension updates toast 2024-01-23 01:36:38 -06:00
rebelonion
de91f1f3fa idk some fixes or smth 2024-01-23 01:23:47 -06:00
rebelonion
9c67a7e357 nice transition for offline mode 2024-01-22 22:51:51 -06:00
rebelonion
f70ce39fb7 some code cleanup 2024-01-22 22:39:01 -06:00
rebel onion
20ffe2273c Update README.md 2024-01-22 22:18:04 -06:00
aayush262
f333051073 Merge pull request #154 from aayush2622/dev
warning menu when deleting episode
2024-01-23 01:18:07 +05:30
Finnley Somdahl
a58f8fa76b random fix 2024-01-22 18:59:57 -06:00
aayush262
625c7d738b warning menu when deleting episode
auto hide desc if ep is downloaded(looked verybad when both was on)
2024-01-23 01:10:51 +05:30
aayush262
563e4f2cbe "Include list" switch changed (#153)
* "Include list" switch change

* remember the toggle state of "Include list"
2024-01-22 12:40:29 -06:00
rebel onion
627bed2407 ping update build 2024-01-22 11:04:13 -06:00
Finnley Somdahl
8313d639d7 fuckujerryidowhatiwant 2024-01-22 16:52:43 -06:00
Finnley Somdahl
c603de70e3 who doesn't love a good delay in their code? 2024-01-22 16:00:15 -06:00
rebel onion
4508cada0f Merge pull request #152 from rebelonion/dev
Dev
2024-01-22 08:41:40 -06:00
aayush262
ab8dc2ee8b added unnecessary check 🤣 2024-01-22 11:54:17 +05:30
rebelonion
acf2dd9a8a remove unnecessary check 2024-01-21 20:04:04 -06:00
Yutatsu
f562e7d7cf remove "Failed to load data from MAL" toast (#151) 2024-01-21 15:26:22 -06:00
aayush262
25372d5251 fixed scroll to top in offline mode (#149)
* fixed small bugs

* fixed scroll to top in offline mode
2024-01-21 15:23:23 -06:00
rebelonion
efb346d0a8 better error message for webview 2024-01-21 15:21:58 -06:00
rebelonion
6d05fb4413 pretest version bump 2024-01-21 02:04:25 -06:00
rebelonion
d67a51791e regex fix :') 2024-01-21 02:02:14 -06:00
rebelonion
332857b2c9 default view bug 2024-01-21 01:46:46 -06:00
rebelonion
a0018b5fb6 pinned sources 2024-01-21 01:40:44 -06:00
rebelonion
734c5d0571 help clear some pesky errors 2024-01-21 00:30:54 -06:00
rebelonion
8e93f66ba8 alert dialog cancel button 2024-01-21 00:26:22 -06:00
rebelonion
5d8cf8a605 cool transition 2024-01-21 00:11:09 -06:00
rebelonion
87c2d82462 download stopping 2024-01-20 23:41:22 -06:00
rebelonion
45a341397b regex fix 2024-01-20 22:51:24 -06:00
rebelonion
b018d0f090 kitsu description fix 2024-01-20 19:18:15 -06:00
Yutatsu
3c992f89f4 use episode titles from kitsu if kitsu doesn't have episode titles use anime source titles (#146) 2024-01-20 13:54:31 -06:00
aayush262
8067e0d0ac INCOGNITO (final fix) (#143) 2024-01-20 13:53:33 -06:00
aayush262
4cee512572 fixed upload image button padding (#142)
* no extra media in offline mode

* incognito display in media

* fixed upload image button padding
2024-01-19 02:08:30 -06:00
rebelonion
87a9df4c12 I FUCKING HATE EXOPLAYER SUBTITLES 2024-01-19 01:49:24 -06:00
rebelonion
ea96291bfc add optional username in crash report 2024-01-18 22:20:56 -06:00
rebelonion
b1eedce229 MANGA REGEX FIX 2024-01-18 21:32:49 -06:00
rebelonion
0d32342765 TOAST BUG 2024-01-18 21:22:13 -06:00
rebelonion
d81391f593 BUG: I FIX 2024-01-18 21:16:14 -06:00
rebelonion
3bd9dc031a download manager fix 2024-01-18 20:31:53 -06:00
rebelonion
4f07421df7 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-01-18 20:16:56 -06:00
aayush262
8eadd20968 lil tweaks (#140) 2024-01-18 03:34:29 -06:00
rebelonion
c6d04d99b3 version bump 2024-01-18 02:26:59 -06:00
rebelonion
91b1f4775b status bar height fix 2024-01-18 02:04:14 -06:00
rebelonion
5bd8f1a3c7 regex fix 2024-01-18 01:55:47 -06:00
rebelonion
39fc508cfe crash fixes 2024-01-18 01:18:38 -06:00
rebelonion
664b5a4bdd ??? 2024-01-18 01:09:30 -06:00
rebelonion
ff02280239 webview for extensions 2024-01-18 01:09:11 -06:00
rebelonion
26b6564825 downloads showing for all media fix 2024-01-17 23:40:14 -06:00
rebelonion
5459908201 download not showing up fix 2024-01-17 23:27:54 -06:00
rebelonion
3693179c78 aniwave fix 2024-01-17 22:48:48 -06:00
rebelonion
9416c88511 regex fix 2024-01-17 20:37:49 -06:00
rebelonion
f18399d529 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-01-17 19:48:31 -06:00
aayush262
6b2ffdaf4f ruined UI (#138)
* removed auto navbar hide

* ruined rebel's fabulous UI
2024-01-17 19:48:17 -06:00
rebelonion
d16dd7ed67 video name too long fix 2024-01-17 02:40:49 -06:00
rebelonion
8142c966c0 read me and weep 2024-01-17 02:34:37 -06:00
rebelonion
b7cc35207c odd color bug in ocean 2024-01-17 02:16:04 -06:00
rebelonion
e398238fe6 remove download on fail 2024-01-17 01:47:42 -06:00
rebelonion
51a5609395 incognito header fix 2024-01-17 01:45:41 -06:00
rebelonion
4e6842862e send offline mode to correct page 2024-01-17 00:56:48 -06:00
rebelonion
ddde08c61b version bump 2024-01-17 00:10:17 -06:00
rebelonion
05b3f57a76 incognito notification onClick 2024-01-17 00:08:32 -06:00
rebelonion
0464cc08c3 fix episode number in exoplayer 2024-01-16 23:16:15 -06:00
rebelonion
4be3ded9c8 default op skip proxy to off 2024-01-16 23:03:39 -06:00
rebelonion
6a42832855 view download status 2024-01-16 22:05:29 -06:00
rebelonion
84fc5e6e2c verbose downloading 2024-01-16 17:53:46 -06:00
rebelonion
8375cb5c03 cast long press fix 2024-01-16 15:27:17 -06:00
rebelonion
2fdee06248 download manager on long click 2024-01-16 15:17:18 -06:00
rebelonion
f861b3621f universal storage name 2024-01-16 15:00:21 -06:00
rebelonion
0cfcfcb9ac no found media error message 2024-01-16 14:59:38 -06:00
rebelonion
68ccff2259 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-01-16 14:54:17 -06:00
aayush262
aa972c916a navbar fix (#135)
* navbar fix

* fixed can uninstall after changing grid view

* removes server selector in offline mode shows amount of Scanlator present
2024-01-16 14:54:07 -06:00
rebelonion
b0673d4f78 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-01-16 14:52:01 -06:00
rebelonion
5170288050 update extension api 2024-01-16 14:51:33 -06:00
aayush262
61150066bd auto hide android navbar (#133)
* auto hide android navbar

* auto hide android navbar
2024-01-15 12:55:21 -06:00
aayush262
bd6197031a downloaded anime page bug fixes (#132)
* after changing grid style items are not accessible fixed

* added total ep released no

* padding fix

* fixed scroll to top coinciding with navbar

* small change
2024-01-15 10:01:41 -06:00
rebelonion
98cb11e841 offline anime 2024-01-14 23:59:39 -06:00
rebelonion
52dadf34cf Delete all 2024-01-14 19:00:02 -06:00
rebelonion
f038dcb255 clean incognito notification function 2024-01-14 18:58:57 -06:00
rebelonion
a851c0f715 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-01-14 16:00:36 -06:00
aayush262
a0b6956ca4 incognito notification (#130) 2024-01-14 15:59:28 -06:00
rebelonion
2f41515b33 public dir for images 2024-01-14 02:18:17 -06:00
rebelonion
063d314c36 error catch for saving image 2024-01-14 01:45:46 -06:00
aayush262
e7631e021e fixed offline mode (#124) 2024-01-13 15:30:39 -06:00
Finnley Somdahl
e65fa8d565 exoplayer string fix 2024-01-13 12:24:18 -06:00
Finnley Somdahl
14d08b9491 sad fucked up 2024-01-13 12:06:45 -06:00
Finnley Somdahl
cc5b512441 clean Sad's shit 2024-01-13 11:20:02 -06:00
Sadwhy
84e300482a Incognito and download switch (#121)
* Offline Mode and incognito Switch

* fix

* Fix 1

* Update MainActivity.kt

* Update MainActivity.kt

---------

Co-authored-by: rebel onion <87634197+rebelonion@users.noreply.github.com>
2024-01-13 11:13:54 -06:00
Finnley Somdahl
46b84ffc76 regex fix 2024-01-13 10:53:22 -06:00
Finnley Somdahl
ad1979505e ok an actual fix this time 2024-01-13 00:24:14 -06:00
Finnley Somdahl
310f068e79 better regex 2024-01-12 23:35:34 -06:00
Finnley Somdahl
431617e6b5 offline manga order fix (again) 2024-01-12 23:13:40 -06:00
Finnley Somdahl
33bb60baad download settings 2024-01-12 22:41:49 -06:00
Finnley Somdahl
e847ec21c3 paging out of bounds exception 2024-01-12 21:09:15 -06:00
Finnley Somdahl
e0a1f6534f weird faq bug 2024-01-12 21:03:48 -06:00
Finnley Somdahl
1ba67280a6 allow episode in title if no other characters 2024-01-12 21:00:05 -06:00
Finnley Somdahl
419d33a3ac allow old cast method 2024-01-12 20:54:34 -06:00
Finnley Somdahl
f12a4de04b PlatformSchedulerService in manifest 2024-01-12 20:10:55 -06:00
Finnley Somdahl
3077f39c9d variable lol 2024-01-12 19:56:59 -06:00
Yutatsu
97cd3dd43b Remove episode number from episode title like saikou (#119)
* Add files via upload

* Add files via upload

* Add files via upload

* use existing robust episode regex

* use existing robust episode regex

* use existing robust episode regex

* use existing robust episode regex

* allow external use of manga chapter regex as well

---------

Co-authored-by: rebel onion <87634197+rebelonion@users.noreply.github.com>
2024-01-12 19:51:10 -06:00
aayush262
038b8f7ff7 better FastForward bar (#117) 2024-01-09 08:42:19 -06:00
Finnley Somdahl
3d3c9feaec potential fix for out of order manga downloads 2024-01-09 06:34:51 -06:00
Finnley Somdahl
7e5def3a37 custom novel search fix 2024-01-09 06:22:25 -06:00
Finnley Somdahl
e3e3965795 manga progress check fix 2024-01-09 06:05:01 -06:00
Finnley Somdahl
158ea60047 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-01-09 00:01:23 -06:00
Finnley Somdahl
2e13d79615 auto debug disable 2024-01-09 00:01:12 -06:00
aayush262
f5297f4927 quick fix (#116) 2024-01-08 23:51:30 -06:00
Finnley Somdahl
326b848e57 manga extensions fix 2024-01-08 23:23:17 -06:00
aayush262
01f9e86475 minor tweaks (#115)
* Added telegram link

* Removed UPI (rebel not indian)

* minor changes

* Shows number of manga/Ln downloaded

* fixed list name overlapping with notch

* wrong index selection in language fixed

* novel icon

* Emerald theme name changed to Ocean

* forgot to remove

* why was these still there
2024-01-08 22:11:00 -06:00
rebel onion
af992bd19c Delete app/src/main/res/values-en-rDW directory 2024-01-04 23:32:06 -06:00
rebel onion
51b3aac0c0 Update strings.xml 2024-01-04 11:28:27 -06:00
rebel onion
8df2107ef9 debuggable false 2024-01-04 11:17:49 -06:00
aayush262
4286232d17 Language name in extension setting (#111)
* Full language name in ext settings

* added more lang name

* changed alter dialog view

* sort language by names

* 3x grid for 360DP mobiles

* Default novel settings

* Oled for LN

* Lang full name

* Notification icon changed to dantotsu

* Remember 'sort' value
2024-01-04 09:52:07 -06:00
Finnley Somdahl
ef30869b62 more visible nav 2023-12-31 06:18:55 -06:00
Finnley Somdahl
ae8b952b4c floating nav bar?? 2023-12-31 05:45:27 -06:00
Finnley Somdahl
486be4827e wrong download offset fix 2023-12-31 05:39:18 -06:00
aayush262
98a3a1107b Title😂😂 (#109)
* small changes

* Changes
* new nest button for settings
* full language name in language selector
* tv banner
* hide lang selector if there is one language only
* and some small changes

* import fix

* alter dialog

* wont refresh if nothing is changed
2023-12-31 01:25:03 -06:00
Finnley Somdahl
7228817c68 I have no idea why that crashes for some people... 2023-12-31 00:55:39 -06:00
Finnley Somdahl
7dbf951d5a download episode images 2023-12-31 00:17:18 -06:00
Finnley Somdahl
3ff492d94c Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2023-12-30 05:14:11 -06:00
Sadwhy
7fae64bee9 Ignore readme on workflow (#107) 2023-12-30 05:13:15 -06:00
Finnley Somdahl
d16fbd9a43 first working version of anime downloads 2023-12-30 05:12:46 -06:00
Finnley Somdahl
41830dba4d tap between manga pages (paged) 2023-12-28 21:57:20 -06:00
Finnley Somdahl
5561c003cf Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2023-12-28 19:27:06 -06:00
Finnley Somdahl
62b1a3b900 move countdown to 28 days 2023-12-28 19:27:03 -06:00
Finnley Somdahl
c9649751d2 rough outline for downloading anime 2023-12-28 06:38:45 -06:00
aayush262
bbc986784b small changes (#102) 2023-12-28 04:15:10 -06:00
Sadwhy
7684a15e94 Changed a few strings (#101)
* :)

Changed some strings

* More strings

* New string
2023-12-28 04:13:42 -06:00
Finnley Somdahl
42c3b42c05 incognito does not require app restart 2023-12-27 10:30:50 -06:00
Finnley Somdahl
a8711241a7 default cast setting to visible 2023-12-27 09:15:47 -06:00
Finnley Somdahl
549d7f9db3 better info on no extensions installed 2023-12-27 09:11:06 -06:00
Finnley Somdahl
e83a580486 native casting support 2023-12-27 08:58:36 -06:00
Finnley Somdahl
bf908c5e37 fix for decimal episodes 2023-12-27 07:07:10 -06:00
Finnley Somdahl
ebabff4667 fix for notification title 2023-12-27 06:42:24 -06:00
Finnley Somdahl
c352222e3a fix for downloading when all chapters are read 2023-12-27 06:34:09 -06:00
Finnley Somdahl
d177087ae6 stop refresh entire page on grid change / hide scroll to top button 2023-12-27 06:24:24 -06:00
SunglassJerry
38c5ae447a Removed manga faq (#93)
* Update README.md

* Update beta.yml

* Update strings.xml

* Update FAQActivity.kt

---------

Co-authored-by: rebel onion <87634197+rebelonion@users.noreply.github.com>
2023-12-25 21:50:41 -06:00
aayush262
eb75d299d2 Bug fixes , download page redesign , new theme(Emerald) (#95)
* Restart option when choosing custom theme
Typo fix
Extension page bug fix

* Downloaded manga page redesign(lol)

* quick fix

* New theme(Emerald)
Fine-tuned colors.xml

* Toggle for list view and compact view in downloaded manga and novels (much more)
2023-12-25 21:49:34 -06:00
Sadwhy
5339593e17 Added mismatched switch (#91)
Ayushs fault
2023-12-19 20:02:29 -06:00
aayush262
0bacfb8494 incognito view fix (#89)
* auto pip

* undo
2023-12-19 16:12:15 -06:00
rebel onion
11d04ecb58 Merge pull request #86 from Sadwhy/main
Clean up switch
2023-12-18 17:17:36 -06:00
rebel onion
74328cf4cf Merge branch 'dev' into main 2023-12-18 17:17:24 -06:00
rebel onion
cfd59a6ba0 Update DisabledReports.kt 2023-12-18 17:15:57 -06:00
rebel onion
1779276154 Update beta.yml 2023-12-18 17:15:24 -06:00
rebel onion
dfc10d5520 Merge pull request #84 from aayush2622/dev
some changes
2023-12-14 18:06:11 -06:00
sadmansaif017@gmail.com
f090f6c630 Finalized 2023-12-13 14:49:53 +06:00
sadmansaif017@gmail.com
a13f98f6da Clean Switch 2023-12-13 14:48:37 +06:00
aayush262
cc98e2f307 correct??? 2023-12-12 15:30:31 +05:30
aayush262
5c4e9d7696 Better incognito (visually)
toggle for navbar in list
2023-12-11 21:39:23 +05:30
aayush262
b180625636 . 2023-12-11 02:45:21 +05:30
aayush262
31482674c0 yt icon 2023-12-10 23:47:06 +05:30
aayush262
c7bc6241dc some changes
better manga title/list name view
2023-12-10 23:42:56 +05:30
Sadwhy
86b74f022b Update build.gradle 2023-12-10 18:50:52 +06:00
Sadwhy
7336c73561 Update build.gradle 2023-12-10 18:49:22 +06:00
Sadwhy
528f70c6de Update beta.yml 2023-12-10 12:09:42 +06:00
Sadwhy
2c0d698ac9 Tracking denied 2023-12-10 11:29:58 +06:00
rebel onion
d404202371 linux no worky 2023-12-09 23:07:30 -06:00
rebel onion
ebeffa2135 fml 2023-12-09 22:59:08 -06:00
rebel onion
51015dc2f4 Update beta.yml 2023-12-09 22:56:37 -06:00
rebel onion
b840cdb695 Update beta.yml 2023-12-09 22:39:48 -06:00
rebel onion
e6cb10df19 Update beta.yml 2023-12-09 22:29:55 -06:00
rebel onion
1cd1b8af23 Update beta.yml 2023-12-09 22:20:40 -06:00
rebel onion
2b38869c41 signing? 2023-12-09 22:19:13 -06:00
Finnley Somdahl
6c310713d6 new color picker 2023-12-09 21:20:19 -06:00
Finnley Somdahl
0a2ecdd190 Update DisabledReports.kt 2023-12-09 14:40:58 -06:00
Sadwhy
3db4363100 Merge branch 'rebelonion:main' into main 2023-12-10 02:38:20 +06:00
Finnley Somdahl
713960e247 example png 2023-12-09 14:15:19 -06:00
Finnley Somdahl
b6be7075b0 widget outline 2023-12-09 14:09:24 -06:00
Finnley Somdahl
82bc215da5 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2023-12-09 14:04:32 -06:00
Finnley Somdahl
e8f3d5525d Update AndroidManifest.xml 2023-12-09 14:04:18 -06:00
rebel onion
d1cf8c4e10 Merge pull request #81 from rebelonion/main
update dev
2023-12-09 13:56:48 -06:00
Sadwhy
9eb29361dc Add files via upload 2023-12-10 01:49:06 +06:00
rebel onion
133959a34e target dev 2023-12-09 13:47:35 -06:00
Sadwhy
bd48ff05eb Delete build.gradle 2023-12-10 01:46:45 +06:00
rebel onion
1d2ce6ccaa Delete app/src/debug/google-services.json 2023-12-09 13:38:38 -06:00
Sadwhy
85f03ece85 Add files via upload 2023-12-10 01:24:03 +06:00
Sadwhy
e2f02dc93c Add files via upload 2023-12-10 01:22:01 +06:00
Sadwhy
88c4d1f8a7 Add files via upload 2023-12-10 01:15:42 +06:00
Sadwhy
2c24a56446 Update build.gradle 2023-12-10 01:09:21 +06:00
Sadwhy
d11b370415 Update build.gradle 2023-12-10 01:04:43 +06:00
Sadwhy
f81c566f12 Update build.gradle 2023-12-10 00:38:59 +06:00
Sadwhy
9a4ed7ad54 Update build.gradle 2023-12-10 00:36:52 +06:00
Sadwhy
07793b11d6 Added a GitHub runner and uploads it to discord 2023-12-10 00:26:28 +06:00
634 changed files with 30647 additions and 9973 deletions

View File

@@ -4,47 +4,120 @@ on:
push: push:
branches: branches:
- dev - dev
paths-ignore:
- '**/README.md'
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
CI: true
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download last SHA artifact
uses: dawidd6/action-download-artifact@v3
with:
workflow: beta.yml
name: last-sha
path: .
continue-on-error: true
- name: Get Commits Since Last Run
run: |
if [ -f last_sha.txt ]; then
LAST_SHA=$(cat last_sha.txt)
else
# Fallback to first commit if no previous SHA available
LAST_SHA=$(git rev-list --max-parents=0 HEAD)
fi
echo "Commits since $LAST_SHA:"
# Accumulate commit logs in a shell variable
COMMIT_LOGS=$(git log $LAST_SHA..HEAD --pretty=format:"● %s ~%an")
# URL-encode the newline characters for GitHub Actions
COMMIT_LOGS="${COMMIT_LOGS//'%'/'%25'}"
COMMIT_LOGS="${COMMIT_LOGS//$'\n'/'%0A'}"
COMMIT_LOGS="${COMMIT_LOGS//$'\r'/'%0D'}"
# Append the encoded commit logs to the COMMIT_LOG environment variable
echo "COMMIT_LOG=${COMMIT_LOGS}" >> $GITHUB_ENV
# Debugging: Print the variable to check its content
echo "$COMMIT_LOGS"
shell: /usr/bin/bash -e {0}
env:
CI: true
- name: Save Current SHA for Next Run
run: echo ${{ github.sha }} > last_sha.txt
- name: Set variables - name: Set variables
run: | run: |
VER=$(grep -E -o "versionName \".*\"" app/build.gradle | sed -e 's/versionName //g' | tr -d '"') VER=$(grep -E -o "versionName \".*\"" app/build.gradle | sed -e 's/versionName //g' | tr -d '"')
SHA=${{ github.sha }} SHA=${{ github.sha }}
VERSION="$VER.${SHA:0:7}" VERSION="$VER+${SHA:0:7}"
echo "Version $VERSION" echo "Version $VERSION"
echo "VERSION=$VERSION" >> $GITHUB_ENV echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: Setup JDK 17 - name: Setup JDK 17
uses: actions/setup-java@v3 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' distribution: 'temurin'
java-version: 17 java-version: 17
cache: gradle cache: gradle
- name: Decode Keystore File
run: echo "${{ secrets.KEYSTORE_FILE }}" | base64 -d > $GITHUB_WORKSPACE/key.keystore
- name: List files in the directory
run: ls -l
- name: Make gradlew executable - name: Make gradlew executable
run: chmod +x ./gradlew run: chmod +x ./gradlew
- name: Build with Gradle - name: Build with Gradle
run: ./gradlew assembleDebug run: ./gradlew assembleGoogleAlpha -Pandroid.injected.signing.store.file=$GITHUB_WORKSPACE/key.keystore -Pandroid.injected.signing.store.password=${{ secrets.KEYSTORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.KEY_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.KEY_PASSWORD }}
- name: Upload a Build Artifact - name: Upload a Build Artifact
uses: actions/upload-artifact@v3.0.0 uses: actions/upload-artifact@v4.3.1
with: with:
name: Dantotsu name: Dantotsu
path: "app/build/outputs/apk/debug/app-debug.apk" path: "app/build/outputs/apk/google/alpha/app-google-alpha.apk"
- name: Upload APK to Discord - name: Upload APK to Discord and Telegram
if: ${{ github.repository == 'rebelonion/Dantotsu' }}
shell: bash shell: bash
run: | run: |
contentbody=$( jq -Rsa . <<< "${{ github.event.head_commit.message }}" ) #Discord
curl -F "payload_json={\"content\":\" everyone **${{ env.VERSION }}**\n\n${contentbody:1:-1}\"}" -F "dantotsu_debug=@app/build/outputs/apk/debug/app-debug.apk" ${{ secrets.DISCORD_WEBHOOK }} commit_messages=$(echo "$COMMIT_LOG" | sed 's/%0A/\n/g; s/^/\n/')
# Truncate commit messages if they are too long
max_length=1900 # Adjust this value as needed
if [ ${#commit_messages} -gt $max_length ]; then
commit_messages="${commit_messages:0:$max_length}... (truncated)"
fi
contentbody=$( jq -nc --arg msg "Alpha-Build: <@714249925248024617> **$VERSION**:" --arg commits "$commit_messages" '{"content": ($msg + "\n" + $commits)}' )
curl -F "payload_json=${contentbody}" -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }}
#Telegram
curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \
-F "document=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" \
-F "caption=Alpha-Build: ${VERSION}: ${commit_messages}" \
https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument
env:
COMMIT_LOG: ${{ env.COMMIT_LOG }}
VERSION: ${{ env.VERSION }}
- name: Upload Current SHA as Artifact
uses: actions/upload-artifact@v2
with:
name: last-sha
path: last_sha.txt
- name: Delete Old Pre-Releases - name: Delete Old Pre-Releases
id: delete-pre-releases id: delete-pre-releases

3
.gitignore vendored
View File

@@ -8,6 +8,9 @@ local.properties
# Log/OS Files # Log/OS Files
*.log *.log
# Secrets
apikey.properties
# Android Studio generated files and folders # Android Studio generated files and folders
captures/ captures/
.externalNativeBuild/ .externalNativeBuild/

View File

@@ -3,8 +3,9 @@
</p> </p>
<p align="center"> <p align="center">
<img src="https://img.shields.io/badge/platforms-android-blueviolet?style=for-the-badge"/> <img src="https://img.shields.io/badge/platforms-android-blueviolet?style=for-the-badge"/>
<a href="https://discord.gg/4HPZ5nAWwM"><img src="https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white"></a>
<a href="https://github.com/rebelonion/Dantotsu/releases"><img src="https://img.shields.io/github/downloads/rebelonion/Dantotsu/total?color=%233DDC84&logo=android&logoColor=%23fff&style=for-the-badge"></a> <a href="https://github.com/rebelonion/Dantotsu/releases"><img src="https://img.shields.io/github/downloads/rebelonion/Dantotsu/total?color=%233DDC84&logo=android&logoColor=%23fff&style=for-the-badge"></a>
<a href="https://www.codefactor.io/repository/github/rebelonion/dantotsu"><img src="https://www.codefactor.io/repository/github/rebelonion/dantotsu/badge?color=%233DDC84&logo=android&logoColor=%23fff&style=for-the-badge" alt="CodeFactor" /></a>
<a href="https://discord.gg/4HPZ5nAWwM"><img src="https://img.shields.io/discord/358599430502481920.svg?style=for-the-badge&logo=discord&colorB=7289DA"></a>
</p> </p>
# **Dantotsu** 🌟 # **Dantotsu** 🌟

4
app/.gitignore vendored
View File

@@ -1,4 +1,8 @@
/build /build
/debug /debug
/debug/output-metadata.json /debug/output-metadata.json
/alpha
/alpha/output-metadata.json
/google/*
/fdroid/*
/release /release

View File

@@ -1,44 +1,66 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'com.google.gms.google-services'
id 'com.google.firebase.crashlytics'
id 'kotlin-android' id 'kotlin-android'
id 'kotlinx-serialization' id 'kotlinx-serialization'
id 'org.jetbrains.kotlin.android' id 'org.jetbrains.kotlin.android'
id 'com.google.devtools.ksp' id 'com.google.devtools.ksp'
} }
def gitCommitHash = providers.exec {
commandLine("git", "rev-parse", "--verify", "--short", "HEAD")
}.standardOutput.asText.get().trim()
android { android {
compileSdk 34 compileSdk 34
defaultConfig { defaultConfig {
applicationId "ani.dantotsu" applicationId "ani.dantotsu"
minSdk 23 minSdk 21
targetSdk 34 targetSdk 34
versionCode ((System.currentTimeMillis() / 60000).toInteger()) versionCode((System.currentTimeMillis() / 60000).toInteger())
versionName "2.0.0-beta00-i" versionName "3.0.0"
versionCode 300000000
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
} }
flavorDimensions += "store"
productFlavors {
fdroid {
// F-Droid specific configuration
dimension "store"
versionNameSuffix "-fdroid"
}
google {
// Google Play specific configuration
dimension "store"
isDefault true
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
}
}
buildTypes { buildTypes {
alpha {
applicationIdSuffix ".beta" // keep as beta by popular request
versionNameSuffix "-alpha01"
manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher_alpha"
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_alpha_round"
debuggable System.getenv("CI") == null
isDefault true
}
debug { debug {
applicationIdSuffix ".beta" applicationIdSuffix ".beta"
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_beta", icon_placeholder_round: "@mipmap/ic_launcher_beta_round"] versionNameSuffix "-beta01"
debuggable true manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher_beta"
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_beta_round"
debuggable false
} }
release { release {
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher", icon_placeholder_round: "@mipmap/ic_launcher_round"] manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher"
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_round"
debuggable false debuggable false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-gson.pro', 'proguard-rules.pro'
} }
} }
buildFeatures { buildFeatures {
viewBinding true viewBinding true
buildConfig true
} }
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_17 sourceCompatibility JavaVersion.VERSION_17
@@ -52,22 +74,27 @@ android {
} }
dependencies { dependencies {
// FireBase
googleImplementation platform('com.google.firebase:firebase-bom:32.7.4')
googleImplementation 'com.google.firebase:firebase-analytics-ktx:21.5.1'
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.2'
// Core // Core
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.browser:browser:1.6.0' implementation 'androidx.browser:browser:1.8.0'
implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.fragment:fragment-ktx:1.6.1' implementation 'androidx.fragment:fragment-ktx:1.6.2'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.multidex:multidex:2.0.1'
implementation "androidx.work:work-runtime-ktx:2.8.1" implementation "androidx.work:work-runtime-ktx:2.9.0"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.google.code.gson:gson:2.8.9' implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.github.Blatzar:NiceHttp:0.4.4' implementation 'com.github.Blatzar:NiceHttp:0.4.4'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3'
implementation 'androidx.preference:preference:1.2.1' implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.webkit:webkit:1.9.0' implementation 'androidx.webkit:webkit:1.10.0'
// Glide // Glide
ext.glide_version = '4.16.0' ext.glide_version = '4.16.0'
@@ -77,30 +104,44 @@ dependencies {
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version" implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
implementation 'jp.wasabeef:glide-transformations:4.3.0' implementation 'jp.wasabeef:glide-transformations:4.3.0'
// FireBase
implementation platform('com.google.firebase:firebase-bom:32.2.3')
implementation 'com.google.firebase:firebase-analytics-ktx:21.5.0'
implementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.0'
// Exoplayer // Exoplayer
ext.exo_version = '1.2.0' ext.exo_version = '1.3.0'
implementation "androidx.media3:media3-exoplayer:$exo_version" implementation "androidx.media3:media3-exoplayer:$exo_version"
implementation "androidx.media3:media3-ui:$exo_version" implementation "androidx.media3:media3-ui:$exo_version"
implementation "androidx.media3:media3-exoplayer-hls:$exo_version" implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
implementation "androidx.media3:media3-exoplayer-dash:$exo_version" implementation "androidx.media3:media3-exoplayer-dash:$exo_version"
implementation "androidx.media3:media3-datasource-okhttp:$exo_version" implementation "androidx.media3:media3-datasource-okhttp:$exo_version"
implementation "androidx.media3:media3-session:$exo_version" implementation "androidx.media3:media3-session:$exo_version"
//media3 casting
implementation "androidx.media3:media3-cast:$exo_version"
implementation "androidx.mediarouter:mediarouter:1.6.0"
// UI // UI
implementation 'com.google.android.material:material:1.10.0' implementation 'com.google.android.material:material:1.11.0'
implementation 'nl.joery.animatedbottombar:library:1.1.0' //implementation 'nl.joery.animatedbottombar:library:1.1.0'
implementation 'io.noties.markwon:core:4.6.2' implementation 'com.github.rebelonion:AnimatedBottomBar:v1.1.0'
implementation 'com.flaviofaria:kenburnsview:1.0.7' implementation 'com.flaviofaria:kenburnsview:1.0.7'
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.alexvasilkov:gesture-views:2.8.3' implementation 'com.alexvasilkov:gesture-views:2.8.3'
implementation 'com.github.VipulOG:ebook-reader:0.1.6' implementation 'com.github.VipulOG:ebook-reader:0.1.6'
implementation 'androidx.paging:paging-runtime-ktx:3.2.1' implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
implementation "com.github.skydoves:colorpickerview:2.3.0" implementation 'com.github.eltos:simpledialogfragments:v3.7'
implementation 'com.github.AAChartModel:AAChartCore-Kotlin:93972bc'
// Markwon
ext.markwon_version = '4.6.2'
implementation "io.noties.markwon:core:$markwon_version"
implementation "io.noties.markwon:editor:$markwon_version"
implementation "io.noties.markwon:ext-strikethrough:$markwon_version"
implementation "io.noties.markwon:ext-tables:$markwon_version"
implementation "io.noties.markwon:ext-tasklist:$markwon_version"
implementation "io.noties.markwon:html:$markwon_version"
implementation "io.noties.markwon:image-glide:$markwon_version"
// Groupie
ext.groupie_version = '2.10.1'
implementation "com.github.lisawray.groupie:groupie:$groupie_version"
implementation "com.github.lisawray.groupie:groupie-viewbinding:$groupie_version"
// string matching // string matching
implementation 'me.xdrop:fuzzywuzzy:1.4.0' implementation 'me.xdrop:fuzzywuzzy:1.4.0'
@@ -112,16 +153,17 @@ dependencies {
implementation 'ca.gosyer:voyager-navigator:1.0.0-rc07' implementation 'ca.gosyer:voyager-navigator:1.0.0-rc07'
implementation 'com.squareup.logcat:logcat:0.1' implementation 'com.squareup.logcat:logcat:0.1'
implementation 'com.github.inorichi.injekt:injekt-core:65b0440' implementation 'com.github.inorichi.injekt:injekt-core:65b0440'
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11' implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.12'
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11' implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps' implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'
implementation 'com.squareup.okio:okio:3.3.0' implementation 'com.squareup.okio:okio:3.8.0'
implementation 'ch.acra:acra-http:5.9.7' implementation 'com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.12'
implementation 'org.jsoup:jsoup:1.15.4' implementation 'org.jsoup:jsoup:1.16.1'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.5.0' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.3'
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0' implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
implementation 'com.github.tachiyomiorg:unifile:17bec43' implementation 'com.github.tachiyomiorg:unifile:17bec43'
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1' implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'app.cash.quickjs:quickjs-android:0.9.2'
} }

View File

@@ -43,6 +43,25 @@
} }
} }
}, },
{
"client_info": {
"mobilesdk_app_id": "1:1039200814590:android:40e14720ee97917e1aacaf",
"android_client_info": {
"package_name": "ani.dantotsu.alpha"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyCiXo_q4S2ofA5oCztsoLnlDqJi3GtTJjY"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
},
{ {
"client_info": { "client_info": {
"mobilesdk_app_id": "1:1039200814590:android:40e14720ee97917e1aacaf", "mobilesdk_app_id": "1:1039200814590:android:40e14720ee97917e1aacaf",

View File

@@ -43,6 +43,25 @@
public static <1> INSTANCE; public static <1> INSTANCE;
kotlinx.serialization.KSerializer serializer(...); kotlinx.serialization.KSerializer serializer(...);
} }
-keep class ani.dantotsu.** { *; }
-keep class ani.dantotsu.download.DownloadsManager { *; }
-keepattributes Signature
-keep class uy.kohesive.injekt.** { *; }
-keep class eu.kanade.tachiyomi.** { *; }
-keep class kotlin.** { *; }
-dontwarn kotlin.**
-keep class kotlinx.** { *; }
-keepclassmembers class uy.kohesive.injekt.api.FullTypeReference {
<init>(...);
}
-keep class com.google.gson.** { *; }
-keepattributes *Annotation*
-keepattributes EnclosingMethod
-keep class com.google.gson.reflect.TypeToken { *; }
-keep class org.jsoup.** { *; }
-keepclassmembers class org.jsoup.nodes.Document { *; }
# @Serializable and @Polymorphic are used at runtime for polymorphic serialization. # @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault -keepattributes RuntimeVisibleAnnotations,AnnotationDefault

View File

@@ -0,0 +1,376 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="768dp"
android:height="768dp"
android:viewportWidth="768"
android:viewportHeight="768">
<group
android:name="wrapper"
android:pivotX="384"
android:pivotY="384">
<clip-path
android:name="clippath"
android:pathData="M 384 128.04 C 329.836 127.869 276.99 144.889 233.11 176.638 C 189.23 208.387 156.539 253.255 139.769 304.75 C 122.999 356.244 122.999 411.756 139.769 463.25 C 156.539 514.745 189.23 559.613 233.11 591.362 C 276.99 623.111 329.836 640.131 384 639.96 C 451.869 639.96 517.028 612.974 565.019 564.991 C 613.01 517.008 640 451.859 640 384 C 640 316.141 613.01 250.992 565.019 203.009 C 517.028 155.026 451.869 128.04 384 128.04 Z" />
<group android:name="group">
<group android:name="group_1">
<path
android:name="path"
android:fillColor="#ED0021"
android:pathData="M 128 128 L 640 128 L 640 639.96 L 128 639.96 Z"
android:strokeWidth="1" />
<group
android:name="group_12"
android:pivotX="384"
android:pivotY="384">
<path
android:name="path_2"
android:fillColor="#D40037"
android:pathData="M 384 211.74 C 338.331 211.74 294.486 229.901 262.194 262.194 C 229.901 294.486 211.74 338.331 211.74 384 C 211.74 429.669 229.901 473.514 262.194 505.806 C 294.486 538.099 338.331 556.26 384 556.26 C 429.669 556.26 473.514 538.099 505.806 505.806 C 538.099 473.514 556.26 429.669 556.26 384 C 556.26 338.331 538.099 294.486 505.806 262.194 C 473.514 229.901 429.669 211.74 384 211.74 Z"
android:strokeWidth="1" />
</group>
</group>
<group android:name="group_2">
<group android:name="group_7">
<group android:name="group_10">
<group
android:name="group_11"
android:pivotX="94"
android:pivotY="440"
android:rotation="-90">
<path
android:name="path_1"
android:fillColor="#A70060"
android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z"
android:strokeWidth="1" />
<clip-path
android:name="mask_2"
android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z" />
</group>
</group>
<group
android:name="group_13"
android:pivotX="384"
android:pivotY="384">
<clip-path
android:name="mask_1"
android:pathData="M 384 211.74 C 338.331 211.74 294.486 229.901 262.194 262.194 C 229.901 294.486 211.74 338.331 211.74 384 C 211.74 429.669 229.901 473.514 262.194 505.806 C 294.486 538.099 338.331 556.26 384 556.26 C 429.669 556.26 473.514 538.099 505.806 505.806 C 538.099 473.514 556.26 429.669 556.26 384 C 556.26 338.331 538.099 294.486 505.806 262.194 C 473.514 229.901 429.669 211.74 384 211.74 Z" />
<group
android:name="group_9"
android:pivotX="94"
android:pivotY="440"
android:rotation="-90">
<path
android:name="path_3"
android:fillColor="#BF005E"
android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z"
android:strokeWidth="1" />
</group>
</group>
<group
android:name="group_6"
android:pivotX="94"
android:pivotY="440"
android:rotation="-5"
android:scaleX="1.2"
android:scaleY="1.2" />
</group>
<group
android:name="group_8"
android:pivotX="94"
android:pivotY="440"
android:rotation="-90">
<group
android:name="group_14"
android:pivotX="94"
android:pivotY="440">
<path
android:name="path_4"
android:fillColor="#C70051"
android:pathData="M 539.28 128 C 503.71 317.07 337.72 460.12 138.31 460.12 C 134.86 460.12 131.42 460.06 128 459.98 L 128 465.73 C 168.23 476.19 210.43 481.78 253.93 481.78 C 409.53 481.78 548.48 410.55 640 298.94 L 640 128.01 L 539.28 128.01 Z"
android:strokeWidth="1" />
</group>
</group>
</group>
<group
android:name="group_3"
android:translateX="-360">
<path
android:name="path_6"
android:fillColor="#251528"
android:pathData="M 481.82 384 C 481.82 438.03 438.02 481.82 384 481.82 L 0 481.82 L 0 286.18 L 384 286.18 C 438.02 286.18 481.82 329.98 481.82 384 Z"
android:strokeWidth="1" />
</group>
<group
android:name="group_4"
android:pivotX="384"
android:pivotY="384"
android:scaleX="1.5"
android:scaleY="1.5">
<path
android:name="path_5"
android:fillColor="#251528"
android:pathData="M 44.26 128 C 44.26 174.25 81.75 211.74 128 211.74 L 384 211.74 C 479.13 211.74 556.26 288.86 556.26 384 C 556.26 479.13 479.14 556.26 384 556.26 L 128 556.26 C 81.76 556.26 44.28 593.73 44.26 639.97 L 768 639.97 L 768 128 L 44.26 128 Z"
android:strokeWidth="1" />
</group>
<group
android:name="group_5"
android:pivotX="384"
android:pivotY="384"
android:rotation="-15"
android:scaleX="3"
android:scaleY="3">
<path
android:name="path_7"
android:fillAlpha="0"
android:fillColor="#FFD8DF"
android:pathData="M 442 366.7 L 365.98 322.81 C 352.66 315.12 336.02 324.73 336.02 340.11 L 336.02 427.89 C 336.02 443.27 352.67 452.88 365.98 445.19 L 442 401.3 C 455.32 393.61 455.32 374.39 442 366.7 Z"
android:strokeWidth="1" />
</group>
</group>
</group>
</vector>
</aapt:attr>
<target android:name="wrapper">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="500"
android:interpolator="@android:anim/overshoot_interpolator"
android:propertyName="scaleX"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
<objectAnimator
android:duration="500"
android:interpolator="@android:anim/overshoot_interpolator"
android:propertyName="scaleY"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="group_6">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="550"
android:interpolator="@android:anim/overshoot_interpolator"
android:propertyName="rotation"
android:startOffset="350"
android:valueFrom="-10"
android:valueTo="0"
android:valueType="floatType" />
<objectAnimator
android:duration="300"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="scaleX"
android:startOffset="350"
android:valueFrom="1.2"
android:valueTo="1"
android:valueType="floatType" />
<objectAnimator
android:duration="300"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="scaleY"
android:startOffset="350"
android:valueFrom="1.2"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="group_3">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="400"
android:interpolator="@android:anim/overshoot_interpolator"
android:propertyName="translateX"
android:startOffset="250"
android:valueFrom="-360"
android:valueTo="0"
android:valueType="floatType" />
</aapt:attr>
</target>
<target android:name="group_4">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="350"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="scaleX"
android:startOffset="400"
android:valueFrom="1.5"
android:valueTo="1"
android:valueType="floatType" />
<objectAnimator
android:duration="350"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="scaleY"
android:startOffset="400"
android:valueFrom="1.5"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="path_7">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="550"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="fillAlpha"
android:startOffset="350"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</aapt:attr>
</target>
<target android:name="group_5">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="550"
android:interpolator="@android:anim/decelerate_interpolator"
android:propertyName="rotation"
android:startOffset="350"
android:valueFrom="-45"
android:valueTo="0"
android:valueType="floatType" />
<objectAnimator
android:duration="550"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="scaleX"
android:startOffset="350"
android:valueFrom="3"
android:valueTo="1"
android:valueType="floatType" />
<objectAnimator
android:duration="550"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="scaleY"
android:startOffset="350"
android:valueFrom="3"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="group_8">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="350"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="rotation"
android:startOffset="100"
android:valueFrom="-90"
android:valueTo="0"
android:valueType="floatType" />
</aapt:attr>
</target>
<target android:name="group_9">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="350"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="rotation"
android:startOffset="100"
android:valueFrom="-90"
android:valueTo="0"
android:valueType="floatType" />
<objectAnimator
android:duration="350"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="scaleX"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
<objectAnimator
android:duration="350"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="scaleY"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="group_11">
<aapt:attr name="android:animation">
<objectAnimator
android:duration="350"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="rotation"
android:startOffset="100"
android:valueFrom="-90"
android:valueTo="0"
android:valueType="floatType" />
</aapt:attr>
</target>
<target android:name="group_12">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="550"
android:interpolator="@android:anim/overshoot_interpolator"
android:propertyName="scaleX"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
<objectAnimator
android:duration="550"
android:interpolator="@android:anim/overshoot_interpolator"
android:propertyName="scaleY"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="group_13">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="550"
android:interpolator="@android:anim/overshoot_interpolator"
android:propertyName="scaleX"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
<objectAnimator
android:duration="550"
android:interpolator="@android:anim/overshoot_interpolator"
android:propertyName="scaleY"
android:valueFrom="0"
android:valueTo="1"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
<target android:name="group_14">
<aapt:attr name="android:animation">
<set>
<objectAnimator
android:duration="200"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:propertyName="rotation"
android:startOffset="350"
android:valueFrom="5"
android:valueTo="0"
android:valueType="floatType" />
<objectAnimator
android:duration="100"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:propertyName="rotation"
android:startOffset="250"
android:valueFrom="0"
android:valueTo="5"
android:valueType="floatType" />
</set>
</aapt:attr>
</target>
</animated-vector>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Dantotsu α</string>
</resources>

View File

@@ -1,5 +1,4 @@
<animated-vector <animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"> xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable"> <aapt:attr name="android:drawable">
<vector <vector
@@ -14,23 +13,23 @@
android:pivotY="384"> android:pivotY="384">
<clip-path <clip-path
android:name="clippath" android:name="clippath"
android:pathData="M 384 128.04 C 329.836 127.869 276.99 144.889 233.11 176.638 C 189.23 208.387 156.539 253.255 139.769 304.75 C 122.999 356.244 122.999 411.756 139.769 463.25 C 156.539 514.745 189.23 559.613 233.11 591.362 C 276.99 623.111 329.836 640.131 384 639.96 C 451.869 639.96 517.028 612.974 565.019 564.991 C 613.01 517.008 640 451.859 640 384 C 640 316.141 613.01 250.992 565.019 203.009 C 517.028 155.026 451.869 128.04 384 128.04 Z"/> android:pathData="M 384 128.04 C 329.836 127.869 276.99 144.889 233.11 176.638 C 189.23 208.387 156.539 253.255 139.769 304.75 C 122.999 356.244 122.999 411.756 139.769 463.25 C 156.539 514.745 189.23 559.613 233.11 591.362 C 276.99 623.111 329.836 640.131 384 639.96 C 451.869 639.96 517.028 612.974 565.019 564.991 C 613.01 517.008 640 451.859 640 384 C 640 316.141 613.01 250.992 565.019 203.009 C 517.028 155.026 451.869 128.04 384 128.04 Z" />
<group android:name="group"> <group android:name="group">
<group android:name="group_1"> <group android:name="group_1">
<path <path
android:name="path" android:name="path"
android:pathData="M 128 128 L 640 128 L 640 639.96 L 128 639.96 Z"
android:fillColor="#6901fd" android:fillColor="#6901fd"
android:strokeWidth="1"/> android:pathData="M 128 128 L 640 128 L 640 639.96 L 128 639.96 Z"
android:strokeWidth="1" />
<group <group
android:name="group_12" android:name="group_12"
android:pivotX="384" android:pivotX="384"
android:pivotY="384"> android:pivotY="384">
<path <path
android:name="path_2" android:name="path_2"
android:pathData="M 384 211.74 C 338.331 211.74 294.486 229.901 262.194 262.194 C 229.901 294.486 211.74 338.331 211.74 384 C 211.74 429.669 229.901 473.514 262.194 505.806 C 294.486 538.099 338.331 556.26 384 556.26 C 429.669 556.26 473.514 538.099 505.806 505.806 C 538.099 473.514 556.26 429.669 556.26 384 C 556.26 338.331 538.099 294.486 505.806 262.194 C 473.514 229.901 429.669 211.74 384 211.74 Z"
android:fillColor="#4800e5" android:fillColor="#4800e5"
android:strokeWidth="1"/> android:pathData="M 384 211.74 C 338.331 211.74 294.486 229.901 262.194 262.194 C 229.901 294.486 211.74 338.331 211.74 384 C 211.74 429.669 229.901 473.514 262.194 505.806 C 294.486 538.099 338.331 556.26 384 556.26 C 429.669 556.26 473.514 538.099 505.806 505.806 C 538.099 473.514 556.26 429.669 556.26 384 C 556.26 338.331 538.099 294.486 505.806 262.194 C 473.514 229.901 429.669 211.74 384 211.74 Z"
android:strokeWidth="1" />
</group> </group>
</group> </group>
<group android:name="group_2"> <group android:name="group_2">
@@ -43,12 +42,12 @@
android:rotation="-90"> android:rotation="-90">
<path <path
android:name="path_1" android:name="path_1"
android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z"
android:fillColor="#2000bd" android:fillColor="#2000bd"
android:strokeWidth="1"/> android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z"
android:strokeWidth="1" />
<clip-path <clip-path
android:name="mask_2" android:name="mask_2"
android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z"/> android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z" />
</group> </group>
</group> </group>
<group <group
@@ -57,7 +56,7 @@
android:pivotY="384"> android:pivotY="384">
<clip-path <clip-path
android:name="mask_1" android:name="mask_1"
android:pathData="M 384 211.74 C 338.331 211.74 294.486 229.901 262.194 262.194 C 229.901 294.486 211.74 338.331 211.74 384 C 211.74 429.669 229.901 473.514 262.194 505.806 C 294.486 538.099 338.331 556.26 384 556.26 C 429.669 556.26 473.514 538.099 505.806 505.806 C 538.099 473.514 556.26 429.669 556.26 384 C 556.26 338.331 538.099 294.486 505.806 262.194 C 473.514 229.901 429.669 211.74 384 211.74 Z"/> android:pathData="M 384 211.74 C 338.331 211.74 294.486 229.901 262.194 262.194 C 229.901 294.486 211.74 338.331 211.74 384 C 211.74 429.669 229.901 473.514 262.194 505.806 C 294.486 538.099 338.331 556.26 384 556.26 C 429.669 556.26 473.514 538.099 505.806 505.806 C 538.099 473.514 556.26 429.669 556.26 384 C 556.26 338.331 538.099 294.486 505.806 262.194 C 473.514 229.901 429.669 211.74 384 211.74 Z" />
<group <group
android:name="group_9" android:name="group_9"
android:pivotX="94" android:pivotX="94"
@@ -65,18 +64,18 @@
android:rotation="-90"> android:rotation="-90">
<path <path
android:name="path_3" android:name="path_3"
android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z"
android:fillColor="#1e00d1" android:fillColor="#1e00d1"
android:strokeWidth="1"/> android:pathData="M 128 128 L 128 463.26 C 151.32 466.96 175.23 468.89 199.58 468.89 C 411.17 468.89 588.92 323.99 639.01 128 L 128 128 Z"
android:strokeWidth="1" />
</group> </group>
</group> </group>
<group <group
android:name="group_6" android:name="group_6"
android:pivotX="94" android:pivotX="94"
android:pivotY="440" android:pivotY="440"
android:rotation="-5"
android:scaleX="1.2" android:scaleX="1.2"
android:scaleY="1.2" android:scaleY="1.2" />
android:rotation="-5"/>
</group> </group>
<group <group
android:name="group_8" android:name="group_8"
@@ -89,9 +88,9 @@
android:pivotY="440"> android:pivotY="440">
<path <path
android:name="path_4" android:name="path_4"
android:pathData="M 539.28 128 C 503.71 317.07 337.72 460.12 138.31 460.12 C 134.86 460.12 131.42 460.06 128 459.98 L 128 465.73 C 168.23 476.19 210.43 481.78 253.93 481.78 C 409.53 481.78 548.48 410.55 640 298.94 L 640 128.01 L 539.28 128.01 Z"
android:fillColor="#2900da" android:fillColor="#2900da"
android:strokeWidth="1"/> android:pathData="M 539.28 128 C 503.71 317.07 337.72 460.12 138.31 460.12 C 134.86 460.12 131.42 460.06 128 459.98 L 128 465.73 C 168.23 476.19 210.43 481.78 253.93 481.78 C 409.53 481.78 548.48 410.55 640 298.94 L 640 128.01 L 539.28 128.01 Z"
android:strokeWidth="1" />
</group> </group>
</group> </group>
</group> </group>
@@ -100,9 +99,9 @@
android:translateX="-360"> android:translateX="-360">
<path <path
android:name="path_6" android:name="path_6"
android:pathData="M 481.82 384 C 481.82 438.03 438.02 481.82 384 481.82 L 0 481.82 L 0 286.18 L 384 286.18 C 438.02 286.18 481.82 329.98 481.82 384 Z"
android:fillColor="#1f1f30" android:fillColor="#1f1f30"
android:strokeWidth="1"/> android:pathData="M 481.82 384 C 481.82 438.03 438.02 481.82 384 481.82 L 0 481.82 L 0 286.18 L 384 286.18 C 438.02 286.18 481.82 329.98 481.82 384 Z"
android:strokeWidth="1" />
</group> </group>
<group <group
android:name="group_4" android:name="group_4"
@@ -112,23 +111,23 @@
android:scaleY="1.5"> android:scaleY="1.5">
<path <path
android:name="path_5" android:name="path_5"
android:pathData="M 44.26 128 C 44.26 174.25 81.75 211.74 128 211.74 L 384 211.74 C 479.13 211.74 556.26 288.86 556.26 384 C 556.26 479.13 479.14 556.26 384 556.26 L 128 556.26 C 81.76 556.26 44.28 593.73 44.26 639.97 L 768 639.97 L 768 128 L 44.26 128 Z"
android:fillColor="#1f1f30" android:fillColor="#1f1f30"
android:strokeWidth="1"/> android:pathData="M 44.26 128 C 44.26 174.25 81.75 211.74 128 211.74 L 384 211.74 C 479.13 211.74 556.26 288.86 556.26 384 C 556.26 479.13 479.14 556.26 384 556.26 L 128 556.26 C 81.76 556.26 44.28 593.73 44.26 639.97 L 768 639.97 L 768 128 L 44.26 128 Z"
android:strokeWidth="1" />
</group> </group>
<group <group
android:name="group_5" android:name="group_5"
android:pivotX="384" android:pivotX="384"
android:pivotY="384" android:pivotY="384"
android:rotation="-15"
android:scaleX="3" android:scaleX="3"
android:scaleY="3" android:scaleY="3">
android:rotation="-15">
<path <path
android:name="path_7" android:name="path_7"
android:pathData="M 442 366.7 L 365.98 322.81 C 352.66 315.12 336.02 324.73 336.02 340.11 L 336.02 427.89 C 336.02 443.27 352.67 452.88 365.98 445.19 L 442 401.3 C 455.32 393.61 455.32 374.39 442 366.7 Z"
android:fillColor="#efe7ff"
android:fillAlpha="0" android:fillAlpha="0"
android:strokeWidth="1"/> android:fillColor="#efe7ff"
android:pathData="M 442 366.7 L 365.98 322.81 C 352.66 315.12 336.02 324.73 336.02 340.11 L 336.02 427.89 C 336.02 443.27 352.67 452.88 365.98 445.19 L 442 401.3 C 455.32 393.61 455.32 374.39 442 366.7 Z"
android:strokeWidth="1" />
</group> </group>
</group> </group>
</group> </group>
@@ -138,19 +137,19 @@
<aapt:attr name="android:animation"> <aapt:attr name="android:animation">
<set> <set>
<objectAnimator <objectAnimator
android:duration="500"
android:interpolator="@android:anim/overshoot_interpolator"
android:propertyName="scaleX" android:propertyName="scaleX"
android:duration="500"
android:valueFrom="0" android:valueFrom="0"
android:valueTo="1" android:valueTo="1"
android:valueType="floatType" android:valueType="floatType" />
android:interpolator="@android:anim/overshoot_interpolator"/>
<objectAnimator <objectAnimator
android:propertyName="scaleY"
android:duration="500" android:duration="500"
android:interpolator="@android:anim/overshoot_interpolator"
android:propertyName="scaleY"
android:valueFrom="0" android:valueFrom="0"
android:valueTo="1" android:valueTo="1"
android:valueType="floatType" android:valueType="floatType" />
android:interpolator="@android:anim/overshoot_interpolator"/>
</set> </set>
</aapt:attr> </aapt:attr>
</target> </target>
@@ -158,177 +157,177 @@
<aapt:attr name="android:animation"> <aapt:attr name="android:animation">
<set> <set>
<objectAnimator <objectAnimator
android:duration="550"
android:interpolator="@android:anim/overshoot_interpolator"
android:propertyName="rotation" android:propertyName="rotation"
android:startOffset="350" android:startOffset="350"
android:duration="550"
android:valueFrom="-10" android:valueFrom="-10"
android:valueTo="0" android:valueTo="0"
android:valueType="floatType" android:valueType="floatType" />
android:interpolator="@android:anim/overshoot_interpolator"/>
<objectAnimator <objectAnimator
android:duration="300"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="scaleX" android:propertyName="scaleX"
android:startOffset="350" android:startOffset="350"
android:duration="300"
android:valueFrom="1.2" android:valueFrom="1.2"
android:valueTo="1" android:valueTo="1"
android:valueType="floatType" android:valueType="floatType" />
android:interpolator="@android:interpolator/fast_out_slow_in"/>
<objectAnimator <objectAnimator
android:duration="300"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="scaleY" android:propertyName="scaleY"
android:startOffset="350" android:startOffset="350"
android:duration="300"
android:valueFrom="1.2" android:valueFrom="1.2"
android:valueTo="1" android:valueTo="1"
android:valueType="floatType" android:valueType="floatType" />
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</set> </set>
</aapt:attr> </aapt:attr>
</target> </target>
<target android:name="group_3"> <target android:name="group_3">
<aapt:attr name="android:animation"> <aapt:attr name="android:animation">
<objectAnimator <objectAnimator
android:duration="400"
android:interpolator="@android:anim/overshoot_interpolator"
android:propertyName="translateX" android:propertyName="translateX"
android:startOffset="250" android:startOffset="250"
android:duration="400"
android:valueFrom="-360" android:valueFrom="-360"
android:valueTo="0" android:valueTo="0"
android:valueType="floatType" android:valueType="floatType" />
android:interpolator="@android:anim/overshoot_interpolator"/>
</aapt:attr> </aapt:attr>
</target> </target>
<target android:name="group_4"> <target android:name="group_4">
<aapt:attr name="android:animation"> <aapt:attr name="android:animation">
<set> <set>
<objectAnimator <objectAnimator
android:duration="350"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="scaleX" android:propertyName="scaleX"
android:startOffset="400" android:startOffset="400"
android:duration="350"
android:valueFrom="1.5" android:valueFrom="1.5"
android:valueTo="1" android:valueTo="1"
android:valueType="floatType" android:valueType="floatType" />
android:interpolator="@android:interpolator/fast_out_slow_in"/>
<objectAnimator <objectAnimator
android:duration="350"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="scaleY" android:propertyName="scaleY"
android:startOffset="400" android:startOffset="400"
android:duration="350"
android:valueFrom="1.5" android:valueFrom="1.5"
android:valueTo="1" android:valueTo="1"
android:valueType="floatType" android:valueType="floatType" />
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</set> </set>
</aapt:attr> </aapt:attr>
</target> </target>
<target android:name="path_7"> <target android:name="path_7">
<aapt:attr name="android:animation"> <aapt:attr name="android:animation">
<objectAnimator <objectAnimator
android:duration="550"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="fillAlpha" android:propertyName="fillAlpha"
android:startOffset="350" android:startOffset="350"
android:duration="550"
android:valueFrom="0" android:valueFrom="0"
android:valueTo="1" android:valueTo="1"
android:valueType="floatType" android:valueType="floatType" />
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr> </aapt:attr>
</target> </target>
<target android:name="group_5"> <target android:name="group_5">
<aapt:attr name="android:animation"> <aapt:attr name="android:animation">
<set> <set>
<objectAnimator <objectAnimator
android:duration="550"
android:interpolator="@android:anim/decelerate_interpolator"
android:propertyName="rotation" android:propertyName="rotation"
android:startOffset="350" android:startOffset="350"
android:duration="550"
android:valueFrom="-45" android:valueFrom="-45"
android:valueTo="0" android:valueTo="0"
android:valueType="floatType" android:valueType="floatType" />
android:interpolator="@android:anim/decelerate_interpolator"/>
<objectAnimator <objectAnimator
android:duration="550"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="scaleX" android:propertyName="scaleX"
android:startOffset="350" android:startOffset="350"
android:duration="550"
android:valueFrom="3" android:valueFrom="3"
android:valueTo="1" android:valueTo="1"
android:valueType="floatType" android:valueType="floatType" />
android:interpolator="@android:interpolator/fast_out_slow_in"/>
<objectAnimator <objectAnimator
android:duration="550"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="scaleY" android:propertyName="scaleY"
android:startOffset="350" android:startOffset="350"
android:duration="550"
android:valueFrom="3" android:valueFrom="3"
android:valueTo="1" android:valueTo="1"
android:valueType="floatType" android:valueType="floatType" />
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</set> </set>
</aapt:attr> </aapt:attr>
</target> </target>
<target android:name="group_8"> <target android:name="group_8">
<aapt:attr name="android:animation"> <aapt:attr name="android:animation">
<objectAnimator <objectAnimator
android:duration="350"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="rotation" android:propertyName="rotation"
android:startOffset="100" android:startOffset="100"
android:duration="350"
android:valueFrom="-90" android:valueFrom="-90"
android:valueTo="0" android:valueTo="0"
android:valueType="floatType" android:valueType="floatType" />
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr> </aapt:attr>
</target> </target>
<target android:name="group_9"> <target android:name="group_9">
<aapt:attr name="android:animation"> <aapt:attr name="android:animation">
<set> <set>
<objectAnimator <objectAnimator
android:duration="350"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="rotation" android:propertyName="rotation"
android:startOffset="100" android:startOffset="100"
android:duration="350"
android:valueFrom="-90" android:valueFrom="-90"
android:valueTo="0" android:valueTo="0"
android:valueType="floatType" android:valueType="floatType" />
android:interpolator="@android:interpolator/fast_out_slow_in"/>
<objectAnimator <objectAnimator
android:duration="350"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="scaleX" android:propertyName="scaleX"
android:duration="350"
android:valueFrom="0" android:valueFrom="0"
android:valueTo="1" android:valueTo="1"
android:valueType="floatType" android:valueType="floatType" />
android:interpolator="@android:interpolator/fast_out_slow_in"/>
<objectAnimator <objectAnimator
android:propertyName="scaleY"
android:duration="350" android:duration="350"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="scaleY"
android:valueFrom="0" android:valueFrom="0"
android:valueTo="1" android:valueTo="1"
android:valueType="floatType" android:valueType="floatType" />
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</set> </set>
</aapt:attr> </aapt:attr>
</target> </target>
<target android:name="group_11"> <target android:name="group_11">
<aapt:attr name="android:animation"> <aapt:attr name="android:animation">
<objectAnimator <objectAnimator
android:duration="350"
android:interpolator="@android:interpolator/fast_out_slow_in"
android:propertyName="rotation" android:propertyName="rotation"
android:startOffset="100" android:startOffset="100"
android:duration="350"
android:valueFrom="-90" android:valueFrom="-90"
android:valueTo="0" android:valueTo="0"
android:valueType="floatType" android:valueType="floatType" />
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr> </aapt:attr>
</target> </target>
<target android:name="group_12"> <target android:name="group_12">
<aapt:attr name="android:animation"> <aapt:attr name="android:animation">
<set> <set>
<objectAnimator <objectAnimator
android:duration="550"
android:interpolator="@android:anim/overshoot_interpolator"
android:propertyName="scaleX" android:propertyName="scaleX"
android:duration="550"
android:valueFrom="0" android:valueFrom="0"
android:valueTo="1" android:valueTo="1"
android:valueType="floatType" android:valueType="floatType" />
android:interpolator="@android:anim/overshoot_interpolator"/>
<objectAnimator <objectAnimator
android:propertyName="scaleY"
android:duration="550" android:duration="550"
android:interpolator="@android:anim/overshoot_interpolator"
android:propertyName="scaleY"
android:valueFrom="0" android:valueFrom="0"
android:valueTo="1" android:valueTo="1"
android:valueType="floatType" android:valueType="floatType" />
android:interpolator="@android:anim/overshoot_interpolator"/>
</set> </set>
</aapt:attr> </aapt:attr>
</target> </target>
@@ -336,19 +335,19 @@
<aapt:attr name="android:animation"> <aapt:attr name="android:animation">
<set> <set>
<objectAnimator <objectAnimator
android:duration="550"
android:interpolator="@android:anim/overshoot_interpolator"
android:propertyName="scaleX" android:propertyName="scaleX"
android:duration="550"
android:valueFrom="0" android:valueFrom="0"
android:valueTo="1" android:valueTo="1"
android:valueType="floatType" android:valueType="floatType" />
android:interpolator="@android:anim/overshoot_interpolator"/>
<objectAnimator <objectAnimator
android:propertyName="scaleY"
android:duration="550" android:duration="550"
android:interpolator="@android:anim/overshoot_interpolator"
android:propertyName="scaleY"
android:valueFrom="0" android:valueFrom="0"
android:valueTo="1" android:valueTo="1"
android:valueType="floatType" android:valueType="floatType" />
android:interpolator="@android:anim/overshoot_interpolator"/>
</set> </set>
</aapt:attr> </aapt:attr>
</target> </target>
@@ -356,21 +355,21 @@
<aapt:attr name="android:animation"> <aapt:attr name="android:animation">
<set> <set>
<objectAnimator <objectAnimator
android:duration="200"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:propertyName="rotation" android:propertyName="rotation"
android:startOffset="350" android:startOffset="350"
android:duration="200"
android:valueFrom="5" android:valueFrom="5"
android:valueTo="0" android:valueTo="0"
android:valueType="floatType" android:valueType="floatType" />
android:interpolator="@android:anim/accelerate_decelerate_interpolator"/>
<objectAnimator <objectAnimator
android:duration="100"
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:propertyName="rotation" android:propertyName="rotation"
android:startOffset="250" android:startOffset="250"
android:duration="100"
android:valueFrom="0" android:valueFrom="0"
android:valueTo="5" android:valueTo="5"
android:valueType="floatType" android:valueType="floatType" />
android:interpolator="@android:anim/accelerate_decelerate_interpolator"/>
</set> </set>
</aapt:attr> </aapt:attr>
</target> </target>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">Dantotsu ß</string> <string name="app_name">Dantotsu β</string>
</resources> </resources>

View File

@@ -0,0 +1,9 @@
package ani.dantotsu.connections.crashlytics
class CrashlyticsFactory {
companion object {
fun createCrashlytics(): CrashlyticsInterface {
return CrashlyticsStub()
}
}
}

View File

@@ -0,0 +1,9 @@
package ani.dantotsu.others
import androidx.fragment.app.FragmentActivity
object AppUpdater {
suspend fun check(activity: FragmentActivity, post: Boolean = false) {
//no-op
}
}

View File

@@ -0,0 +1,9 @@
package ani.dantotsu.connections.crashlytics
class CrashlyticsFactory {
companion object {
fun createCrashlytics(): CrashlyticsInterface {
return FirebaseCrashlytics()
}
}
}

View File

@@ -0,0 +1,34 @@
package ani.dantotsu.connections.crashlytics
import android.content.Context
import com.google.firebase.FirebaseApp
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
import com.google.firebase.ktx.app
class FirebaseCrashlytics : CrashlyticsInterface {
override fun initialize(context: Context) {
FirebaseApp.initializeApp(context)
}
override fun logException(e: Throwable) {
FirebaseCrashlytics.getInstance().recordException(e)
}
override fun log(message: String) {
FirebaseCrashlytics.getInstance().log(message)
}
override fun setUserId(id: String) {
Firebase.crashlytics.setUserId(id)
}
override fun setCustomKey(key: String, value: String) {
FirebaseCrashlytics.getInstance().setCustomKey(key, value)
}
override fun setCrashlyticsCollectionEnabled(enabled: Boolean) {
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(enabled)
}
}

View File

@@ -11,12 +11,21 @@ import android.net.Uri
import android.os.Environment import android.os.Environment
import android.widget.TextView import android.widget.TextView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import ani.dantotsu.* import ani.dantotsu.BuildConfig
import io.noties.markwon.Markwon import ani.dantotsu.Mapper
import io.noties.markwon.SoftBreakAddsNewLinePlugin import ani.dantotsu.R
import ani.dantotsu.buildMarkwon
import ani.dantotsu.client
import ani.dantotsu.currContext
import ani.dantotsu.logError
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.snackString
import ani.dantotsu.toast
import ani.dantotsu.tryWithSuspend
import ani.dantotsu.util.Logger
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -24,9 +33,8 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.decodeFromJsonElement import kotlinx.serialization.json.decodeFromJsonElement
import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.Locale
object AppUpdater { object AppUpdater {
suspend fun check(activity: FragmentActivity, post: Boolean = false) { suspend fun check(activity: FragmentActivity, post: Boolean = false) {
@@ -38,9 +46,10 @@ object AppUpdater {
.parsed<JsonArray>().map { .parsed<JsonArray>().map {
Mapper.json.decodeFromJsonElement<GithubResponse>(it) Mapper.json.decodeFromJsonElement<GithubResponse>(it)
} }
val r = res.filter { it.prerelease }.maxByOrNull { val r = res.filter { it.prerelease }.filter { !it.tagName.contains("fdroid") }
it.timeStamp() .maxByOrNull {
} ?: throw Exception("No Pre Release Found") it.timeStamp()
} ?: throw Exception("No Pre Release Found")
val v = r.tagName.substringAfter("v", "") val v = r.tagName.substringAfter("v", "")
(r.body ?: "") to v.ifEmpty { throw Exception("Weird Version : ${r.tagName}") } (r.body ?: "") to v.ifEmpty { throw Exception("Weird Version : ${r.tagName}") }
} else { } else {
@@ -49,8 +58,8 @@ object AppUpdater {
res to res.substringAfter("# ").substringBefore("\n") res to res.substringAfter("# ").substringBefore("\n")
} }
logger("Git Version : $version") Logger.log("Git Version : $version")
val dontShow = loadData("dont_ask_for_update_$version") ?: false val dontShow = PrefManager.getCustomVal("dont_ask_for_update_$version", false)
if (compareVersion(version) && !dontShow && !activity.isDestroyed) activity.runOnUiThread { if (compareVersion(version) && !dontShow && !activity.isDestroyed) activity.runOnUiThread {
CustomBottomDialog.newInstance().apply { CustomBottomDialog.newInstance().apply {
setTitleText( setTitleText(
@@ -60,8 +69,7 @@ object AppUpdater {
) )
addView( addView(
TextView(activity).apply { TextView(activity).apply {
val markWon = Markwon.builder(activity) val markWon = buildMarkwon(activity, false)
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
markWon.setMarkdown(this, md) markWon.setMarkdown(this, md)
} }
) )
@@ -71,7 +79,7 @@ object AppUpdater {
false false
) { isChecked -> ) { isChecked ->
if (isChecked) { if (isChecked) {
saveData("dont_ask_for_update_$version", true) PrefManager.setCustomVal("dont_ask_for_update_$version", true)
} }
} }
setPositiveButton(currContext()!!.getString(R.string.lets_go)) { setPositiveButton(currContext()!!.getString(R.string.lets_go)) {
@@ -160,21 +168,8 @@ object AppUpdater {
DownloadManager.EXTRA_DOWNLOAD_ID, id DownloadManager.EXTRA_DOWNLOAD_ID, id
) ?: id ) ?: id
val query = DownloadManager.Query() downloadManager.getUriForDownloadedFile(downloadId)?.let {
query.setFilterById(downloadId) openApk(this@downloadUpdate, it)
val c = downloadManager.query(query)
if (c.moveToFirst()) {
val columnIndex = c.getColumnIndex(DownloadManager.COLUMN_STATUS)
if (DownloadManager.STATUS_SUCCESSFUL == c
.getInt(columnIndex)
) {
c.getColumnIndex(DownloadManager.COLUMN_MEDIAPROVIDER_URI)
val uri = Uri.parse(
c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
)
openApk(this@downloadUpdate, uri)
}
} }
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
@@ -186,19 +181,14 @@ object AppUpdater {
return true return true
} }
fun openApk(context: Context, uri: Uri) { private fun openApk(context: Context, uri: Uri) {
try { try {
uri.path?.let { uri.path?.let {
val contentUri = FileProvider.getUriForFile(
context,
BuildConfig.APPLICATION_ID + ".provider",
File(it)
)
val installIntent = Intent(Intent.ACTION_VIEW).apply { val installIntent = Intent(Intent.ACTION_VIEW).apply {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true) putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
data = contentUri data = uri
} }
context.startActivity(installIntent) context.startActivity(installIntent)
} }

View File

@@ -10,32 +10,32 @@
android:required="false" /> android:required="false" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission
android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"
tools:ignore="LeanbackUsesWifi" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission <uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" /> android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" <uses-permission
android:maxSdkVersion="32" /> android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" /> <!-- For background jobs -->
<!-- For background jobs -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <!-- For managing extensions -->
<!-- For managing extensions -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- To view extension packages in API 30+ -->
<!-- To view extension packages in API 30+ --> <uses-permission
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" /> tools:ignore="QueryAllPackagesPermission" />
<uses-permission
<uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES" android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
tools:ignore="ProtectedPermissions" /> tools:ignore="ProtectedPermissions" />
<queries> <queries>
@@ -48,6 +48,7 @@
<application <application
android:name=".App" android:name=".App"
android:allowBackup="true" android:allowBackup="true"
android:banner="@mipmap/ic_banner_foreground"
android:icon="${icon_placeholder}" android:icon="${icon_placeholder}"
android:label="@string/app_name" android:label="@string/app_name"
android:largeHeap="true" android:largeHeap="true"
@@ -56,14 +57,28 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Dantotsu" android:theme="@style/Theme.Dantotsu"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:ignore="AllowBackup" tools:ignore="AllowBackup">
android:banner="@drawable/ic_banner_foreground"> <receiver
android:name=".widgets.CurrentlyAiringWidget"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/currently_airing_widget_info" />
</receiver>
<receiver android:name=".notifications.IncognitoNotificationClickReceiver" />
<activity <activity
android:name="ani.dantotsu.media.novel.novelreader.NovelReaderActivity" android:name=".media.novel.novelreader.NovelReaderActivity"
android:configChanges="orientation|screenSize" android:configChanges="orientation|screenSize"
android:exported="true" > android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/epub+zip" /> <data android:mimeType="application/epub+zip" />
@@ -71,13 +86,11 @@
<data android:mimeType="application/vnd.amazon.ebook" /> <data android:mimeType="application/vnd.amazon.ebook" />
<data android:mimeType="application/fb2+zip" /> <data android:mimeType="application/fb2+zip" />
<data android:mimeType="application/vnd.comicbook+zip" /> <data android:mimeType="application/vnd.comicbook+zip" />
<data android:pathPattern=".*\\.epub" /> <data android:pathPattern=".*\\.epub" />
<data android:pathPattern=".*\\.mobi" /> <data android:pathPattern=".*\\.mobi" />
<data android:pathPattern=".*\\.kf8" /> <data android:pathPattern=".*\\.kf8" />
<data android:pathPattern=".*\\.fb2" /> <data android:pathPattern=".*\\.fb2" />
<data android:pathPattern=".*\\.cbz" /> <data android:pathPattern=".*\\.cbz" />
<data android:scheme="content" /> <data android:scheme="content" />
<data android:scheme="file" /> <data android:scheme="file" />
</intent-filter> </intent-filter>
@@ -91,7 +104,26 @@
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
<activity <activity
android:name=".settings.ExtensionsActivity" android:name=".settings.ExtensionsActivity"
android:windowSoftInputMode="adjustResize|stateHidden"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
<activity
android:name=".profile.ProfileActivity"
android:windowSoftInputMode="adjustResize|stateHidden"
android:parentActivityName=".MainActivity" />
<activity
android:name=".profile.FollowActivity"
android:windowSoftInputMode="adjustResize|stateHidden"
android:parentActivityName=".MainActivity" />
<activity
android:name=".profile.activity.FeedActivity"
android:label="Inbox Activity"
android:parentActivityName=".MainActivity" >
</activity>
<activity
android:name=".profile.activity.NotificationActivity"
android:label="Inbox Activity"
android:parentActivityName=".MainActivity" >
</activity>
<activity <activity
android:name=".others.imagesearch.ImageSearchActivity" android:name=".others.imagesearch.ImageSearchActivity"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
@@ -103,9 +135,11 @@
<activity <activity
android:name=".media.CalendarActivity" android:name=".media.CalendarActivity"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
<activity android:name="ani.dantotsu.media.user.ListActivity" /> <activity android:name=".media.user.ListActivity" />
<activity android:name=".profile.SingleStatActivity"
android:parentActivityName=".profile.ProfileActivity"/>
<activity <activity
android:name="ani.dantotsu.media.manga.mangareader.MangaReaderActivity" android:name=".media.manga.mangareader.MangaReaderActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
android:label="@string/manga" android:label="@string/manga"
@@ -114,11 +148,12 @@
<activity <activity
android:name=".media.MediaDetailsActivity" android:name=".media.MediaDetailsActivity"
android:parentActivityName=".MainActivity" android:parentActivityName=".MainActivity"
android:theme="@style/Theme.Dantotsu.NeverCutout" /> android:theme="@style/Theme.Dantotsu.NeverCutout"
android:windowSoftInputMode="adjustResize|stateHidden"/>
<activity android:name=".media.CharacterDetailsActivity" /> <activity android:name=".media.CharacterDetailsActivity" />
<activity android:name=".home.NoInternet" /> <activity android:name=".home.NoInternet" />
<activity <activity
android:name="ani.dantotsu.media.anime.ExoplayerView" android:name=".media.anime.ExoplayerView"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
@@ -127,7 +162,7 @@
android:supportsPictureInPicture="true" android:supportsPictureInPicture="true"
tools:targetApi="n" /> tools:targetApi="n" />
<activity <activity
android:name="ani.dantotsu.connections.anilist.Login" android:name=".connections.anilist.Login"
android:configChanges="orientation|screenSize|layoutDirection" android:configChanges="orientation|screenSize|layoutDirection"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
@@ -144,7 +179,7 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name="ani.dantotsu.connections.mal.Login" android:name=".connections.mal.Login"
android:configChanges="orientation|screenSize|layoutDirection" android:configChanges="orientation|screenSize|layoutDirection"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
@@ -160,8 +195,8 @@
android:scheme="dantotsu" /> android:scheme="dantotsu" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
<activity android:name="ani.dantotsu.connections.discord.Login" android:name=".connections.discord.Login"
android:configChanges="orientation|screenSize|layoutDirection" android:configChanges="orientation|screenSize|layoutDirection"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
@@ -172,15 +207,33 @@
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="dantotsu"/> <data android:scheme="dantotsu" />
<data android:scheme="http" /> <data android:scheme="http" />
<data android:scheme="https" /> <data android:scheme="https" />
<data android:host="discord.dantotsu.com"/> <data android:host="discord.dantotsu.com" />
</intent-filter>
</activity>
<activity
android:name=".others.webview.CookieCatcher"
android:configChanges="orientation|screenSize|layoutDirection"
android:excludeFromRecents="true"
android:exported="true"
android:launchMode="singleTask">
<intent-filter android:label="Discord Login for Dantotsu">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="dantotsu" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="discord.dantotsu.com" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name="ani.dantotsu.connections.anilist.UrlMedia" android:name=".connections.anilist.UrlMedia"
android:configChanges="orientation|screenSize|layoutDirection" android:configChanges="orientation|screenSize|layoutDirection"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
@@ -209,6 +262,17 @@
<data android:host="myanimelist.net" /> <data android:host="myanimelist.net" />
<data android:pathPrefix="/anime" /> <data android:pathPrefix="/anime" />
</intent-filter> </intent-filter>
<intent-filter android:label="@string/view_profile_in_dantotsu">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="anilist.co" />
<data android:pathPrefix="/user" />
</intent-filter>
</activity> </activity>
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
@@ -216,32 +280,49 @@
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.Main" /> <action android:name="android.intent.action.Main" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="content" />
<data android:mimeType="*/*" />
<data android:pathPattern=".*\\.ani" />
<data android:pathPattern=".*\\.sani" />
<data android:host="*" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".download.DownloadContainerActivity" />
<activity <activity
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity" android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar" android:exported="false"
android:exported="false" /> android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity <activity
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallActivity" android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar" android:exported="false"
android:exported="false" /> android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<receiver <receiver android:name=".notifications.AlarmPermissionStateReceiver"
android:name=".subcriptions.AlarmReceiver" android:exported="true">
<intent-filter>
<action android:name="android.app.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED" />
</intent-filter>
</receiver>
<receiver android:name=".notifications.BootCompletedReceiver"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="Aani.dantotsu.ACTION_ALARM"/>
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name=".notifications.anilist.AnilistNotificationReceiver"/>
<receiver android:name=".notifications.comment.CommentNotificationReceiver"/>
<receiver android:name=".notifications.subscription.SubscriptionNotificationReceiver"/>
<meta-data <meta-data
android:name="preloaded_fonts" android:name="preloaded_fonts"
@@ -258,32 +339,52 @@
android:resource="@xml/provider_paths" /> android:resource="@xml/provider_paths" />
</provider> </provider>
<service android:name=".download.video.MyDownloadService" <service
android:exported="false"> android:name=".widgets.CurrentlyAiringRemoteViewsService"
android:exported="true"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<service
android:name=".download.video.ExoplayerDownloadService"
android:exported="false"
android:foregroundServiceType="dataSync">
<intent-filter> <intent-filter>
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART"/> <action android:name="androidx.media3.exoplayer.downloadService.action.RESTART" />
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
</service> </service>
<service android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService" <service
android:foregroundServiceType="dataSync" android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService"
android:exported="false" />
<service android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
android:foregroundServiceType="dataSync"
android:exported="false" />
<service android:name=".download.manga.MangaDownloaderService"
android:exported="false" android:exported="false"
android:foregroundServiceType="dataSync" /> android:foregroundServiceType="dataSync" />
<service
<service android:name=".download.novel.NovelDownloaderService" android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
android:exported="false" android:exported="false"
android:foregroundServiceType="dataSync" /> android:foregroundServiceType="dataSync" />
<service
<service android:name=".connections.discord.DiscordService" android:name=".download.manga.MangaDownloaderService"
android:exported="false" android:exported="false"
android:foregroundServiceType="dataSync" /> android:foregroundServiceType="dataSync" />
<service
android:name=".download.novel.NovelDownloaderService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service
android:name=".download.anime.AnimeDownloaderService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service
android:name=".connections.discord.DiscordService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service
android:name="androidx.media3.exoplayer.scheduler.PlatformScheduler$PlatformSchedulerService"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" />
<meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="androidx.media3.cast.DefaultCastOptionsProvider" />
</application> </application>
</manifest> </manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -8,14 +8,20 @@ import androidx.multidex.MultiDex
import androidx.multidex.MultiDexApplication import androidx.multidex.MultiDexApplication
import ani.dantotsu.aniyomi.anime.custom.AppModule import ani.dantotsu.aniyomi.anime.custom.AppModule
import ani.dantotsu.aniyomi.anime.custom.PreferenceModule import ani.dantotsu.aniyomi.anime.custom.PreferenceModule
import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.notifications.TaskScheduler
import ani.dantotsu.others.DisabledReports import ani.dantotsu.others.DisabledReports
import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.MangaSources import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.parsers.NovelSources import ani.dantotsu.parsers.NovelSources
import ani.dantotsu.parsers.novel.NovelExtensionManager import ani.dantotsu.parsers.novel.NovelExtensionManager
import ani.dantotsu.settings.SettingsActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.util.FinalExceptionHandler
import ani.dantotsu.util.Logger
import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColors
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
@@ -26,7 +32,6 @@ import kotlinx.coroutines.launch
import logcat.AndroidLogcatLogger import logcat.AndroidLogcatLogger
import logcat.LogPriority import logcat.LogPriority
import logcat.LogcatLogger import logcat.LogcatLogger
import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@@ -49,18 +54,37 @@ class App : MultiDexApplication() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
val sharedPreferences = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
val useMaterialYou = sharedPreferences.getBoolean("use_material_you", false) PrefManager.init(this)
Injekt.importModule(AppModule(this))
Injekt.importModule(PreferenceModule(this))
val crashlytics = Injekt.get<CrashlyticsInterface>()
crashlytics.initialize(this)
val useMaterialYou: Boolean = PrefManager.getVal(PrefName.UseMaterialYou)
if (useMaterialYou) { if (useMaterialYou) {
DynamicColors.applyToActivitiesIfAvailable(this) DynamicColors.applyToActivitiesIfAvailable(this)
//TODO: HarmonizedColors
} }
registerActivityLifecycleCallbacks(mFTActivityLifecycleCallbacks) registerActivityLifecycleCallbacks(mFTActivityLifecycleCallbacks)
Firebase.crashlytics.setCrashlyticsCollectionEnabled(!DisabledReports) crashlytics.setCrashlyticsCollectionEnabled(!DisabledReports)
(PrefManager.getVal(PrefName.SharedUserID) as Boolean).let {
if (!it) return@let
val dUsername = PrefManager.getVal(PrefName.DiscordUserName, null as String?)
val aUsername = PrefManager.getVal(PrefName.AnilistUserName, null as String?)
if (dUsername != null) {
crashlytics.setCustomKey("dUsername", dUsername)
}
if (aUsername != null) {
crashlytics.setCustomKey("aUsername", aUsername)
}
}
crashlytics.setCustomKey("device Info", SettingsActivity.getDeviceInfo())
Injekt.importModule(AppModule(this)) Logger.init(this)
Injekt.importModule(PreferenceModule(this)) Thread.setDefaultUncaughtExceptionHandler(FinalExceptionHandler())
Logger.log("App: Logging started")
initializeNetwork(baseContext) initializeNetwork(baseContext)
@@ -76,30 +100,36 @@ class App : MultiDexApplication() {
val animeScope = CoroutineScope(Dispatchers.Default) val animeScope = CoroutineScope(Dispatchers.Default)
animeScope.launch { animeScope.launch {
animeExtensionManager.findAvailableExtensions() animeExtensionManager.findAvailableExtensions()
logger("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}") Logger.log("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
AnimeSources.init(animeExtensionManager.installedExtensionsFlow) AnimeSources.init(animeExtensionManager.installedExtensionsFlow)
} }
val mangaScope = CoroutineScope(Dispatchers.Default) val mangaScope = CoroutineScope(Dispatchers.Default)
mangaScope.launch { mangaScope.launch {
mangaExtensionManager.findAvailableExtensions() mangaExtensionManager.findAvailableExtensions()
logger("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}") Logger.log("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
MangaSources.init(mangaExtensionManager.installedExtensionsFlow) MangaSources.init(mangaExtensionManager.installedExtensionsFlow)
} }
val novelScope = CoroutineScope(Dispatchers.Default) val novelScope = CoroutineScope(Dispatchers.Default)
novelScope.launch { novelScope.launch {
novelExtensionManager.findAvailableExtensions() novelExtensionManager.findAvailableExtensions()
logger("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}") Logger.log("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
NovelSources.init(novelExtensionManager.installedExtensionsFlow) NovelSources.init(novelExtensionManager.installedExtensionsFlow)
} }
val commentsScope = CoroutineScope(Dispatchers.Default)
commentsScope.launch {
CommentsAPI.fetchAuthToken()
}
val useAlarmManager = PrefManager.getVal<Boolean>(PrefName.UseAlarmManager)
TaskScheduler.create(this, useAlarmManager).scheduleAllTasks(this)
} }
private fun setupNotificationChannels() { private fun setupNotificationChannels() {
try { try {
Notifications.createChannels(this) Notifications.createChannels(this)
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to modify notification channels" } Logger.log("Failed to modify notification channels")
Logger.log(e)
} }
} }

View File

@@ -1,38 +1,88 @@
package ani.dantotsu package ani.dantotsu
import android.Manifest
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.DatePickerDialog import android.app.DatePickerDialog
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.ActivityNotFoundException
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Resources.getSystem import android.content.res.Resources.getSystem
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.Drawable
import android.media.MediaScannerConnection import android.media.MediaScannerConnection
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkCapabilities.* import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
import android.net.NetworkCapabilities.TRANSPORT_LOWPAN
import android.net.NetworkCapabilities.TRANSPORT_USB
import android.net.NetworkCapabilities.TRANSPORT_VPN
import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE
import android.net.Uri import android.net.Uri
import android.os.* import android.os.Build
import android.os.Bundle
import android.os.CountDownTimer
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.os.PowerManager
import android.os.SystemClock
import android.provider.Settings import android.provider.Settings
import android.telephony.TelephonyManager import android.telephony.TelephonyManager
import android.text.InputFilter import android.text.InputFilter
import android.text.Spanned import android.text.Spanned
import android.util.AttributeSet import android.util.AttributeSet
import android.view.* import android.util.TypedValue
import android.view.GestureDetector
import android.view.Gravity
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewAnimationUtils
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.animation.* import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.* import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.animation.AnimationSet
import android.view.animation.OvershootInterpolator
import android.view.animation.ScaleAnimation
import android.view.animation.TranslateAnimation
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.DatePicker
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getSystemService import androidx.core.content.ContextCompat.getSystemService
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.math.MathUtils.clamp import androidx.core.math.MathUtils.clamp
import androidx.core.view.* import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@@ -40,26 +90,67 @@ import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.BuildConfig.APPLICATION_ID import ani.dantotsu.BuildConfig.APPLICATION_ID
import ani.dantotsu.connections.anilist.Genre import ani.dantotsu.connections.anilist.Genre
import ani.dantotsu.connections.anilist.api.FuzzyDate import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.databinding.ItemCountDownBinding import ani.dantotsu.databinding.ItemCountDownBinding
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.notifications.IncognitoNotificationClickReceiver
import ani.dantotsu.others.SpoilerPlugin
import ani.dantotsu.parsers.ShowResponse import ani.dantotsu.parsers.ShowResponse
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt
import ani.dantotsu.util.Logger
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.RequestManager
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade
import com.bumptech.glide.load.resource.gif.GifDrawable
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.Target
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.internal.ViewUtils import com.google.android.material.internal.ViewUtils
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.* import eu.kanade.tachiyomi.data.notification.Notifications
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonConfiguration
import io.noties.markwon.SoftBreakAddsNewLinePlugin
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin
import io.noties.markwon.ext.tables.TablePlugin
import io.noties.markwon.ext.tasklist.TaskListPlugin
import io.noties.markwon.html.HtmlPlugin
import io.noties.markwon.html.TagHandlerNoOp
import io.noties.markwon.image.AsyncDrawable
import io.noties.markwon.image.glide.GlideImagesPlugin
import jp.wasabeef.glide.transformations.BlurTransformation
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import nl.joery.animatedbottombar.AnimatedBottomBar import nl.joery.animatedbottombar.AnimatedBottomBar
import java.io.* import uy.kohesive.injekt.Injekt
import java.lang.Runnable import uy.kohesive.injekt.api.get
import java.io.File
import java.io.FileOutputStream
import java.io.OutputStream
import java.lang.reflect.Field import java.lang.reflect.Field
import java.util.* import java.util.Calendar
import kotlin.math.* import java.util.TimeZone
import java.util.Timer
import java.util.TimerTask
import kotlin.collections.set
import kotlin.math.log2
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
var statusBarHeight = 0 var statusBarHeight = 0
@@ -91,68 +182,31 @@ fun currActivity(): Activity? {
var loadMedia: Int? = null var loadMedia: Int? = null
var loadIsMAL = false var loadIsMAL = false
fun logger(e: Any?, print: Boolean = true) {
if (print)
println(e)
}
fun saveData(fileName: String, data: Any?, context: Context? = null) {
tryWith {
val a = context ?: currContext()
if (a != null) {
val fos: FileOutputStream = a.openFileOutput(fileName, Context.MODE_PRIVATE)
val os = ObjectOutputStream(fos)
os.writeObject(data)
os.close()
fos.close()
}
}
}
@Suppress("UNCHECKED_CAST")
fun <T> loadData(fileName: String, context: Context? = null, toast: Boolean = true): T? {
val a = context ?: currContext()
try {
if (a?.fileList() != null)
if (fileName in a.fileList()) {
val fileIS: FileInputStream = a.openFileInput(fileName)
val objIS = ObjectInputStream(fileIS)
val data = objIS.readObject() as T
objIS.close()
fileIS.close()
return data
}
} catch (e: Exception) {
if (toast) snackString(a?.getString(R.string.error_loading_data, fileName))
e.printStackTrace()
}
return null
}
fun initActivity(a: Activity) { fun initActivity(a: Activity) {
val window = a.window val window = a.window
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
val uiSettings = loadData<UserInterfaceSettings>("ui_settings", toast = false) val darkMode = PrefManager.getVal<Int>(PrefName.DarkMode)
?: UserInterfaceSettings().apply { val immersiveMode: Boolean = PrefManager.getVal(PrefName.ImmersiveMode)
saveData("ui_settings", this) darkMode.apply {
}
uiSettings.darkMode.apply {
AppCompatDelegate.setDefaultNightMode( AppCompatDelegate.setDefaultNightMode(
when (this) { when (this) {
true -> AppCompatDelegate.MODE_NIGHT_YES 2 -> AppCompatDelegate.MODE_NIGHT_YES
false -> AppCompatDelegate.MODE_NIGHT_NO 1 -> AppCompatDelegate.MODE_NIGHT_NO
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
} }
) )
} }
if (uiSettings.immersiveMode) { if (immersiveMode) {
if (navBarHeight == 0) { if (navBarHeight == 0) {
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content)) ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
?.apply { ?.apply {
navBarHeight = this.getInsets(WindowInsetsCompat.Type.systemBars()).bottom navBarHeight = this.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
} }
} }
a.hideStatusBar() WindowInsetsControllerCompat(
window,
window.decorView
).hide(WindowInsetsCompat.Type.statusBars())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && statusBarHeight == 0 && a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && statusBarHeight == 0 && a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
window.decorView.rootWindowInsets?.displayCutout?.apply { window.decorView.rootWindowInsets?.displayCutout?.apply {
if (boundingRects.size > 0) { if (boundingRects.size > 0) {
@@ -165,38 +219,72 @@ fun initActivity(a: Activity) {
val windowInsets = val windowInsets =
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content)) ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
if (windowInsets != null) { if (windowInsets != null) {
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) statusBarHeight = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).top
statusBarHeight = insets.top navBarHeight =
navBarHeight = insets.bottom windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
} }
} }
if (a !is MainActivity) a.setNavigationTheme()
} }
@Suppress("DEPRECATION")
fun Activity.hideSystemBars() { fun Activity.hideSystemBars() {
window.decorView.systemUiVisibility = ( WindowInsetsControllerCompat(window, window.decorView).let { controller ->
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY controller.systemBarsBehavior =
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
or View.SYSTEM_UI_FLAG_FULLSCREEN controller.hide(WindowInsetsCompat.Type.systemBars())
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION }
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
)
} }
@Suppress("DEPRECATION") fun Activity.hideSystemBarsExtendView() {
fun Activity.hideStatusBar() { WindowCompat.setDecorFitsSystemWindows(window, false)
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN) hideSystemBars()
}
fun Activity.showSystemBars() {
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
controller.show(WindowInsetsCompat.Type.systemBars())
}
}
fun Activity.showSystemBarsRetractView() {
WindowCompat.setDecorFitsSystemWindows(window, true)
showSystemBars()
}
fun Activity.setNavigationTheme() {
val tv = TypedValue()
theme.resolveAttribute(android.R.attr.colorBackground, tv, true)
if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && tv.isColorType)
|| (tv.type >= TypedValue.TYPE_FIRST_COLOR_INT && tv.type <= TypedValue.TYPE_LAST_COLOR_INT)
) {
window.navigationBarColor = tv.data
}
} }
open class BottomSheetDialogFragment : BottomSheetDialogFragment() { open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
val window = dialog?.window dialog?.window?.let { window ->
val decorView: View = window?.decorView ?: return WindowCompat.setDecorFitsSystemWindows(window, false)
decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN val immersiveMode: Boolean = PrefManager.getVal(PrefName.ImmersiveMode)
if (this.resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) { if (immersiveMode) {
val behavior = BottomSheetBehavior.from(requireView().parent as View) WindowInsetsControllerCompat(
behavior.state = BottomSheetBehavior.STATE_EXPANDED window, window.decorView
).hide(WindowInsetsCompat.Type.statusBars())
}
if (this.resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) {
val behavior = BottomSheetBehavior.from(requireView().parent as View)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
val typedValue = TypedValue()
val theme = requireContext().theme
theme.resolveAttribute(
com.google.android.material.R.attr.colorSurface,
typedValue,
true
)
window.navigationBarColor = typedValue.data
} }
} }
@@ -211,21 +299,35 @@ fun isOnline(context: Context): Boolean {
val connectivityManager = val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
return tryWith { return tryWith {
val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return@tryWith if (cap != null) { val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
when { return@tryWith if (cap != null) {
cap.hasTransport(TRANSPORT_BLUETOOTH) || when {
cap.hasTransport(TRANSPORT_CELLULAR) || cap.hasTransport(TRANSPORT_BLUETOOTH) ||
cap.hasTransport(TRANSPORT_ETHERNET) || cap.hasTransport(TRANSPORT_CELLULAR) ||
cap.hasTransport(TRANSPORT_LOWPAN) || cap.hasTransport(TRANSPORT_ETHERNET) ||
cap.hasTransport(TRANSPORT_USB) || cap.hasTransport(TRANSPORT_LOWPAN) ||
cap.hasTransport(TRANSPORT_VPN) || cap.hasTransport(TRANSPORT_USB) ||
cap.hasTransport(TRANSPORT_WIFI) || cap.hasTransport(TRANSPORT_VPN) ||
cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true cap.hasTransport(TRANSPORT_WIFI) ||
cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true
else -> false else -> false
} }
} else false } else false
} else {
@Suppress("DEPRECATION")
return@tryWith connectivityManager.activeNetworkInfo?.run {
type == ConnectivityManager.TYPE_BLUETOOTH ||
type == ConnectivityManager.TYPE_ETHERNET ||
type == ConnectivityManager.TYPE_MOBILE ||
type == ConnectivityManager.TYPE_MOBILE_DUN ||
type == ConnectivityManager.TYPE_MOBILE_HIPRI ||
type == ConnectivityManager.TYPE_WIFI ||
type == ConnectivityManager.TYPE_WIMAX ||
type == ConnectivityManager.TYPE_VPN
} ?: false
}
} ?: false } ?: false
} }
@@ -287,7 +389,7 @@ class InputFilterMinMax(
val input = (dest.toString() + source.toString()).toDouble() val input = (dest.toString() + source.toString()).toDouble()
if (isInRange(min, max, input)) return null if (isInRange(min, max, input)) return null
} catch (nfe: NumberFormatException) { } catch (nfe: NumberFormatException) {
logger(nfe.stackTraceToString()) Logger.log(nfe)
} }
return "" return ""
} }
@@ -305,20 +407,20 @@ class InputFilterMinMax(
} }
class ZoomOutPageTransformer(private val uiSettings: UserInterfaceSettings) : class ZoomOutPageTransformer() :
ViewPager2.PageTransformer { ViewPager2.PageTransformer {
override fun transformPage(view: View, position: Float) { override fun transformPage(view: View, position: Float) {
if (position == 0.0f && uiSettings.layoutAnimations) { if (position == 0.0f && PrefManager.getVal(PrefName.LayoutAnimations)) {
setAnimation( setAnimation(
view.context, view.context,
view, view,
uiSettings,
300, 300,
floatArrayOf(1.3f, 1f, 1.3f, 1f), floatArrayOf(1.3f, 1f, 1.3f, 1f),
0.5f to 0f 0.5f to 0f
) )
ObjectAnimator.ofFloat(view, "alpha", 0f, 1.0f) ObjectAnimator.ofFloat(view, "alpha", 0f, 1.0f)
.setDuration((200 * uiSettings.animationSpeed).toLong()).start() .setDuration((200 * (PrefManager.getVal(PrefName.AnimationSpeed) as Float)).toLong())
.start()
} }
} }
} }
@@ -326,12 +428,11 @@ class ZoomOutPageTransformer(private val uiSettings: UserInterfaceSettings) :
fun setAnimation( fun setAnimation(
context: Context, context: Context,
viewToAnimate: View, viewToAnimate: View,
uiSettings: UserInterfaceSettings,
duration: Long = 150, duration: Long = 150,
list: FloatArray = floatArrayOf(0.0f, 1.0f, 0.0f, 1.0f), list: FloatArray = floatArrayOf(0.0f, 1.0f, 0.0f, 1.0f),
pivot: Pair<Float, Float> = 0.5f to 0.5f pivot: Pair<Float, Float> = 0.5f to 0.5f
) { ) {
if (uiSettings.layoutAnimations) { if (PrefManager.getVal(PrefName.LayoutAnimations)) {
val anim = ScaleAnimation( val anim = ScaleAnimation(
list[0], list[0],
list[1], list[1],
@@ -342,7 +443,7 @@ fun setAnimation(
Animation.RELATIVE_TO_SELF, Animation.RELATIVE_TO_SELF,
pivot.second pivot.second
) )
anim.duration = (duration * uiSettings.animationSpeed).toLong() anim.duration = (duration * (PrefManager.getVal(PrefName.AnimationSpeed) as Float)).toLong()
anim.setInterpolator(context, R.anim.over_shoot) anim.setInterpolator(context, R.anim.over_shoot)
viewToAnimate.startAnimation(anim) viewToAnimate.startAnimation(anim)
} }
@@ -459,6 +560,7 @@ fun ImageView.loadImage(url: String?, size: Int = 0) {
} }
fun ImageView.loadImage(file: FileUrl?, size: Int = 0) { fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
file?.url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { file?.url ?: "" }
if (file?.url?.isNotEmpty() == true) { if (file?.url?.isNotEmpty() == true) {
tryWith { tryWith {
val glideUrl = GlideUrl(file.url) { file.headers } val glideUrl = GlideUrl(file.url) { file.headers }
@@ -589,24 +691,143 @@ fun View.circularReveal(ex: Int, ey: Int, subX: Boolean, time: Long) {
} }
fun openLinkInBrowser(link: String?) { fun openLinkInBrowser(link: String?) {
tryWith { link?.let {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link)) try {
currContext()?.startActivity(intent) val emptyBrowserIntent = Intent(Intent.ACTION_VIEW).apply {
addCategory(Intent.CATEGORY_BROWSABLE)
data = Uri.fromParts("http", "", null)
}
val sendIntent = Intent().apply {
action = Intent.ACTION_VIEW
addCategory(Intent.CATEGORY_BROWSABLE)
data = Uri.parse(link)
selector = emptyBrowserIntent
}
currContext()!!.startActivity(sendIntent)
} catch (e: ActivityNotFoundException) {
snackString("No browser found")
} catch (e: Exception) {
Logger.log(e)
}
} }
} }
fun saveImageToDownloads(title: String, bitmap: Bitmap, context: Context) { fun saveImageToDownloads(title: String, bitmap: Bitmap, context: Activity) {
FileProvider.getUriForFile( FileProvider.getUriForFile(
context, context,
"$APPLICATION_ID.provider", "$APPLICATION_ID.provider",
saveImage( saveImage(
bitmap, bitmap,
Environment.getExternalStorageDirectory().absolutePath + "/" + Environment.DIRECTORY_DOWNLOADS, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath,
title title
) ?: return ) ?: return
) )
} }
fun savePrefsToDownloads(
title: String,
serialized: String,
context: Activity,
password: CharArray? = null
) {
FileProvider.getUriForFile(
context,
"$APPLICATION_ID.provider",
if (password != null) {
savePrefs(
serialized,
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath,
title,
context,
password
) ?: return
} else {
savePrefs(
serialized,
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath,
title,
context
) ?: return
}
)
}
fun savePrefs(serialized: String, path: String, title: String, context: Context): File? {
var file = File(path, "$title.ani")
var counter = 1
while (file.exists()) {
file = File(path, "${title}_${counter}.ani")
counter++
}
return try {
file.writeText(serialized)
scanFile(file.absolutePath, context)
toast(String.format(context.getString(R.string.saved_to_path, file.absolutePath)))
file
} catch (e: Exception) {
snackString("Failed to save settings: ${e.localizedMessage}")
null
}
}
fun savePrefs(
serialized: String,
path: String,
title: String,
context: Context,
password: CharArray
): File? {
var file = File(path, "$title.sani")
var counter = 1
while (file.exists()) {
file = File(path, "${title}_${counter}.sani")
counter++
}
val salt = generateSalt()
return try {
val encryptedData = PreferenceKeystore.encryptWithPassword(password, serialized, salt)
// Combine salt and encrypted data
val dataToSave = salt + encryptedData
file.writeBytes(dataToSave)
scanFile(file.absolutePath, context)
toast(String.format(context.getString(R.string.saved_to_path, file.absolutePath)))
file
} catch (e: Exception) {
snackString("Failed to save settings: ${e.localizedMessage}")
null
}
}
fun downloadsPermission(activity: AppCompatActivity): Boolean {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) return true
val permissions = arrayOf(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
val requiredPermissions = permissions.filter {
ContextCompat.checkSelfPermission(activity, it) != PackageManager.PERMISSION_GRANTED
}.toTypedArray()
return if (requiredPermissions.isNotEmpty()) {
ActivityCompat.requestPermissions(
activity,
requiredPermissions,
DOWNLOADS_PERMISSION_REQUEST_CODE
)
false
} else {
true
}
}
private const val DOWNLOADS_PERMISSION_REQUEST_CODE = 100
fun shareImage(title: String, bitmap: Bitmap, context: Context) { fun shareImage(title: String, bitmap: Bitmap, context: Context) {
val contentUri = FileProvider.getUriForFile( val contentUri = FileProvider.getUriForFile(
@@ -624,19 +845,22 @@ fun shareImage(title: String, bitmap: Bitmap, context: Context) {
fun saveImage(image: Bitmap, path: String, imageFileName: String): File? { fun saveImage(image: Bitmap, path: String, imageFileName: String): File? {
val imageFile = File(path, "$imageFileName.png") val imageFile = File(path, "$imageFileName.png")
return tryWith { return try {
val fOut: OutputStream = FileOutputStream(imageFile) val fOut: OutputStream = FileOutputStream(imageFile)
image.compress(Bitmap.CompressFormat.PNG, 0, fOut) image.compress(Bitmap.CompressFormat.PNG, 0, fOut)
fOut.close() fOut.close()
scanFile(imageFile.absolutePath, currContext()!!) scanFile(imageFile.absolutePath, currContext()!!)
toast(String.format(currContext()!!.getString(R.string.saved_to_path, path))) toast(String.format(currContext()!!.getString(R.string.saved_to_path, path)))
imageFile imageFile
} catch (e: Exception) {
snackString("Failed to save image: ${e.localizedMessage}")
null
} }
} }
private fun scanFile(path: String, context: Context) { private fun scanFile(path: String, context: Context) {
MediaScannerConnection.scanFile(context, arrayOf(path), null) { p, _ -> MediaScannerConnection.scanFile(context, arrayOf(path), null) { p, _ ->
logger("Finished scanning $p") Logger.log("Finished scanning $p")
} }
} }
@@ -668,12 +892,14 @@ fun copyToClipboard(string: String, toast: Boolean = true) {
val clipboard = getSystemService(activity, ClipboardManager::class.java) val clipboard = getSystemService(activity, ClipboardManager::class.java)
val clip = ClipData.newPlainText("label", string) val clip = ClipData.newPlainText("label", string)
clipboard?.setPrimaryClip(clip) clipboard?.setPrimaryClip(clip)
if (toast) snackString(activity.getString(R.string.copied_text, string)) if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
if (toast) snackString(activity.getString(R.string.copied_text, string))
}
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun countDown(media: Media, view: ViewGroup) { fun countDown(media: Media, view: ViewGroup) {
if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null && (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 7.toLong()) { if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null && (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 28.toLong()) {
val v = ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false) val v = ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
view.addView(v.root, 0) view.addView(v.root, 0)
v.mediaCountdownText.text = v.mediaCountdownText.text =
@@ -720,10 +946,11 @@ fun MutableMap<String, Genre>.checkGenreTime(genre: String): Boolean {
return true return true
} }
fun setSlideIn(uiSettings: UserInterfaceSettings) = AnimationSet(false).apply { fun setSlideIn() = AnimationSet(false).apply {
if (uiSettings.layoutAnimations) { if (PrefManager.getVal(PrefName.LayoutAnimations)) {
var animation: Animation = AlphaAnimation(0.0f, 1.0f) var animation: Animation = AlphaAnimation(0.0f, 1.0f)
animation.duration = (500 * uiSettings.animationSpeed).toLong() val animationSpeed: Float = PrefManager.getVal(PrefName.AnimationSpeed)
animation.duration = (500 * animationSpeed).toLong()
animation.interpolator = AccelerateDecelerateInterpolator() animation.interpolator = AccelerateDecelerateInterpolator()
addAnimation(animation) addAnimation(animation)
@@ -734,16 +961,17 @@ fun setSlideIn(uiSettings: UserInterfaceSettings) = AnimationSet(false).apply {
Animation.RELATIVE_TO_SELF, 0f Animation.RELATIVE_TO_SELF, 0f
) )
animation.duration = (750 * uiSettings.animationSpeed).toLong() animation.duration = (750 * animationSpeed).toLong()
animation.interpolator = OvershootInterpolator(1.1f) animation.interpolator = OvershootInterpolator(1.1f)
addAnimation(animation) addAnimation(animation)
} }
} }
fun setSlideUp(uiSettings: UserInterfaceSettings) = AnimationSet(false).apply { fun setSlideUp() = AnimationSet(false).apply {
if (uiSettings.layoutAnimations) { if (PrefManager.getVal(PrefName.LayoutAnimations)) {
var animation: Animation = AlphaAnimation(0.0f, 1.0f) var animation: Animation = AlphaAnimation(0.0f, 1.0f)
animation.duration = (500 * uiSettings.animationSpeed).toLong() val animationSpeed: Float = PrefManager.getVal(PrefName.AnimationSpeed)
animation.duration = (500 * animationSpeed).toLong()
animation.interpolator = AccelerateDecelerateInterpolator() animation.interpolator = AccelerateDecelerateInterpolator()
addAnimation(animation) addAnimation(animation)
@@ -754,7 +982,7 @@ fun setSlideUp(uiSettings: UserInterfaceSettings) = AnimationSet(false).apply {
Animation.RELATIVE_TO_SELF, 0f Animation.RELATIVE_TO_SELF, 0f
) )
animation.duration = (750 * uiSettings.animationSpeed).toLong() animation.duration = (750 * animationSpeed).toLong()
animation.interpolator = OvershootInterpolator(1.1f) animation.interpolator = OvershootInterpolator(1.1f)
addAnimation(animation) addAnimation(animation)
} }
@@ -774,7 +1002,7 @@ class EmptyAdapter(private val count: Int) : RecyclerView.Adapter<RecyclerView.V
fun toast(string: String?) { fun toast(string: String?) {
if (string != null) { if (string != null) {
logger(string) Logger.log(string)
MainScope().launch { MainScope().launch {
Toast.makeText(currActivity()?.application ?: return@launch, string, Toast.LENGTH_SHORT) Toast.makeText(currActivity()?.application ?: return@launch, string, Toast.LENGTH_SHORT)
.show() .show()
@@ -782,37 +1010,44 @@ fun toast(string: String?) {
} }
} }
fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null) { fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null): Snackbar? {
if (s != null) { try { //I have no idea why this sometimes crashes for some people...
(activity ?: currActivity())?.apply { if (s != null) {
runOnUiThread { (activity ?: currActivity())?.apply {
val snackBar = Snackbar.make( val snackBar = Snackbar.make(
window.decorView.findViewById(android.R.id.content), window.decorView.findViewById(android.R.id.content),
s, s,
Snackbar.LENGTH_SHORT Snackbar.LENGTH_SHORT
) )
snackBar.view.apply { runOnUiThread {
updateLayoutParams<FrameLayout.LayoutParams> { snackBar.view.apply {
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM) updateLayoutParams<FrameLayout.LayoutParams> {
width = WRAP_CONTENT gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)
} width = WRAP_CONTENT
translationY = -(navBarHeight.dp + 32f) }
translationZ = 32f translationY = -(navBarHeight.dp + 32f)
updatePadding(16f.px, right = 16f.px) translationZ = 32f
setOnClickListener { updatePadding(16f.px, right = 16f.px)
snackBar.dismiss() setOnClickListener {
} snackBar.dismiss()
setOnLongClickListener { }
copyToClipboard(clipboard ?: s, false) setOnLongClickListener {
toast(getString(R.string.copied_to_clipboard)) copyToClipboard(clipboard ?: s, false)
true toast(getString(R.string.copied_to_clipboard))
true
}
} }
snackBar.show()
} }
snackBar.show() return snackBar
} }
Logger.log(s)
} }
logger(s) } catch (e: Exception) {
Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e)
} }
return null
} }
open class NoPaddingArrayAdapter<T>(context: Context, layoutId: Int, items: List<T>) : open class NoPaddingArrayAdapter<T>(context: Context, layoutId: Int, items: List<T>) :
@@ -930,6 +1165,54 @@ fun checkCountry(context: Context): Boolean {
} }
} }
const val INCOGNITO_CHANNEL_ID = 26
@SuppressLint("LaunchActivityFromNotification")
fun incognitoNotification(context: Context) {
val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
if (incognito) {
val intent = Intent(context, IncognitoNotificationClickReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context, 0, intent,
PendingIntent.FLAG_IMMUTABLE
)
val builder = NotificationCompat.Builder(context, Notifications.CHANNEL_INCOGNITO_MODE)
.setSmallIcon(R.drawable.ic_incognito_24)
.setContentTitle("Incognito Mode")
.setContentText("Disable Incognito Mode")
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(pendingIntent)
.setOngoing(true)
notificationManager.notify(INCOGNITO_CHANNEL_ID, builder.build())
} else {
notificationManager.cancel(INCOGNITO_CHANNEL_ID)
}
}
fun hasNotificationPermission(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
} else {
NotificationManagerCompat.from(context).areNotificationsEnabled()
}
}
fun openSettings(context: Context, channelId: String?): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val intent = Intent(
if (channelId != null) Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS
else Settings.ACTION_APP_NOTIFICATION_SETTINGS
).apply {
putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
putExtra(Settings.EXTRA_CHANNEL_ID, channelId)
}
context.startActivity(intent)
true
} else false
}
suspend fun View.pop() { suspend fun View.pop() {
currActivity()?.runOnUiThread { currActivity()?.runOnUiThread {
ObjectAnimator.ofFloat(this@pop, "scaleX", 1f, 1.25f).setDuration(120).start() ObjectAnimator.ofFloat(this@pop, "scaleX", 1f, 1.25f).setDuration(120).start()
@@ -942,3 +1225,100 @@ suspend fun View.pop() {
} }
delay(100) delay(100)
} }
fun blurImage(imageView: ImageView, banner: String?) {
if (banner != null) {
val radius = PrefManager.getVal<Float>(PrefName.BlurRadius).toInt()
val sampling = PrefManager.getVal<Float>(PrefName.BlurSampling).toInt()
if (PrefManager.getVal(PrefName.BlurBanners)) {
val context = imageView.context
if (!(context as Activity).isDestroyed) {
val url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { banner }
Glide.with(context as Context)
.load(GlideUrl(url))
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
.apply(RequestOptions.bitmapTransform(BlurTransformation(radius, sampling)))
.into(imageView)
}
} else {
imageView.loadImage(banner)
}
} else {
imageView.setImageResource(R.drawable.linear_gradient_bg)
}
}
/**
* Builds the markwon instance with all the plugins
* @return the markwon instance
*/
fun buildMarkwon(
activity: Context,
userInputContent: Boolean = true,
fragment: Fragment? = null
): Markwon {
val glideContext = fragment?.let { Glide.with(it) } ?: Glide.with(activity)
val markwon = Markwon.builder(activity)
.usePlugin(object : AbstractMarkwonPlugin() {
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
builder.linkResolver { _, link ->
copyToClipboard(link, true)
}
}
})
.usePlugin(SoftBreakAddsNewLinePlugin.create())
.usePlugin(StrikethroughPlugin.create())
.usePlugin(TablePlugin.create(activity))
.usePlugin(TaskListPlugin.create(activity))
.usePlugin(SpoilerPlugin())
.usePlugin(HtmlPlugin.create { plugin ->
if (userInputContent) {
plugin.addHandler(
TagHandlerNoOp.create("h1", "h2", "h3", "h4", "h5", "h6", "hr", "pre", "a")
)
}
})
.usePlugin(GlideImagesPlugin.create(object : GlideImagesPlugin.GlideStore {
private val requestManager: RequestManager = glideContext.apply {
addDefaultRequestListener(object : RequestListener<Any> {
override fun onResourceReady(
resource: Any,
model: Any,
target: Target<Any>,
dataSource: DataSource,
isFirstResource: Boolean
): Boolean {
if (resource is GifDrawable) {
resource.start()
}
return false
}
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Any>,
isFirstResource: Boolean
): Boolean {
Logger.log("Image failed to load: $model")
Logger.log(e as Exception)
return false
}
})
}
override fun load(drawable: AsyncDrawable): RequestBuilder<Drawable> {
Logger.log("Loading image: ${drawable.destination}")
return requestManager.load(drawable.destination)
}
override fun cancel(target: Target<*>) {
Logger.log("Cancelling image load")
requestManager.clear(target)
}
}))
.build()
return markwon
}

View File

@@ -1,8 +1,11 @@
package ani.dantotsu package ani.dantotsu
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.content.Context import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Intent import android.content.Intent
import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
import android.graphics.drawable.GradientDrawable import android.graphics.drawable.GradientDrawable
import android.net.Uri import android.net.Uri
@@ -11,37 +14,60 @@ import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.provider.Settings import android.provider.Settings
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.AnticipateInterpolator import android.view.animation.AnticipateInterpolator
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.activity.addCallback import androidx.activity.addCallback
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.animation.doOnEnd import androidx.core.animation.doOnEnd
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.doOnAttach import androidx.core.view.doOnAttach
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updateMargins
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.Download
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.work.OneTimeWorkRequest
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistHomeViewModel import ani.dantotsu.connections.anilist.AnilistHomeViewModel
import ani.dantotsu.databinding.ActivityMainBinding import ani.dantotsu.databinding.ActivityMainBinding
import ani.dantotsu.databinding.SplashScreenBinding import ani.dantotsu.databinding.SplashScreenBinding
import ani.dantotsu.download.video.Helper
import ani.dantotsu.home.AnimeFragment import ani.dantotsu.home.AnimeFragment
import ani.dantotsu.home.HomeFragment import ani.dantotsu.home.HomeFragment
import ani.dantotsu.home.LoginFragment import ani.dantotsu.home.LoginFragment
import ani.dantotsu.home.MangaFragment import ani.dantotsu.home.MangaFragment
import ani.dantotsu.home.NoInternet import ani.dantotsu.home.NoInternet
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.notifications.anilist.AnilistNotificationWorker
import ani.dantotsu.notifications.comment.CommentNotificationWorker
import ani.dantotsu.others.CustomBottomDialog import ani.dantotsu.others.CustomBottomDialog
import ani.dantotsu.others.LangSet import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.profile.activity.FeedActivity
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription import ani.dantotsu.profile.activity.NotificationActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefManager.asLiveBool
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.settings.saving.SharedPreferenceBooleanLiveData
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferencePackager
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.Logger
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputEditText
import eu.kanade.domain.source.service.SourcePreferences
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin import io.noties.markwon.SoftBreakAddsNewLinePlugin
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -49,41 +75,144 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import nl.joery.animatedbottombar.AnimatedBottomBar import nl.joery.animatedbottombar.AnimatedBottomBar
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.Serializable import java.io.Serializable
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private lateinit var incognitoLiveData: SharedPreferenceBooleanLiveData
private val scope = lifecycleScope private val scope = lifecycleScope
private var load = false private var load = false
private var uiSettings = UserInterfaceSettings()
@SuppressLint("InternalInsetResource", "DiscouragedApi")
@OptIn(UnstableApi::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
LangSet.setLocale(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
//get FRAGMENT_CLASS_NAME from intent
val fragment = intent.getStringExtra("FRAGMENT_CLASS_NAME")
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
androidx.work.WorkManager.getInstance(this)
.enqueue(OneTimeWorkRequest.Companion.from(CommentNotificationWorker::class.java))
androidx.work.WorkManager.getInstance(this)
.enqueue(OneTimeWorkRequest.Companion.from(AnilistNotificationWorker::class.java))
val action = intent.action
val type = intent.type
if (Intent.ACTION_VIEW == action && type != null) {
val uri: Uri? = intent.data
try {
if (uri == null) {
throw Exception("Uri is null")
}
val jsonString =
contentResolver.openInputStream(uri)?.readBytes()
?: throw Exception("Error reading file")
val name =
DocumentFile.fromSingleUri(this, uri)?.name ?: "settings"
//.sani is encrypted, .ani is not
if (name.endsWith(".sani")) {
passwordAlertDialog { password ->
if (password != null) {
val salt = jsonString.copyOfRange(0, 16)
val encrypted = jsonString.copyOfRange(16, jsonString.size)
val decryptedJson = try {
PreferenceKeystore.decryptWithPassword(
password,
encrypted,
salt
)
} catch (e: Exception) {
toast("Incorrect password")
return@passwordAlertDialog
}
if (PreferencePackager.unpack(decryptedJson)) {
val intent = Intent(this, this.javaClass)
this.finish()
startActivity(intent)
}
} else {
toast("Password cannot be empty")
}
}
} else if (name.endsWith(".ani")) {
val decryptedJson = jsonString.toString(Charsets.UTF_8)
if (PreferencePackager.unpack(decryptedJson)) {
val intent = Intent(this, this.javaClass)
this.finish()
startActivity(intent)
}
} else {
toast("Invalid file type")
}
} catch (e: Exception) {
e.printStackTrace()
toast("Error importing settings")
}
}
val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar) val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val backgroundDrawable = _bottomBar.background as GradientDrawable val backgroundDrawable = _bottomBar.background as GradientDrawable
val currentColor = backgroundDrawable.color?.defaultColor ?: 0 val currentColor = backgroundDrawable.color?.defaultColor ?: 0
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xE8000000.toInt() val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xF9000000.toInt()
backgroundDrawable.setColor(semiTransparentColor) backgroundDrawable.setColor(semiTransparentColor)
_bottomBar.background = backgroundDrawable _bottomBar.background = backgroundDrawable
} }
val colorOverflow = this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) _bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
.getBoolean("colorOverflow", false)
if (!colorOverflow) {
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
val offset = try {
val statusBarHeightId = resources.getIdentifier("status_bar_height", "dimen", "android")
resources.getDimensionPixelSize(statusBarHeightId)
} catch (e: Exception) {
statusBarHeight
} }
val layoutParams = binding.incognito.layoutParams as ViewGroup.MarginLayoutParams
layoutParams.topMargin = 11 * offset / 12
binding.incognito.layoutParams = layoutParams
incognitoLiveData = PrefManager.getLiveVal(
PrefName.Incognito,
false
).asLiveBool()
incognitoLiveData.observe(this) {
if (it) {
val slideDownAnim = ObjectAnimator.ofFloat(
binding.incognito,
View.TRANSLATION_Y,
-(binding.incognito.height.toFloat() + statusBarHeight),
0f
)
slideDownAnim.duration = 200
slideDownAnim.start()
binding.incognito.visibility = View.VISIBLE
} else {
val slideUpAnim = ObjectAnimator.ofFloat(
binding.incognito,
View.TRANSLATION_Y,
0f,
-(binding.incognito.height.toFloat() + statusBarHeight)
)
slideUpAnim.duration = 200
slideUpAnim.start()
//wait for animation to finish
Handler(Looper.getMainLooper()).postDelayed(
{ binding.incognito.visibility = View.GONE },
200
)
}
}
incognitoNotification(this)
var doubleBackToExitPressedOnce = false var doubleBackToExitPressedOnce = false
onBackPressedDispatcher.addCallback(this) { onBackPressedDispatcher.addCallback(this) {
@@ -91,11 +220,25 @@ class MainActivity : AppCompatActivity() {
finish() finish()
} }
doubleBackToExitPressedOnce = true doubleBackToExitPressedOnce = true
snackString(this@MainActivity.getString(R.string.back_to_exit)) snackString(this@MainActivity.getString(R.string.back_to_exit)).apply {
Handler(Looper.getMainLooper()).postDelayed( this?.addCallback(object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
{ doubleBackToExitPressedOnce = false }, override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
2000 super.onDismissed(transientBottomBar, event)
) doubleBackToExitPressedOnce = false
}
})
}
}
val preferences: SourcePreferences = Injekt.get()
if (preferences.animeExtensionUpdatesCount()
.get() > 0 || preferences.mangaExtensionUpdatesCount().get() > 0
) {
Toast.makeText(
this,
"You have extension updates available!",
Toast.LENGTH_LONG
).show()
} }
binding.root.isMotionEventSplittingEnabled = false binding.root.isMotionEventSplittingEnabled = false
@@ -141,109 +284,240 @@ class MainActivity : AppCompatActivity() {
binding.root.doOnAttach { binding.root.doOnAttach {
initActivity(this) initActivity(this)
uiSettings = loadData("ui_settings") ?: uiSettings window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent)
selectedOption = uiSettings.defaultStartUpTab selectedOption = if (fragment != null) {
when (fragment) {
AnimeFragment::class.java.name -> 0
HomeFragment::class.java.name -> 1
MangaFragment::class.java.name -> 2
else -> 1
}
} else {
PrefManager.getVal(PrefName.DefaultStartUpTab)
}
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight bottomMargin = navBarHeight
} }
} }
intent.extras?.let { extras ->
val fragmentToLoad = extras.getString("FRAGMENT_TO_LOAD")
val mediaId = extras.getInt("mediaId", -1)
val commentId = extras.getInt("commentId", -1)
val activityId = extras.getInt("activityId", -1)
if (fragmentToLoad != null && mediaId != -1 && commentId != -1) {
val detailIntent = Intent(this, MediaDetailsActivity::class.java).apply {
putExtra("FRAGMENT_TO_LOAD", fragmentToLoad)
putExtra("mediaId", mediaId)
putExtra("commentId", commentId)
}
startActivity(detailIntent)
} else if (fragmentToLoad == "FEED" && activityId != -1) {
val feedIntent = Intent(this, FeedActivity::class.java).apply {
putExtra("FRAGMENT_TO_LOAD", "NOTIFICATIONS")
putExtra("activityId", activityId)
}
startActivity(feedIntent)
} else if (fragmentToLoad == "NOTIFICATIONS" && activityId != -1) {
Logger.log("MainActivity, onCreate: $activityId")
val notificationIntent = Intent(this, NotificationActivity::class.java).apply {
putExtra("FRAGMENT_TO_LOAD", "NOTIFICATIONS")
putExtra("activityId", activityId)
}
startActivity(notificationIntent)
}
}
val offlineMode: Boolean = PrefManager.getVal(PrefName.OfflineMode)
if (!isOnline(this)) { if (!isOnline(this)) {
snackString(this@MainActivity.getString(R.string.no_internet_connection)) snackString(this@MainActivity.getString(R.string.no_internet_connection))
startActivity(Intent(this, NoInternet::class.java)) startActivity(Intent(this, NoInternet::class.java))
} else { } else {
val model: AnilistHomeViewModel by viewModels() if (offlineMode) {
model.genres.observe(this) { snackString(this@MainActivity.getString(R.string.no_internet_connection))
if (it != null) { startActivity(Intent(this, NoInternet::class.java))
if (it) { } else {
val navbar = binding.includedNavbar.navbar val model: AnilistHomeViewModel by viewModels()
bottomBar = navbar model.genres.observe(this) { it ->
navbar.visibility = View.VISIBLE if (it != null) {
binding.mainProgressBar.visibility = View.GONE if (it) {
val mainViewPager = binding.viewpager val navbar = binding.includedNavbar.navbar
mainViewPager.isUserInputEnabled = false bottomBar = navbar
mainViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle) navbar.visibility = View.VISIBLE
mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings)) binding.mainProgressBar.visibility = View.GONE
navbar.setOnTabSelectListener(object : val mainViewPager = binding.viewpager
AnimatedBottomBar.OnTabSelectListener { mainViewPager.isUserInputEnabled = false
override fun onTabSelected( mainViewPager.adapter =
lastIndex: Int, ViewPagerAdapter(supportFragmentManager, lifecycle)
lastTab: AnimatedBottomBar.Tab?, mainViewPager.setPageTransformer(ZoomOutPageTransformer())
newIndex: Int, navbar.setOnTabSelectListener(object :
newTab: AnimatedBottomBar.Tab AnimatedBottomBar.OnTabSelectListener {
) { override fun onTabSelected(
navbar.animate().translationZ(12f).setDuration(200).start() lastIndex: Int,
selectedOption = newIndex lastTab: AnimatedBottomBar.Tab?,
mainViewPager.setCurrentItem(newIndex, false) newIndex: Int,
newTab: AnimatedBottomBar.Tab
) {
navbar.animate().translationZ(12f).setDuration(200).start()
selectedOption = newIndex
mainViewPager.setCurrentItem(newIndex, false)
}
})
if (mainViewPager.getCurrentItem() != selectedOption) {
navbar.selectTabAt(selectedOption)
mainViewPager.post {
mainViewPager.setCurrentItem(
selectedOption,
false
)
}
} }
})
navbar.selectTabAt(selectedOption)
mainViewPager.post { mainViewPager.setCurrentItem(selectedOption, false) }
} else {
binding.mainProgressBar.visibility = View.GONE
}
}
}
//Load Data
if (!load) {
scope.launch(Dispatchers.IO) {
model.loadMain(this@MainActivity)
val id = intent.extras?.getInt("mediaId", 0)
val isMAL = intent.extras?.getBoolean("mal") ?: false
val cont = intent.extras?.getBoolean("continue") ?: false
if (id != null && id != 0) {
val media = withContext(Dispatchers.IO) {
Anilist.query.getMedia(id, isMAL)
}
if (media != null) {
media.cameFromContinue = cont
startActivity(
Intent(this@MainActivity, MediaDetailsActivity::class.java)
.putExtra("media", media as Serializable)
)
} else { } else {
snackString(this@MainActivity.getString(R.string.anilist_not_found)) binding.mainProgressBar.visibility = View.GONE
} }
} }
delay(500)
startSubscription()
} }
load = true //Load Data
} if (!load) {
scope.launch(Dispatchers.IO) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { model.loadMain(this@MainActivity)
if (loadData<Boolean>("allow_opening_links", this) != true) { val id = intent.extras?.getInt("mediaId", 0)
CustomBottomDialog.newInstance().apply { val isMAL = intent.extras?.getBoolean("mal") ?: false
title = "Allow Dantotsu to automatically open Anilist & MAL Links?" val cont = intent.extras?.getBoolean("continue") ?: false
val md = "Open settings & click +Add Links & select Anilist & Mal urls" if (id != null && id != 0) {
addView(TextView(this@MainActivity).apply { val media = withContext(Dispatchers.IO) {
val markWon = Anilist.query.getMedia(id, isMAL)
Markwon.builder(this@MainActivity) }
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build() if (media != null) {
markWon.setMarkdown(this, md) media.cameFromContinue = cont
})
setNegativeButton(this@MainActivity.getString(R.string.no)) {
saveData("allow_opening_links", true, this@MainActivity)
dismiss()
}
setPositiveButton(this@MainActivity.getString(R.string.yes)) {
saveData("allow_opening_links", true, this@MainActivity)
tryWith(true) {
startActivity( startActivity(
Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS) Intent(this@MainActivity, MediaDetailsActivity::class.java)
.setData(Uri.parse("package:$packageName")) .putExtra("media", media as Serializable)
)
} else {
snackString(this@MainActivity.getString(R.string.anilist_not_found))
}
}
val username = intent.extras?.getString("username")
if (username != null) {
val nameInt = username.toIntOrNull()
if (nameInt != null) {
startActivity(
Intent(this@MainActivity, ProfileActivity::class.java)
.putExtra("userId", nameInt)
)
} else {
startActivity(
Intent(this@MainActivity, ProfileActivity::class.java)
.putExtra("username", username)
) )
} }
} }
}.show(supportFragmentManager, "dialog") }
load = true
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (!(PrefManager.getVal(PrefName.AllowOpeningLinks) as Boolean)) {
CustomBottomDialog.newInstance().apply {
title = "Allow Dantotsu to automatically open Anilist & MAL Links?"
val md = "Open settings & click +Add Links & select Anilist & Mal urls"
addView(TextView(this@MainActivity).apply {
val markWon =
Markwon.builder(this@MainActivity)
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
markWon.setMarkdown(this, md)
})
setNegativeButton(this@MainActivity.getString(R.string.no)) {
PrefManager.setVal(PrefName.AllowOpeningLinks, true)
dismiss()
}
setPositiveButton(this@MainActivity.getString(R.string.yes)) {
PrefManager.setVal(PrefName.AllowOpeningLinks, true)
tryWith(true) {
startActivity(
Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS)
.setData(Uri.parse("package:$packageName"))
)
}
dismiss()
}
}.show(supportFragmentManager, "dialog")
}
}
}
}
lifecycleScope.launch(Dispatchers.IO) { //simple cleanup
val index = Helper.downloadManager(this@MainActivity).downloadIndex
val downloadCursor = index.getDownloads()
while (downloadCursor.moveToNext()) {
val download = downloadCursor.download
if (download.state == Download.STATE_FAILED) {
Helper.downloadManager(this@MainActivity).removeDownload(download.request.id)
} }
} }
} }
} }
override fun onRestart() {
super.onRestart()
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent)
}
private val Int.toPx get() = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), Resources.getSystem().displayMetrics
).toInt()
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
val params : ViewGroup.MarginLayoutParams =
binding.includedNavbar.navbar.layoutParams as ViewGroup.MarginLayoutParams
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE)
params.updateMargins(bottom = 8.toPx)
else
params.updateMargins(bottom = 32.toPx)
}
private fun passwordAlertDialog(callback: (CharArray?) -> Unit) {
val password = CharArray(16).apply { fill('0') }
// Inflate the dialog layout
val dialogView =
LayoutInflater.from(this).inflate(R.layout.dialog_user_agent, null)
dialogView.findViewById<TextInputEditText>(R.id.userAgentTextBox)?.hint = "Password"
val subtitleTextView = dialogView.findViewById<TextView>(R.id.subtitle)
subtitleTextView?.visibility = View.VISIBLE
subtitleTextView?.text = "Enter your password to decrypt the file"
val dialog = AlertDialog.Builder(this, R.style.MyPopup)
.setTitle("Enter Password")
.setView(dialogView)
.setPositiveButton("OK", null)
.setNegativeButton("Cancel") { dialog, _ ->
password.fill('0')
dialog.dismiss()
callback(null)
}
.create()
dialog.window?.setDimAmount(0.8f)
dialog.show()
// Override the positive button here
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val editText = dialog.findViewById<TextInputEditText>(R.id.userAgentTextBox)
if (editText?.text?.isNotBlank() == true) {
editText.text?.toString()?.trim()?.toCharArray(password)
dialog.dismiss()
callback(password)
} else {
toast("Password cannot be empty")
}
}
}
//ViewPager //ViewPager
private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
@@ -261,4 +535,4 @@ class MainActivity : AppCompatActivity() {
} }
} }
} }

View File

@@ -5,6 +5,7 @@ import android.os.Build
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import ani.dantotsu.others.webview.CloudFlare import ani.dantotsu.others.webview.CloudFlare
import ani.dantotsu.others.webview.WebViewBottomDialog import ani.dantotsu.others.webview.WebViewBottomDialog
import ani.dantotsu.util.Logger
import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.ResponseParser import com.lagradost.nicehttp.ResponseParser
import com.lagradost.nicehttp.addGenericDns import com.lagradost.nicehttp.addGenericDns
@@ -104,6 +105,7 @@ fun logError(e: Throwable, post: Boolean = true, snackbar: Boolean = true) {
toast(e.localizedMessage) toast(e.localizedMessage)
} }
e.printStackTrace() e.printStackTrace()
Logger.log(e)
} }
fun <T> tryWith(post: Boolean = false, snackbar: Boolean = true, call: () -> T): T? { fun <T> tryWith(post: Boolean = false, snackbar: Boolean = true, call: () -> T): T? {
@@ -134,7 +136,7 @@ suspend fun <T> tryWithSuspend(
* A url, which can also have headers * A url, which can also have headers
* **/ * **/
data class FileUrl( data class FileUrl(
val url: String, var url: String,
val headers: Map<String, String> = mapOf() val headers: Map<String, String> = mapOf()
) : Serializable { ) : Serializable {
companion object { companion object {

View File

@@ -2,8 +2,11 @@ package ani.dantotsu.aniyomi.anime.custom
import android.app.Application import android.app.Application
import android.content.Context import androidx.annotation.OptIn
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.media3.common.util.UnstableApi
import androidx.media3.database.StandaloneDatabaseProvider
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.media.manga.MangaCache import ani.dantotsu.media.manga.MangaCache
import ani.dantotsu.parsers.novel.NovelExtensionManager import ani.dantotsu.parsers.novel.NovelExtensionManager
@@ -13,7 +16,6 @@ import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.source.anime.AndroidAnimeSourceManager import eu.kanade.tachiyomi.source.anime.AndroidAnimeSourceManager
import eu.kanade.tachiyomi.source.manga.AndroidMangaSourceManager import eu.kanade.tachiyomi.source.manga.AndroidMangaSourceManager
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@@ -27,12 +29,13 @@ import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class AppModule(val app: Application) : InjektModule { class AppModule(val app: Application) : InjektModule {
@OptIn(UnstableApi::class)
override fun InjektRegistrar.registerInjectables() { override fun InjektRegistrar.registerInjectables() {
addSingleton(app) addSingleton(app)
addSingletonFactory { DownloadsManager(app) } addSingletonFactory { DownloadsManager(app) }
addSingletonFactory { NetworkHelper(app, get()) } addSingletonFactory { NetworkHelper(app) }
addSingletonFactory { AnimeExtensionManager(app) } addSingletonFactory { AnimeExtensionManager(app) }
addSingletonFactory { MangaExtensionManager(app) } addSingletonFactory { MangaExtensionManager(app) }
@@ -41,9 +44,6 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory<AnimeSourceManager> { AndroidAnimeSourceManager(app, get()) } addSingletonFactory<AnimeSourceManager> { AndroidAnimeSourceManager(app, get()) }
addSingletonFactory<MangaSourceManager> { AndroidMangaSourceManager(app, get()) } addSingletonFactory<MangaSourceManager> { AndroidMangaSourceManager(app, get()) }
val sharedPreferences = app.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
addSingleton(sharedPreferences)
addSingletonFactory { addSingletonFactory {
Json { Json {
ignoreUnknownKeys = true ignoreUnknownKeys = true
@@ -51,6 +51,12 @@ class AppModule(val app: Application) : InjektModule {
} }
} }
addSingletonFactory { StandaloneDatabaseProvider(app) }
addSingletonFactory<CrashlyticsInterface> {
ani.dantotsu.connections.crashlytics.CrashlyticsFactory.createCrashlytics()
}
addSingletonFactory { MangaCache() } addSingletonFactory { MangaCache() }
ContextCompat.getMainExecutor(app).execute { ContextCompat.getMainExecutor(app).execute {
@@ -66,13 +72,6 @@ class PreferenceModule(val application: Application) : InjektModule {
AndroidPreferenceStore(application) AndroidPreferenceStore(application)
} }
addSingletonFactory {
NetworkPreferences(
preferenceStore = get(),
verboseLogging = false,
)
}
addSingletonFactory { addSingletonFactory {
SourcePreferences(get()) SourcePreferences(get())
} }

View File

@@ -6,20 +6,20 @@ import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.mal.MAL import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.toast import ani.dantotsu.toast
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.roundToInt
fun updateProgress(media: Media, number: String) { fun updateProgress(media: Media, number: String) {
val incognito = currContext()?.getSharedPreferences("Dantotsu", 0) val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
?.getBoolean("incognito", false) ?: false
if (!incognito) { if (!incognito) {
if (Anilist.userid != null) { if (Anilist.userid != null) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val a = number.toFloatOrNull()?.toInt() val a = number.toFloatOrNull()?.toInt()
if ((a ?: 0) > (media.userProgress ?: 0)) { if ((a ?: 0) > (media.userProgress ?: -1)) {
Anilist.mutation.editList( Anilist.mutation.editList(
media.id, media.id,
a, a,

View File

@@ -3,13 +3,18 @@ package ani.dantotsu.connections.anilist
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.util.Log
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.client import ani.dantotsu.client
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.openLinkInBrowser import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.toast
import ani.dantotsu.tryWithSuspend import ani.dantotsu.tryWithSuspend
import java.io.File import ani.dantotsu.util.Logger
import java.util.Calendar import java.util.Calendar
object Anilist { object Anilist {
@@ -24,10 +29,13 @@ object Anilist {
var bg: String? = null var bg: String? = null
var episodesWatched: Int? = null var episodesWatched: Int? = null
var chapterRead: Int? = null var chapterRead: Int? = null
var unreadNotificationCount: Int = 0
var genres: ArrayList<String>? = null var genres: ArrayList<String>? = null
var tags: Map<Boolean, List<String>>? = null var tags: Map<Boolean, List<String>>? = null
var rateLimitReset: Long = 0
val sortBy = listOf( val sortBy = listOf(
"SCORE_DESC", "SCORE_DESC",
"POPULARITY_DESC", "POPULARITY_DESC",
@@ -94,15 +102,12 @@ object Anilist {
} }
} }
fun getSavedToken(context: Context): Boolean { fun getSavedToken(): Boolean {
if ("anilistToken" in context.fileList()) { token = PrefManager.getVal(PrefName.AnilistToken, null as String?)
token = File(context.filesDir, "anilistToken").readText() return !token.isNullOrEmpty()
return true
}
return false
} }
fun removeSavedToken(context: Context) { fun removeSavedToken() {
token = null token = null
username = null username = null
adult = false adult = false
@@ -111,9 +116,7 @@ object Anilist {
bg = null bg = null
episodesWatched = null episodesWatched = null
chapterRead = null chapterRead = null
if ("anilistToken" in context.fileList()) { PrefManager.removeVal(PrefName.AnilistToken)
File(context.filesDir, "anilistToken").delete()
}
} }
suspend inline fun <reified T : Any> executeQuery( suspend inline fun <reified T : Any> executeQuery(
@@ -124,7 +127,12 @@ object Anilist {
show: Boolean = false, show: Boolean = false,
cache: Int? = null cache: Int? = null
): T? { ): T? {
return tryWithSuspend { return try {
if (show) Logger.log("Anilist Query: $query")
if (rateLimitReset > System.currentTimeMillis() / 1000) {
toast("Rate limited. Try after ${rateLimitReset - (System.currentTimeMillis() / 1000)} seconds")
throw Exception("Rate limited after ${rateLimitReset - (System.currentTimeMillis() / 1000)} seconds")
}
val data = mapOf( val data = mapOf(
"query" to query, "query" to query,
"variables" to variables "variables" to variables
@@ -143,10 +151,26 @@ object Anilist {
data = data, data = data,
cacheTime = cache ?: 10 cacheTime = cache ?: 10
) )
if (!json.text.startsWith("{")) throw Exception(currContext()?.getString(R.string.anilist_down)) val remaining = json.headers["X-RateLimit-Remaining"]?.toIntOrNull() ?: -1
if (show) println("Response : ${json.text}") Logger.log("Remaining requests: $remaining")
if (json.code == 429) {
val retry = json.headers["Retry-After"]?.toIntOrNull() ?: -1
val passedLimitReset = json.headers["X-RateLimit-Reset"]?.toLongOrNull() ?: 0
if (retry > 0) {
rateLimitReset = passedLimitReset
}
toast("Rate limited. Try after $retry seconds")
throw Exception("Rate limited after $retry seconds")
}
if (!json.text.startsWith("{")) {throw Exception(currContext()?.getString(R.string.anilist_down))}
if (show) Logger.log("Anilist Response: ${json.text}")
json.parsed() json.parsed()
} else null } else null
} catch (e: Exception) {
if (show) snackString("Error fetching Anilist data: ${e.message}")
Logger.log("Anilist Query Error: ${e.message}")
null
} }
} }
} }

View File

@@ -13,6 +13,23 @@ class AnilistMutations {
executeQuery<JsonObject>(query, variables) executeQuery<JsonObject>(query, variables)
} }
suspend fun toggleFav(type: FavType, id: Int): Boolean {
val filter = when (type) {
FavType.ANIME -> "animeId"
FavType.MANGA -> "mangaId"
FavType.CHARACTER -> "characterId"
FavType.STAFF -> "staffId"
FavType.STUDIO -> "studioId"
}
val query = """mutation{ToggleFavourite($filter:$id){anime{pageInfo{total}}}}"""
val result = executeQuery<JsonObject>(query)
return result?.get("errors") == null && result != null
}
enum class FavType {
ANIME, MANGA, CHARACTER, STAFF, STUDIO
}
suspend fun editList( suspend fun editList(
mediaID: Int, mediaID: Int,
progress: Int? = null, progress: Int? = null,

View File

@@ -1,45 +1,61 @@
package ani.dantotsu.connections.anilist package ani.dantotsu.connections.anilist
import android.app.Activity import android.util.Base64
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.checkGenreTime import ani.dantotsu.checkGenreTime
import ani.dantotsu.checkId import ani.dantotsu.checkId
import ani.dantotsu.connections.anilist.Anilist.authorRoles import ani.dantotsu.connections.anilist.Anilist.authorRoles
import ani.dantotsu.connections.anilist.Anilist.executeQuery import ani.dantotsu.connections.anilist.Anilist.executeQuery
import ani.dantotsu.connections.anilist.api.FeedResponse
import ani.dantotsu.connections.anilist.api.FuzzyDate import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.anilist.api.NotificationResponse
import ani.dantotsu.connections.anilist.api.Page import ani.dantotsu.connections.anilist.api.Page
import ani.dantotsu.connections.anilist.api.Query import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.connections.anilist.api.ToggleLike
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.loadData import ani.dantotsu.isOnline
import ani.dantotsu.logError import ani.dantotsu.logError
import ani.dantotsu.media.Author import ani.dantotsu.media.Author
import ani.dantotsu.media.Character import ani.dantotsu.media.Character
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.Studio import ani.dantotsu.media.Studio
import ani.dantotsu.others.MalScraper import ani.dantotsu.others.MalScraper
import ani.dantotsu.saveData import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.io.Serializable
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
class AnilistQueries { class AnilistQueries {
suspend fun getUserData(): Boolean { suspend fun getUserData(): Boolean {
val response: Query.Viewer? val response: Query.Viewer?
measureTimeMillis { measureTimeMillis {
response = response =
executeQuery("""{Viewer{name options{displayAdultContent}avatar{medium}bannerImage id mediaListOptions{rowOrder animeList{sectionOrder customLists}mangaList{sectionOrder customLists}}statistics{anime{episodesWatched}manga{chaptersRead}}}}""") executeQuery("""{Viewer{name options{displayAdultContent}avatar{medium}bannerImage id mediaListOptions{rowOrder animeList{sectionOrder customLists}mangaList{sectionOrder customLists}}statistics{anime{episodesWatched}manga{chaptersRead}}unreadNotificationCount}}""")
}.also { println("time : $it") } }.also { println("time : $it") }
val user = response?.data?.user ?: return false val user = response?.data?.user ?: return false
PrefManager.setVal(PrefName.AnilistUserName, user.name)
Anilist.userid = user.id Anilist.userid = user.id
PrefManager.setVal(PrefName.AnilistUserId, user.id.toString())
Anilist.username = user.name Anilist.username = user.name
Anilist.bg = user.bannerImage Anilist.bg = user.bannerImage
Anilist.avatar = user.avatar?.medium Anilist.avatar = user.avatar?.medium
Anilist.episodesWatched = user.statistics?.anime?.episodesWatched Anilist.episodesWatched = user.statistics?.anime?.episodesWatched
Anilist.chapterRead = user.statistics?.manga?.chaptersRead Anilist.chapterRead = user.statistics?.manga?.chaptersRead
Anilist.adult = user.options?.displayAdultContent ?: false Anilist.adult = user.options?.displayAdultContent ?: false
Anilist.unreadNotificationCount = user.unreadNotificationCount ?: 0
val unread = PrefManager.getVal<Int>(PrefName.UnreadCommentNotifications)
Anilist.unreadNotificationCount += unread
return true return true
} }
@@ -56,7 +72,7 @@ class AnilistQueries {
media.cameFromContinue = false media.cameFromContinue = false
val query = val query =
"""{Media(id:${media.id}){id mediaListEntry{id status score(format:POINT_100) progress private notes repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}isFavourite siteUrl idMal nextAiringEpisode{episode airingAt}source countryOfOrigin format duration season seasonYear startDate{year month day}endDate{year month day}genres studios(isMain:true){nodes{id name siteUrl}}description trailer { site id } synonyms tags { name rank isMediaSpoiler } characters(sort:[ROLE,FAVOURITES_DESC],perPage:25,page:1){edges{role node{id image{medium}name{userPreferred}}}}relations{edges{relationType(version:2)node{id idMal mediaListEntry{progress private score(format:POINT_100) status} episodes chapters nextAiringEpisode{episode} popularity meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}staffPreview: staff(perPage: 8, sort: [RELEVANCE, ID]) {edges{role node{id name{userPreferred}}}}recommendations(sort:RATING_DESC){nodes{mediaRecommendation{id idMal mediaListEntry{progress private score(format:POINT_100) status} episodes chapters nextAiringEpisode{episode}meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}externalLinks{url site}}}""" """{Media(id:${media.id}){id mediaListEntry{id status score(format:POINT_100)progress private notes repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}isFavourite siteUrl idMal nextAiringEpisode{episode airingAt}source countryOfOrigin format duration season seasonYear startDate{year month day}endDate{year month day}genres studios(isMain:true){nodes{id name siteUrl}}description trailer{site id}synonyms tags{name rank isMediaSpoiler}characters(sort:[ROLE,FAVOURITES_DESC],perPage:25,page:1){edges{role node{id image{medium}name{userPreferred}isFavourite}}}relations{edges{relationType(version:2)node{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}popularity meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}staffPreview:staff(perPage:8,sort:[RELEVANCE,ID]){edges{role node{id image{large medium}name{userPreferred}}}}recommendations(sort:RATING_DESC){nodes{mediaRecommendation{id idMal mediaListEntry{progress private score(format:POINT_100)status}episodes chapters nextAiringEpisode{episode}meanScore isAdult isFavourite format title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}externalLinks{url site}}}"""
runBlocking { runBlocking {
val anilist = async { val anilist = async {
var response = executeQuery<Query.Media>(query, force = true, show = true) var response = executeQuery<Query.Media>(query, force = true, show = true)
@@ -113,6 +129,30 @@ class AnilistQueries {
name = i.node?.name?.userPreferred, name = i.node?.name?.userPreferred,
image = i.node?.image?.medium, image = i.node?.image?.medium,
banner = media.banner ?: media.cover, banner = media.banner ?: media.cover,
isFav = i.node?.isFavourite ?: false,
role = when (i.role.toString()) {
"MAIN" -> currContext()?.getString(R.string.main_role)
?: "MAIN"
"SUPPORTING" -> currContext()?.getString(R.string.supporting_role)
?: "SUPPORTING"
else -> i.role.toString()
}
)
)
}
}
}
if (fetchedMedia.staff != null) {
media.staff = arrayListOf()
fetchedMedia.staff?.edges?.forEach { i ->
i.node?.apply {
media.staff?.add(
Author(
id = id,
name = i.node?.name?.userPreferred,
image = i.node?.image?.medium,
role = when (i.role.toString()) { role = when (i.role.toString()) {
"MAIN" -> currContext()?.getString(R.string.main_role) "MAIN" -> currContext()?.getString(R.string.main_role)
?: "MAIN" ?: "MAIN"
@@ -203,8 +243,10 @@ class AnilistQueries {
fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let { fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let {
media.anime.author = Author( media.anime.author = Author(
it.id.toString(), it.id,
it.name?.userPreferred ?: "N/A" it.name?.userPreferred ?: "N/A",
it.image?.medium,
"AUTHOR"
) )
} }
@@ -223,8 +265,10 @@ class AnilistQueries {
} else if (media.manga != null) { } else if (media.manga != null) {
fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let { fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let {
media.manga.author = Author( media.manga.author = Author(
it.id.toString(), it.id,
it.name?.userPreferred ?: "N/A" it.name?.userPreferred ?: "N/A",
it.image?.medium,
"AUTHOR"
) )
} }
} }
@@ -239,7 +283,10 @@ class AnilistQueries {
else snackString(currContext()?.getString(R.string.what_did_you_open)) else snackString(currContext()?.getString(R.string.what_did_you_open))
} }
} else { } else {
snackString(currContext()?.getString(R.string.error_getting_data)) if (currContext()?.let { isOnline(it) } == true) {
snackString(currContext()?.getString(R.string.error_getting_data))
} else {
}
} }
} }
val mal = async { val mal = async {
@@ -252,15 +299,82 @@ class AnilistQueries {
return media return media
} }
fun userMediaDetails(media: Media): Media {
val query =
"""{Media(id:${media.id}){id mediaListEntry{id status progress private repeat customLists updatedAt startedAt{year month day}completedAt{year month day}}isFavourite idMal}}"""
runBlocking {
val anilist = async {
var response = executeQuery<Query.Media>(query, force = true, show = true)
if (response != null) {
fun parse() {
val fetchedMedia = response?.data?.media ?: return
if (fetchedMedia.mediaListEntry != null) {
fetchedMedia.mediaListEntry?.apply {
media.userProgress = progress
media.isListPrivate = private ?: false
media.userListId = id
media.userStatus = status?.toString()
media.inCustomListsOf = customLists?.toMutableMap()
media.userRepeat = repeat ?: 0
media.userUpdatedAt = updatedAt?.toString()?.toLong()?.times(1000)
media.userCompletedAt = completedAt ?: FuzzyDate()
media.userStartedAt = startedAt ?: FuzzyDate()
}
} else {
media.isListPrivate = false
media.userStatus = null
media.userListId = null
media.userProgress = null
media.userRepeat = 0
media.userUpdatedAt = null
media.userCompletedAt = FuzzyDate()
media.userStartedAt = FuzzyDate()
}
}
if (response.data?.media != null) parse()
else {
response = executeQuery(query, force = true, useToken = false)
if (response?.data?.media != null) parse()
}
}
}
awaitAll(anilist)
}
return media
}
suspend fun continueMedia(type: String, planned: Boolean = false): ArrayList<Media> { suspend fun continueMedia(type: String, planned: Boolean = false): ArrayList<Media> {
val returnArray = arrayListOf<Media>() val returnArray = arrayListOf<Media>()
val map = mutableMapOf<Int, Media>() val map = mutableMapOf<Int, Media>()
val statuses = if (!planned) arrayOf("CURRENT", "REPEATING") else arrayOf("PLANNING") val query = if (planned) {
suspend fun repeat(status: String) { """{ planned: ${continueMediaQuery(type, "PLANNING")} }"""
val response = } else {
executeQuery<Query.MediaListCollection>(""" { MediaListCollection(userId: ${Anilist.userid}, type: $type, status: $status , sort: UPDATED_TIME ) { lists { entries { progress private score(format:POINT_100) status media { id idMal type isAdult status chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } } } """) """{
current: ${continueMediaQuery(type, "CURRENT")},
repeating: ${continueMediaQuery(type, "REPEATING")}
}"""
}
response?.data?.mediaListCollection?.lists?.forEach { li -> val response = executeQuery<Query.CombinedMediaListResponse>(query)
if (planned) {
response?.data?.planned?.lists?.forEach { li ->
li.entries?.reversed()?.forEach {
val m = Media(it)
m.cameFromContinue = true
map[m.id] = m
}
}
} else {
response?.data?.current?.lists?.forEach { li ->
li.entries?.reversed()?.forEach {
val m = Media(it)
m.cameFromContinue = true
map[m.id] = m
}
}
response?.data?.repeating?.lists?.forEach { li ->
li.entries?.reversed()?.forEach { li.entries?.reversed()?.forEach {
val m = Media(it) val m = Media(it)
m.cameFromContinue = true m.cameFromContinue = true
@@ -268,11 +382,17 @@ class AnilistQueries {
} }
} }
} }
if (type != "ANIME") {
statuses.forEach { repeat(it) } returnArray.addAll(map.values)
val set = loadData<MutableSet<Int>>("continue_$type") return returnArray
if (set != null) { }
set.reversed().forEach { val list = PrefManager.getNullableCustomVal(
"continueAnimeList",
listOf<Int>(),
List::class.java
) as List<Int>
if (list.isNotEmpty()) {
list.reversed().forEach {
if (map.containsKey(it)) returnArray.add(map[it]!!) if (map.containsKey(it)) returnArray.add(map[it]!!)
} }
for (i in map) { for (i in map) {
@@ -282,13 +402,16 @@ class AnilistQueries {
return returnArray return returnArray
} }
suspend fun favMedia(anime: Boolean): ArrayList<Media> { private fun continueMediaQuery(type: String, status: String): String {
return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: $status , sort: UPDATED_TIME ) { lists { entries { progress private score(format:POINT_100) status media { id idMal type isAdult status chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } } """
}
suspend fun favMedia(anime: Boolean, id: Int? = Anilist.userid): ArrayList<Media> {
var hasNextPage = true var hasNextPage = true
var page = 0 var page = 0
suspend fun getNextPage(page: Int): List<Media> { suspend fun getNextPage(page: Int): List<Media> {
val response = val response = executeQuery<Query.User>("""{${favMediaQuery(anime, page, id)}}""")
executeQuery<Query.User>("""{User(id:${Anilist.userid}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}}""")
val favourites = response?.data?.user?.favourites val favourites = response?.data?.user?.favourites
val apiMediaList = if (anime) favourites?.anime else favourites?.manga val apiMediaList = if (anime) favourites?.anime else favourites?.manga
hasNextPage = apiMediaList?.pageInfo?.hasNextPage ?: false hasNextPage = apiMediaList?.pageInfo?.hasNextPage ?: false
@@ -307,9 +430,12 @@ class AnilistQueries {
return responseArray return responseArray
} }
private fun favMediaQuery(anime: Boolean, page: Int, id: Int? = Anilist.userid): String {
return """User(id:${id}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}"""
}
suspend fun recommendations(): ArrayList<Media> { suspend fun recommendations(): ArrayList<Media> {
val response = val response = executeQuery<Query.Page>("""{${recommendationQuery()}}""")
executeQuery<Query.Page>(""" { Page(page: 1, perPage:30) { pageInfo { total currentPage hasNextPage } recommendations(sort: RATING_DESC, onList: true) { rating userRating mediaRecommendation { id idMal isAdult mediaListEntry { progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode {episode} popularity meanScore isFavourite format title {english romaji userPreferred } type status(version: 2) bannerImage coverImage { large } } } } } """)
val map = mutableMapOf<Int, Media>() val map = mutableMapOf<Int, Media>()
response?.data?.page?.apply { response?.data?.page?.apply {
recommendations?.onEach { recommendations?.onEach {
@@ -325,7 +451,7 @@ class AnilistQueries {
val types = arrayOf("ANIME", "MANGA") val types = arrayOf("ANIME", "MANGA")
suspend fun repeat(type: String) { suspend fun repeat(type: String) {
val res = val res =
executeQuery<Query.MediaListCollection>(""" { MediaListCollection(userId: ${Anilist.userid}, type: $type, status: PLANNING , sort: MEDIA_POPULARITY_DESC ) { lists { entries { media { id mediaListEntry { progress private score(format:POINT_100) status } idMal type isAdult popularity status(version: 2) chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } } } """) executeQuery<Query.MediaListCollection>("""{${recommendationPlannedQuery(type)}}""")
res?.data?.mediaListCollection?.lists?.forEach { li -> res?.data?.mediaListCollection?.lists?.forEach { li ->
li.entries?.forEach { li.entries?.forEach {
val m = Media(it) val m = Media(it)
@@ -343,9 +469,204 @@ class AnilistQueries {
return list return list
} }
private fun recommendationQuery(): String {
return """ Page(page: 1, perPage:30) { pageInfo { total currentPage hasNextPage } recommendations(sort: RATING_DESC, onList: true) { rating userRating mediaRecommendation { id idMal isAdult mediaListEntry { progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode {episode} popularity meanScore isFavourite format title {english romaji userPreferred } type status(version: 2) bannerImage coverImage { large } } } } """
}
private fun recommendationPlannedQuery(type: String): String {
return """ MediaListCollection(userId: ${Anilist.userid}, type: $type, status: PLANNING${if (type == "ANIME") ", sort: MEDIA_POPULARITY_DESC" else ""} ) { lists { entries { media { id mediaListEntry { progress private score(format:POINT_100) status } idMal type isAdult popularity status(version: 2) chapters episodes nextAiringEpisode {episode} meanScore isFavourite format bannerImage coverImage{large} title { english romaji userPreferred } } } } }"""
}
suspend fun initHomePage(): Map<String, ArrayList<Media>> {
val toShow: List<Boolean> =
PrefManager.getVal(PrefName.HomeLayoutShow) // anime continue, anime fav, anime planned, manga continue, manga fav, manga planned, recommendations
var query = """{"""
if (toShow.getOrNull(0) == true) query += """currentAnime: ${
continueMediaQuery(
"ANIME",
"CURRENT"
)
}, repeatingAnime: ${continueMediaQuery("ANIME", "REPEATING")}"""
if (toShow.getOrNull(1) == true) query += """favoriteAnime: ${favMediaQuery(true, 1)}"""
if (toShow.getOrNull(2) == true) query += """plannedAnime: ${
continueMediaQuery(
"ANIME",
"PLANNING"
)
}"""
if (toShow.getOrNull(3) == true) query += """currentManga: ${
continueMediaQuery(
"MANGA",
"CURRENT"
)
}, repeatingManga: ${continueMediaQuery("MANGA", "REPEATING")}"""
if (toShow.getOrNull(4) == true) query += """favoriteManga: ${favMediaQuery(false, 1)}"""
if (toShow.getOrNull(5) == true) query += """plannedManga: ${
continueMediaQuery(
"MANGA",
"PLANNING"
)
}"""
if (toShow.getOrNull(6) == true) query += """recommendationQuery: ${recommendationQuery()}, recommendationPlannedQueryAnime: ${
recommendationPlannedQuery(
"ANIME"
)
}, recommendationPlannedQueryManga: ${recommendationPlannedQuery("MANGA")}"""
query += """}""".trimEnd(',')
val response = executeQuery<Query.HomePageMedia>(query, show = true)
Logger.log(response.toString())
val returnMap = mutableMapOf<String, ArrayList<Media>>()
fun current(type: String) {
val subMap = mutableMapOf<Int, Media>()
val returnArray = arrayListOf<Media>()
val current =
if (type == "Anime") response?.data?.currentAnime else response?.data?.currentManga
val repeating =
if (type == "Anime") response?.data?.repeatingAnime else response?.data?.repeatingManga
current?.lists?.forEach { li ->
li.entries?.reversed()?.forEach {
val m = Media(it)
m.cameFromContinue = true
subMap[m.id] = m
}
}
repeating?.lists?.forEach { li ->
li.entries?.reversed()?.forEach {
val m = Media(it)
m.cameFromContinue = true
subMap[m.id] = m
}
}
if (type != "Anime") {
returnArray.addAll(subMap.values)
returnMap["current$type"] = returnArray
return
}
val list = PrefManager.getNullableCustomVal(
"continueAnimeList",
listOf<Int>(),
List::class.java
) as List<Int>
if (list.isNotEmpty()) {
list.reversed().forEach {
if (subMap.containsKey(it)) returnArray.add(subMap[it]!!)
}
for (i in subMap) {
if (i.value !in returnArray) returnArray.add(i.value)
}
} else returnArray.addAll(subMap.values)
returnMap["current$type"] = returnArray
}
fun planned(type: String) {
val subMap = mutableMapOf<Int, Media>()
val returnArray = arrayListOf<Media>()
val current =
if (type == "Anime") response?.data?.plannedAnime else response?.data?.plannedManga
current?.lists?.forEach { li ->
li.entries?.reversed()?.forEach {
val m = Media(it)
m.cameFromContinue = true
subMap[m.id] = m
}
}
val list = PrefManager.getNullableCustomVal(
"continueAnimeList",
listOf<Int>(),
List::class.java
) as List<Int>
if (list.isNotEmpty()) {
list.reversed().forEach {
if (subMap.containsKey(it)) returnArray.add(subMap[it]!!)
}
for (i in subMap) {
if (i.value !in returnArray) returnArray.add(i.value)
}
} else returnArray.addAll(subMap.values)
returnMap["planned$type"] = returnArray
}
fun favorite(type: String) {
val favourites =
if (type == "Anime") response?.data?.favoriteAnime?.favourites else response?.data?.favoriteManga?.favourites
val apiMediaList = if (type == "Anime") favourites?.anime else favourites?.manga
val returnArray = arrayListOf<Media>()
apiMediaList?.edges?.forEach {
it.node?.let { i ->
returnArray.add(Media(i).apply { isFav = true })
}
}
returnMap["favorite$type"] = returnArray
}
if (toShow.getOrNull(0) == true) {
current("Anime")
}
if (toShow.getOrNull(1) == true) {
favorite("Anime")
}
if (toShow.getOrNull(2) == true) {
planned("Anime")
}
if (toShow.getOrNull(3) == true) {
current("Manga")
}
if (toShow.getOrNull(4) == true) {
favorite("Manga")
}
if (toShow.getOrNull(5) == true) {
planned("Manga")
}
if (toShow.getOrNull(6) == true) {
val subMap = mutableMapOf<Int, Media>()
response?.data?.recommendationQuery?.apply {
recommendations?.onEach {
val json = it.mediaRecommendation
if (json != null) {
val m = Media(json)
m.relation = json.type?.toString()
subMap[m.id] = m
}
}
}
response?.data?.recommendationPlannedQueryAnime?.apply {
lists?.forEach { li ->
li.entries?.forEach {
val m = Media(it)
if (m.status == "RELEASING" || m.status == "FINISHED") {
m.relation = it.media?.type?.toString()
subMap[m.id] = m
}
}
}
}
response?.data?.recommendationPlannedQueryManga?.apply {
lists?.forEach { li ->
li.entries?.forEach {
val m = Media(it)
if (m.status == "RELEASING" || m.status == "FINISHED") {
m.relation = it.media?.type?.toString()
subMap[m.id] = m
}
}
}
}
val list = ArrayList(subMap.values.toList())
list.sortByDescending { it.meanScore }
returnMap["recommendations"] = list
}
return returnMap
}
private suspend fun bannerImage(type: String): String? { private suspend fun bannerImage(type: String): String? {
var image = loadData<BannerImage>("banner_$type") val image = BannerImage(
if (image == null || image.checkTime()) { PrefManager.getCustomVal("banner_${type}_url", ""),
PrefManager.getCustomVal("banner_${type}_time", 0L)
)
if (image.url.isNullOrEmpty() || image.checkTime()) {
val response = val response =
executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: ${Anilist.userid}, type: $type, chunk:1,perChunk:25, sort: [SCORE_DESC,UPDATED_TIME_DESC]) { lists { entries{ media { id bannerImage } } } } } """) executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: ${Anilist.userid}, type: $type, chunk:1,perChunk:25, sort: [SCORE_DESC,UPDATED_TIME_DESC]) { lists { entries{ media { id bannerImage } } } } } """)
val random = response?.data?.mediaListCollection?.lists?.mapNotNull { val random = response?.data?.mediaListCollection?.lists?.mapNotNull {
@@ -355,13 +676,9 @@ class AnilistQueries {
else null else null
} }
}?.flatten()?.randomOrNull() ?: return null }?.flatten()?.randomOrNull() ?: return null
PrefManager.setCustomVal("banner_${type}_url", random)
image = BannerImage( PrefManager.setCustomVal("banner_${type}_time", System.currentTimeMillis())
random, return random
System.currentTimeMillis()
)
saveData("banner_$type", image)
return image.url
} else return image.url } else return image.url
} }
@@ -378,7 +695,7 @@ class AnilistQueries {
sortOrder: String? = null sortOrder: String? = null
): MutableMap<String, ArrayList<Media>> { ): MutableMap<String, ArrayList<Media>> {
val response = val response =
executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: $userId, type: ${if (anime) "ANIME" else "MANGA"}) { lists { name isCustomList entries { status progress private score(format:POINT_100) updatedAt media { id idMal isAdult type status chapters episodes nextAiringEpisode {episode} bannerImage meanScore isFavourite format coverImage{large} startDate{year month day} title {english romaji userPreferred } } } } user { id mediaListOptions { rowOrder animeList { sectionOrder } mangaList { sectionOrder } } } } }""") executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: $userId, type: ${if (anime) "ANIME" else "MANGA"}) { lists { name isCustomList entries { status progress private score(format:POINT_100) updatedAt media { id idMal isAdult type status chapters episodes nextAiringEpisode {episode} bannerImage genres meanScore isFavourite format coverImage{large} startDate{year month day} title {english romaji userPreferred } } } } user { id mediaListOptions { rowOrder animeList { sectionOrder } mangaList { sectionOrder } } } } }""")
val sorted = mutableMapOf<String, ArrayList<Media>>() val sorted = mutableMapOf<String, ArrayList<Media>>()
val unsorted = mutableMapOf<String, ArrayList<Media>>() val unsorted = mutableMapOf<String, ArrayList<Media>>()
val all = arrayListOf<Media>() val all = arrayListOf<Media>()
@@ -406,12 +723,19 @@ class AnilistQueries {
if (!sorted.containsKey(it.key)) sorted[it.key] = it.value if (!sorted.containsKey(it.key)) sorted[it.key] = it.value
} }
sorted["Favourites"] = favMedia(anime) sorted["Favourites"] = favMedia(anime, userId)
sorted["Favourites"]?.sortWith(compareBy { it.userFavOrder }) sorted["Favourites"]?.sortWith(compareBy { it.userFavOrder })
//favMedia doesn't fill userProgress, so we need to fill it manually by searching :(
sorted["Favourites"]?.forEach { fav ->
all.find { it.id == fav.id }?.let {
fav.userProgress = it.userProgress
}
}
sorted["All"] = all sorted["All"] = all
val listSort: String = if (anime) PrefManager.getVal(PrefName.AnimeListSortOrder)
val sort = sortOrder ?: options?.rowOrder else PrefManager.getVal(PrefName.MangaListSortOrder)
val sort = listSort ?: sortOrder ?: options?.rowOrder
for (i in sorted.keys) { for (i in sorted.keys) {
when (sort) { when (sort) {
"score" -> sorted[i]?.sortWith { b, a -> "score" -> sorted[i]?.sortWith { b, a ->
@@ -432,11 +756,19 @@ class AnilistQueries {
} }
suspend fun getGenresAndTags(activity: Activity): Boolean { suspend fun getGenresAndTags(): Boolean {
var genres: ArrayList<String>? = loadData("genres_list", activity) var genres: ArrayList<String>? = PrefManager.getVal<Set<String>>(PrefName.GenresList)
var tags: Map<Boolean, List<String>>? = loadData("tags_map", activity) .toMutableList() as ArrayList<String>?
val adultTags = PrefManager.getVal<Set<String>>(PrefName.TagsListIsAdult).toMutableList()
val nonAdultTags =
PrefManager.getVal<Set<String>>(PrefName.TagsListNonAdult).toMutableList()
var tags = if (adultTags.isEmpty() || nonAdultTags.isEmpty()) null else
mapOf(
true to adultTags.sortedBy { it },
false to nonAdultTags.sortedBy { it }
)
if (genres == null) { if (genres.isNullOrEmpty()) {
executeQuery<Query.GenreCollection>( executeQuery<Query.GenreCollection>(
"""{GenreCollection}""", """{GenreCollection}""",
force = true, force = true,
@@ -446,7 +778,7 @@ class AnilistQueries {
forEach { forEach {
genres?.add(it) genres?.add(it)
} }
saveData("genres_list", genres!!) PrefManager.setVal(PrefName.GenresList, genres?.toSet())
} }
} }
if (tags == null) { if (tags == null) {
@@ -464,11 +796,12 @@ class AnilistQueries {
true to adult, true to adult,
false to good false to good
) )
saveData("tags_map", tags) PrefManager.setVal(PrefName.TagsListIsAdult, adult.toSet())
PrefManager.setVal(PrefName.TagsListNonAdult, good.toSet())
} }
} }
return if (genres != null && tags != null) { return if (!genres.isNullOrEmpty() && tags != null) {
Anilist.genres = genres Anilist.genres = genres?.sortedBy { it }?.toMutableList() as ArrayList<String>
Anilist.tags = tags Anilist.tags = tags
true true
} else false } else false
@@ -484,8 +817,37 @@ class AnilistQueries {
} }
} }
private fun <K, V : Serializable> saveSerializableMap(prefKey: String, map: Map<K, V>) {
val byteStream = ByteArrayOutputStream()
ObjectOutputStream(byteStream).use { outputStream ->
outputStream.writeObject(map)
}
val serializedMap = Base64.encodeToString(byteStream.toByteArray(), Base64.DEFAULT)
PrefManager.setCustomVal(prefKey, serializedMap)
}
@Suppress("UNCHECKED_CAST")
private fun <K, V : Serializable> loadSerializableMap(prefKey: String): Map<K, V>? {
try {
val serializedMap = PrefManager.getCustomVal(prefKey, "")
if (serializedMap.isEmpty()) return null
val bytes = Base64.decode(serializedMap, Base64.DEFAULT)
val byteArrayStream = ByteArrayInputStream(bytes)
return ObjectInputStream(byteArrayStream).use { inputStream ->
inputStream.readObject() as? Map<K, V>
}
} catch (e: Exception) {
return null
}
}
private suspend fun getGenreThumbnail(genre: String): Genre? { private suspend fun getGenreThumbnail(genre: String): Genre? {
val genres = loadData<MutableMap<String, Genre>>("genre_thumb") ?: mutableMapOf() val genres: MutableMap<String, Genre> =
loadSerializableMap<String, Genre>("genre_thumb")?.toMutableMap()
?: mutableMapOf()
if (genres.checkGenreTime(genre)) { if (genres.checkGenreTime(genre)) {
try { try {
val genreQuery = val genreQuery =
@@ -498,7 +860,7 @@ class AnilistQueries {
it.bannerImage!!, it.bannerImage!!,
System.currentTimeMillis() System.currentTimeMillis()
) )
saveData("genre_thumb", genres) saveSerializableMap("genre_thumb", genres)
return genres[genre] return genres[genre]
} }
} }
@@ -653,7 +1015,7 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
page = pageInfo.currentPage.toString().toIntOrNull() ?: 0, page = pageInfo.currentPage.toString().toIntOrNull() ?: 0,
hasNextPage = pageInfo.hasNextPage == true, hasNextPage = pageInfo.hasNextPage == true,
) )
} else snackString(currContext()?.getString(R.string.empty_response)) }
return null return null
} }
@@ -711,7 +1073,7 @@ Page(page:$page,perPage:50) {
if (smaller) { if (smaller) {
val response = execute()?.airingSchedules ?: return null val response = execute()?.airingSchedules ?: return null
val idArr = mutableListOf<Int>() val idArr = mutableListOf<Int>()
val listOnly = loadData("recently_list_only") ?: false val listOnly: Boolean = PrefManager.getVal(PrefName.RecentlyListOnly)
return response.mapNotNull { i -> return response.mapNotNull { i ->
i.media?.let { i.media?.let {
if (!idArr.contains(it.id)) if (!idArr.contains(it.id))
@@ -966,4 +1328,116 @@ Page(page:$page,perPage:50) {
return author return author
} }
suspend fun toggleFollow(id: Int): Query.ToggleFollow? {
return executeQuery<Query.ToggleFollow>(
"""mutation{ToggleFollow(userId:$id){id, isFollowing, isFollower}}"""
)
}
suspend fun toggleLike(id: Int, type: String): ToggleLike? {
return executeQuery<ToggleLike>(
"""mutation Like{ToggleLikeV2(id:$id,type:$type){__typename}}"""
)
}
suspend fun getUserProfile(id: Int): Query.UserProfileResponse? {
return executeQuery<Query.UserProfileResponse>(
"""{followerPage:Page{followers(userId:$id){id}pageInfo{total}}followingPage:Page{following(userId:$id){id}pageInfo{total}}user:User(id:$id){id name about(asHtml:true)avatar{medium large}bannerImage isFollowing isFollower isBlocked favourites{anime{nodes{id coverImage{extraLarge large medium color}}}manga{nodes{id coverImage{extraLarge large medium color}}}characters{nodes{id name{first middle last full native alternative userPreferred}image{large medium}isFavourite}}staff{nodes{id name{first middle last full native alternative userPreferred}image{large medium}isFavourite}}studios{nodes{id name isFavourite}}}statistics{anime{count meanScore standardDeviation minutesWatched episodesWatched chaptersRead volumesRead}manga{count meanScore standardDeviation minutesWatched episodesWatched chaptersRead volumesRead}}siteUrl}}""",
force = true
)
}
suspend fun getUserProfile(username: String): Query.UserProfileResponse? {
val id = getUserId(username) ?: return null
return getUserProfile(id)
}
suspend fun getUserId(username: String): Int? {
return executeQuery<Query.User>(
"""{User(name:"$username"){id}}""",
force = true
)?.data?.user?.id
}
suspend fun getUserStatistics(id: Int, sort: String = "ID"): Query.StatisticsResponse? {
return executeQuery<Query.StatisticsResponse>(
"""{User(id:$id){id name mediaListOptions{scoreFormat}statistics{anime{...UserStatistics}manga{...UserStatistics}}}}fragment UserStatistics on UserStatistics{count meanScore standardDeviation minutesWatched episodesWatched chaptersRead volumesRead formats(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds format}statuses(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds status}scores(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds score}lengths(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds length}releaseYears(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds releaseYear}startYears(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds startYear}genres(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds genre}tags(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds tag{id name}}countries(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds country}voiceActors(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds voiceActor{id name{first middle last full native alternative userPreferred}}characterIds}staff(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds staff{id name{first middle last full native alternative userPreferred}}}studios(sort:$sort){count meanScore minutesWatched chaptersRead mediaIds studio{id name isAnimationStudio}}}""",
force = true,
show = true
)
}
private fun userFavMediaQuery(anime: Boolean, page: Int, id: Int): String {
return """User(id:${id}){id favourites{${if (anime) "anime" else "manga"}(page:$page){pageInfo{hasNextPage}edges{favouriteOrder node{id idMal isAdult mediaListEntry{ progress private score(format:POINT_100) status } chapters isFavourite format episodes nextAiringEpisode{episode}meanScore isFavourite format startDate{year month day} title{english romaji userPreferred}type status(version:2)bannerImage coverImage{large}}}}}}"""
}
suspend fun userFollowing(id: Int): Query.Following? {
return executeQuery<Query.Following>(
"""{Page {following(userId:${id},sort:[USERNAME]){id name avatar{large medium}bannerImage}}}""",
force = true
)
}
suspend fun userFollowers(id: Int): Query.Follower? {
return executeQuery<Query.Follower>(
"""{Page {followers(userId:${id},sort:[USERNAME]){id name avatar{large medium}bannerImage}}}""",
force = true
)
}
suspend fun initProfilePage(id: Int): Query.ProfilePageMedia? {
return executeQuery<Query.ProfilePageMedia>(
"""{
favoriteAnime:${userFavMediaQuery(true, 1, id)}
favoriteManga:${userFavMediaQuery(false, 1, id)}
animeMediaList:${bannerImageQuery("ANIME", id)}
mangaMediaList:${bannerImageQuery("MANGA", id)}
}""".trimIndent(), force = true
)
}
private fun bannerImageQuery(type: String, id: Int?): String {
return """MediaListCollection(userId: ${id}, type: $type, chunk:1,perChunk:25, sort: [SCORE_DESC,UPDATED_TIME_DESC]) { lists { entries{ media { id bannerImage } } } }"""
}
suspend fun getNotifications(id: Int, page: Int = 1, resetNotification: Boolean = true): NotificationResponse? {
val reset = if (resetNotification) "true" else "false"
val res = executeQuery<NotificationResponse>(
"""{User(id:$id){unreadNotificationCount}Page(page:$page,perPage:$ITEMS_PER_PAGE){pageInfo{currentPage,hasNextPage}notifications(resetNotificationCount:$reset){__typename...on AiringNotification{id,type,animeId,episode,contexts,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}},}...on FollowingNotification{id,userId,type,context,createdAt,user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityMessageNotification{id,userId,type,activityId,context,createdAt,message{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityMentionNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplyNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplySubscribedNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityLikeNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ActivityReplyLikeNotification{id,userId,type,activityId,context,createdAt,activity{__typename}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentMentionNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentReplyNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentSubscribedNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadCommentLikeNotification{id,userId,type,commentId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on ThreadLikeNotification{id,userId,type,threadId,context,createdAt,thread{id}comment{id}user{id,name,bannerImage,avatar{medium,large,}}}...on RelatedMediaAdditionNotification{id,type,context,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaDataChangeNotification{id,type,mediaId,context,reason,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaMergeNotification{id,type,mediaId,deletedMediaTitles,context,reason,createdAt,media{id,title{romaji,english,native,userPreferred}bannerImage,coverImage{medium,large}}}...on MediaDeletionNotification{id,type,deletedMediaTitle,context,reason,createdAt,}}}}""",
force = true
)
if (res != null && resetNotification) {
val commentNotifications = PrefManager.getVal(PrefName.UnreadCommentNotifications, 0)
res.data.user.unreadNotificationCount += commentNotifications
PrefManager.setVal(PrefName.UnreadCommentNotifications, 0)
Anilist.unreadNotificationCount = 0
}
return res
}
suspend fun getFeed(userId: Int?, global: Boolean = false, page: Int = 1, activityId: Int? = null): FeedResponse? {
val filter = if (activityId != null) "id:$activityId,"
else if (userId != null) "userId:$userId,"
else if (global) "isFollowing:false,hasRepliesOrTypeText:true,"
else "isFollowing:true,type_not:MESSAGE,"
return executeQuery<FeedResponse>(
"""{Page(page:$page,perPage:$ITEMS_PER_PAGE){activities(${filter}sort:ID_DESC){__typename ... on TextActivity{id userId type replyCount text(asHtml:true)siteUrl isLocked isSubscribed likeCount isLiked isPinned createdAt user{id name bannerImage avatar{medium large}}replies{id userId activityId text(asHtml:true)likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}likes{id name bannerImage avatar{medium large}}}... on ListActivity{id userId type replyCount status progress siteUrl isLocked isSubscribed likeCount isLiked isPinned createdAt user{id name bannerImage avatar{medium large}}media{id title{english romaji native userPreferred}bannerImage coverImage{medium large}}replies{id userId activityId text(asHtml:true)likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}likes{id name bannerImage avatar{medium large}}}... on MessageActivity{id recipientId messengerId type replyCount likeCount message(asHtml:true)isLocked isSubscribed isLiked isPrivate siteUrl createdAt recipient{id name bannerImage avatar{medium large}}messenger{id name bannerImage avatar{medium large}}replies{id userId activityId text(asHtml:true)likeCount isLiked createdAt user{id name bannerImage avatar{medium large}}likes{id name bannerImage avatar{medium large}}}likes{id name bannerImage avatar{medium large}}}}}}""",
force = true
)
}
suspend fun isUserFav(favType: AnilistMutations.FavType, id: Int): Boolean { //anilist isFavourite is broken, so we need to check it manually
val res = getUserProfile(Anilist.userid?: return false)
return when (favType) {
AnilistMutations.FavType.ANIME -> res?.data?.user?.favourites?.anime?.nodes?.any { it.id == id } ?: false
AnilistMutations.FavType.MANGA -> res?.data?.user?.favourites?.manga?.nodes?.any { it.id == id } ?: false
AnilistMutations.FavType.CHARACTER -> res?.data?.user?.favourites?.characters?.nodes?.any { it.id == id } ?: false
AnilistMutations.FavType.STAFF -> res?.data?.user?.favourites?.staff?.nodes?.any { it.id == id } ?: false
AnilistMutations.FavType.STUDIO -> res?.data?.user?.favourites?.studios?.nodes?.any { it.id == id } ?: false
}
}
companion object {
const val ITEMS_PER_PAGE = 25
}
} }

View File

@@ -5,26 +5,25 @@ import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import ani.dantotsu.BuildConfig
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.discord.Discord import ani.dantotsu.connections.discord.Discord
import ani.dantotsu.connections.mal.MAL import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.loadData
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.others.AppUpdater import ani.dantotsu.others.AppUpdater
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.tryWithSuspend import ani.dantotsu.tryWithSuspend
import ani.dantotsu.util.Logger
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
suspend fun getUserId(context: Context, block: () -> Unit) { suspend fun getUserId(context: Context, block: () -> Unit) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val sharedPref = context.getSharedPreferences( val token = PrefManager.getVal(PrefName.DiscordToken, null as String?)
context.getString(R.string.preference_file_key), val userid = PrefManager.getVal(PrefName.DiscordId, null as String?)
Context.MODE_PRIVATE
)
val token = sharedPref.getString("discord_token", null)
val userid = sharedPref.getString("discord_id", null)
if (userid == null && token != null) { if (userid == null && token != null) {
/*if (!Discord.getUserData()) /*if (!Discord.getUserData())
snackString(context.getString(R.string.error_loading_discord_user_data))*/ snackString(context.getString(R.string.error_loading_discord_user_data))*/
@@ -99,12 +98,26 @@ class AnilistHomeViewModel : ViewModel() {
fun getRecommendation(): LiveData<ArrayList<Media>> = recommendation fun getRecommendation(): LiveData<ArrayList<Media>> = recommendation
suspend fun setRecommendation() = recommendation.postValue(Anilist.query.recommendations()) suspend fun setRecommendation() = recommendation.postValue(Anilist.query.recommendations())
suspend fun initHomePage() {
val res = Anilist.query.initHomePage()
Logger.log("AnilistHomeViewModel : res=$res")
res["currentAnime"]?.let { animeContinue.postValue(it) }
res["favoriteAnime"]?.let { animeFav.postValue(it) }
res["plannedAnime"]?.let { animePlanned.postValue(it) }
res["currentManga"]?.let { mangaContinue.postValue(it) }
res["favoriteManga"]?.let { mangaFav.postValue(it) }
res["plannedManga"]?.let { mangaPlanned.postValue(it) }
res["recommendations"]?.let { recommendation.postValue(it) }
}
suspend fun loadMain(context: FragmentActivity) { suspend fun loadMain(context: FragmentActivity) {
Anilist.getSavedToken(context) Anilist.getSavedToken()
MAL.getSavedToken(context) MAL.getSavedToken(context)
Discord.getSavedToken(context) Discord.getSavedToken(context)
if (loadData<Boolean>("check_update") != false) AppUpdater.check(context) if (!BuildConfig.FLAVOR.contains("fdroid")) {
genres.postValue(Anilist.query.getGenresAndTags(context)) if (PrefManager.getVal(PrefName.CheckUpdate)) AppUpdater.check(context)
}
genres.postValue(Anilist.query.getGenresAndTags())
} }
val empty = MutableLiveData<Boolean>(null) val empty = MutableLiveData<Boolean>(null)
@@ -320,4 +333,64 @@ class GenresViewModel : ViewModel() {
} }
} }
} }
}
class ProfileViewModel : ViewModel() {
private val mangaFav: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getMangaFav(): LiveData<ArrayList<Media>> = mangaFav
private val animeFav: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
private val listImages: MutableLiveData<ArrayList<String?>> =
MutableLiveData<ArrayList<String?>>(arrayListOf())
fun getListImages(): LiveData<ArrayList<String?>> = listImages
suspend fun setData(id: Int) {
val res = Anilist.query.initProfilePage(id)
val mangaList = res?.data?.favoriteManga?.favourites?.manga?.edges?.mapNotNull {
it.node?.let { i ->
Media(i).apply { isFav = true }
}
}
mangaFav.postValue(ArrayList(mangaList ?: arrayListOf()))
val animeList = res?.data?.favoriteAnime?.favourites?.anime?.edges?.mapNotNull {
it.node?.let { i ->
Media(i).apply { isFav = true }
}
}
animeFav.postValue(ArrayList(animeList ?: arrayListOf()))
val bannerImages = arrayListOf<String?>(null, null)
val animeRandom = res?.data?.animeMediaList?.lists?.mapNotNull {
it.entries?.mapNotNull { entry ->
val imageUrl = entry.media?.bannerImage
if (imageUrl != null && imageUrl != "null") imageUrl
else null
}
}?.flatten()?.randomOrNull()
bannerImages[0] = animeRandom
val mangaRandom = res?.data?.mangaMediaList?.lists?.mapNotNull {
it.entries?.mapNotNull { entry ->
val imageUrl = entry.media?.bannerImage
if (imageUrl != null && imageUrl != "null") imageUrl
else null
}
}?.flatten()?.randomOrNull()
bannerImages[1] = mangaRandom
listImages.postValue(bannerImages)
}
fun refresh() {
mangaFav.postValue(mangaFav.value)
animeFav.postValue(animeFav.value)
listImages.postValue(listImages.value)
}
} }

View File

@@ -1,29 +1,25 @@
package ani.dantotsu.connections.anilist package ani.dantotsu.connections.anilist
import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import ani.dantotsu.logError import ani.dantotsu.logError
import ani.dantotsu.logger import ani.dantotsu.util.Logger
import ani.dantotsu.others.LangSet import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.startMainActivity import ani.dantotsu.startMainActivity
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
class Login : AppCompatActivity() { class Login : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
val data: Uri? = intent?.data val data: Uri? = intent?.data
logger(data.toString())
try { try {
Anilist.token = Anilist.token =
Regex("""(?<=access_token=).+(?=&token_type)""").find(data.toString())!!.value Regex("""(?<=access_token=).+(?=&token_type)""").find(data.toString())!!.value
val filename = "anilistToken" PrefManager.setVal(PrefName.AnilistToken, Anilist.token ?: "")
this.openFileOutput(filename, Context.MODE_PRIVATE).use {
it.write(Anilist.token!!.toByteArray())
}
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
} }

View File

@@ -5,27 +5,31 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import ani.dantotsu.loadMedia import ani.dantotsu.loadMedia
import ani.dantotsu.others.LangSet
import ani.dantotsu.startMainActivity import ani.dantotsu.startMainActivity
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
class UrlMedia : Activity() { class UrlMedia : Activity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
var id: Int? = intent?.extras?.getInt("media", 0) ?: 0 val data: Uri? = intent?.data
var isMAL = false val type = data?.pathSegments?.getOrNull(0)
var continueMedia = true if (type != "user") {
if (id == 0) { var id: Int? = intent?.extras?.getInt("media", 0) ?: 0
continueMedia = false var isMAL = false
val data: Uri? = intent?.data var continueMedia = true
isMAL = data?.host != "anilist.co" if (id == 0) {
id = data?.pathSegments?.getOrNull(1)?.toIntOrNull() continueMedia = false
} else loadMedia = id isMAL = data?.host != "anilist.co"
startMainActivity( id = data?.pathSegments?.getOrNull(1)?.toIntOrNull()
this, } else loadMedia = id
bundleOf("mediaId" to id, "mal" to isMAL, "continue" to continueMedia) startMainActivity(
) this,
bundleOf("mediaId" to id, "mal" to isMAL, "continue" to continueMedia)
)
} else {
val username = data.pathSegments?.getOrNull(1)
startMainActivity(this, bundleOf("username" to username))
}
} }
} }

View File

@@ -46,7 +46,7 @@ data class Character(
// Notes for site moderators // Notes for site moderators
@SerialName("modNotes") var modNotes: String?, @SerialName("modNotes") var modNotes: String?,
) ) : java.io.Serializable
@Serializable @Serializable
data class CharacterConnection( data class CharacterConnection(
@@ -56,7 +56,7 @@ data class CharacterConnection(
// The pagination information // The pagination information
// @SerialName("pageInfo") var pageInfo: PageInfo?, // @SerialName("pageInfo") var pageInfo: PageInfo?,
) ) : java.io.Serializable
@Serializable @Serializable
data class CharacterEdge( data class CharacterEdge(
@@ -82,7 +82,7 @@ data class CharacterEdge(
// The order the character should be displayed from the users favourites // The order the character should be displayed from the users favourites
@SerialName("favouriteOrder") var favouriteOrder: Int?, @SerialName("favouriteOrder") var favouriteOrder: Int?,
) ) : java.io.Serializable
@Serializable @Serializable
data class CharacterName( data class CharacterName(
@@ -109,7 +109,7 @@ data class CharacterName(
// The currently authenticated users preferred name language. Default romaji for non-authenticated // The currently authenticated users preferred name language. Default romaji for non-authenticated
@SerialName("userPreferred") var userPreferred: String?, @SerialName("userPreferred") var userPreferred: String?,
) ) : java.io.Serializable
@Serializable @Serializable
data class CharacterImage( data class CharacterImage(
@@ -118,4 +118,4 @@ data class CharacterImage(
// The character's image of media at medium size // The character's image of media at medium size
@SerialName("medium") var medium: String?, @SerialName("medium") var medium: String?,
) ) : java.io.Serializable

View File

@@ -105,41 +105,584 @@ class Query {
) )
} }
@Serializable
data class CombinedMediaListResponse(
@SerialName("data")
val data: Data?
) {
@Serializable
data class Data(
@SerialName("current") val current: ani.dantotsu.connections.anilist.api.MediaListCollection?,
@SerialName("planned") val planned: ani.dantotsu.connections.anilist.api.MediaListCollection?,
@SerialName("repeating") val repeating: ani.dantotsu.connections.anilist.api.MediaListCollection?,
)
}
@Serializable
data class HomePageMedia(
@SerialName("data")
val data: Data?
) {
@Serializable
data class Data(
@SerialName("currentAnime") val currentAnime: ani.dantotsu.connections.anilist.api.MediaListCollection?,
@SerialName("repeatingAnime") val repeatingAnime: ani.dantotsu.connections.anilist.api.MediaListCollection?,
@SerialName("favoriteAnime") val favoriteAnime: ani.dantotsu.connections.anilist.api.User?,
@SerialName("plannedAnime") val plannedAnime: ani.dantotsu.connections.anilist.api.MediaListCollection?,
@SerialName("currentManga") val currentManga: ani.dantotsu.connections.anilist.api.MediaListCollection?,
@SerialName("repeatingManga") val repeatingManga: ani.dantotsu.connections.anilist.api.MediaListCollection?,
@SerialName("favoriteManga") val favoriteManga: ani.dantotsu.connections.anilist.api.User?,
@SerialName("plannedManga") val plannedManga: ani.dantotsu.connections.anilist.api.MediaListCollection?,
@SerialName("recommendationQuery") val recommendationQuery: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("recommendationPlannedQueryAnime") val recommendationPlannedQueryAnime: ani.dantotsu.connections.anilist.api.MediaListCollection?,
@SerialName("recommendationPlannedQueryManga") val recommendationPlannedQueryManga: ani.dantotsu.connections.anilist.api.MediaListCollection?,
)
}
@Serializable
data class ProfilePageMedia(
@SerialName("data")
val data: Data?
) {
@Serializable
data class Data(
@SerialName("favoriteAnime") val favoriteAnime: ani.dantotsu.connections.anilist.api.User?,
@SerialName("favoriteManga") val favoriteManga: ani.dantotsu.connections.anilist.api.User?,
@SerialName("animeMediaList") val animeMediaList: ani.dantotsu.connections.anilist.api.MediaListCollection?,
@SerialName("mangaMediaList") val mangaMediaList: ani.dantotsu.connections.anilist.api.MediaListCollection?
)
}
@Serializable
data class ToggleFollow(
@SerialName("data")
val data: Data?
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("ToggleFollow")
val toggleFollow: FollowData
) : java.io.Serializable
}
@Serializable @Serializable
data class GenreCollection( data class GenreCollection(
@SerialName("data") @SerialName("data")
val data: Data val data: Data
) { ) : java.io.Serializable {
@Serializable @Serializable
data class Data( data class Data(
@SerialName("GenreCollection") @SerialName("GenreCollection")
val genreCollection: List<String>? val genreCollection: List<String>?
) ) : java.io.Serializable
} }
@Serializable @Serializable
data class MediaTagCollection( data class MediaTagCollection(
@SerialName("data") @SerialName("data")
val data: Data val data: Data
) { ) : java.io.Serializable {
@Serializable @Serializable
data class Data( data class Data(
@SerialName("MediaTagCollection") @SerialName("MediaTagCollection")
val mediaTagCollection: List<MediaTag>? val mediaTagCollection: List<MediaTag>?
) ) : java.io.Serializable
} }
@Serializable @Serializable
data class User( data class User(
@SerialName("data") @SerialName("data")
val data: Data val data: Data
) { ) : java.io.Serializable {
@Serializable @Serializable
data class Data( data class Data(
@SerialName("User") @SerialName("User")
val user: ani.dantotsu.connections.anilist.api.User? val user: ani.dantotsu.connections.anilist.api.User?
) ) : java.io.Serializable
} }
@Serializable
data class UserProfileResponse(
@SerialName("data")
val data: Data
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("followerPage")
val followerPage: UserProfilePage?,
@SerialName("followingPage")
val followingPage: UserProfilePage?,
@SerialName("user")
val user: UserProfile?
) : java.io.Serializable
}
@Serializable
data class UserProfilePage(
@SerialName("pageInfo")
val pageInfo: PageInfo,
) : java.io.Serializable
@Serializable
data class Following(
@SerialName("data")
val data: Data
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("Page")
val page: FollowingPage?
) : java.io.Serializable
}
@Serializable
data class Follower(
@SerialName("data")
val data: Data
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("Page")
val page: FollowerPage?
) : java.io.Serializable
}
@Serializable
data class FollowerPage(
@SerialName("followers")
val followers: List<ani.dantotsu.connections.anilist.api.User>?
) : java.io.Serializable
@Serializable
data class FollowingPage(
@SerialName("following")
val following: List<ani.dantotsu.connections.anilist.api.User>?
) : java.io.Serializable
@Serializable
data class UserProfile(
@SerialName("id")
val id: Int,
@SerialName("name")
val name: String,
@SerialName("about")
val about: String?,
@SerialName("avatar")
val avatar: UserAvatar?,
@SerialName("bannerImage")
val bannerImage: String?,
@SerialName("isFollowing")
var isFollowing: Boolean,
@SerialName("isFollower")
val isFollower: Boolean,
@SerialName("isBlocked")
val isBlocked: Boolean,
@SerialName("favourites")
val favourites: UserFavourites?,
@SerialName("statistics")
val statistics: NNUserStatisticTypes,
@SerialName("siteUrl")
val siteUrl: String,
): java.io.Serializable
@Serializable
data class NNUserStatisticTypes(
@SerialName("anime") var anime: NNUserStatistics,
@SerialName("manga") var manga: NNUserStatistics
): java.io.Serializable
@Serializable
data class NNUserStatistics(
@SerialName("count") var count: Int,
@SerialName("meanScore") var meanScore: Float,
@SerialName("standardDeviation") var standardDeviation: Float,
@SerialName("minutesWatched") var minutesWatched: Int,
@SerialName("episodesWatched") var episodesWatched: Int,
@SerialName("chaptersRead") var chaptersRead: Int,
@SerialName("volumesRead") var volumesRead: Int,
): java.io.Serializable
@Serializable
data class UserFavourites(
@SerialName("anime")
val anime: UserMediaFavouritesCollection,
@SerialName("manga")
val manga: UserMediaFavouritesCollection,
@SerialName("characters")
val characters: UserCharacterFavouritesCollection,
@SerialName("staff")
val staff: UserStaffFavouritesCollection,
@SerialName("studios")
val studios: UserStudioFavouritesCollection,
): java.io.Serializable
@Serializable
data class UserMediaFavouritesCollection(
@SerialName("nodes")
val nodes: List<UserMediaImageFavorite>,
): java.io.Serializable
@Serializable
data class UserMediaImageFavorite(
@SerialName("id")
val id: Int,
@SerialName("coverImage")
val coverImage: MediaCoverImage
): java.io.Serializable
@Serializable
data class UserCharacterFavouritesCollection(
@SerialName("nodes")
val nodes: List<UserCharacterImageFavorite>,
): java.io.Serializable
@Serializable
data class UserCharacterImageFavorite(
@SerialName("id")
val id: Int,
@SerialName("name")
val name: CharacterName,
@SerialName("image")
val image: CharacterImage,
@SerialName("isFavourite")
val isFavourite: Boolean
): java.io.Serializable
@Serializable
data class UserStaffFavouritesCollection(
@SerialName("nodes")
val nodes: List<UserCharacterImageFavorite>, //downstream it's the same as character
): java.io.Serializable
@Serializable
data class UserStudioFavouritesCollection(
@SerialName("nodes")
val nodes: List<UserStudioFavorite>,
): java.io.Serializable
@Serializable
data class UserStudioFavorite(
@SerialName("id")
val id: Int,
@SerialName("name")
val name: String,
): java.io.Serializable
//----------------------------------------
// Statistics
@Serializable
data class StatisticsResponse(
@SerialName("data")
val data: Data
): java.io.Serializable {
@Serializable
data class Data(
@SerialName("User")
val user: StatisticsUser?
): java.io.Serializable
}
@Serializable
data class StatisticsUser(
@SerialName("id")
val id: Int,
@SerialName("name")
val name: String,
@SerialName("mediaListOptions")
val mediaListOptions: MediaListOptions,
@SerialName("statistics")
val statistics: StatisticsTypes
) : java.io.Serializable
@Serializable
data class StatisticsTypes(
@SerialName("anime")
val anime: Statistics,
@SerialName("manga")
val manga: Statistics
) : java.io.Serializable
@Serializable
data class Statistics(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("standardDeviation")
val standardDeviation: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("episodesWatched")
val episodesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("volumesRead")
val volumesRead: Int,
@SerialName("formats")
val formats: List<StatisticsFormat>,
@SerialName("statuses")
val statuses: List<StatisticsStatus>,
@SerialName("scores")
val scores: List<StatisticsScore>,
@SerialName("lengths")
val lengths: List<StatisticsLength>,
@SerialName("releaseYears")
val releaseYears: List<StatisticsReleaseYear>,
@SerialName("startYears")
val startYears: List<StatisticsStartYear>,
@SerialName("genres")
val genres: List<StatisticsGenre>,
@SerialName("tags")
val tags: List<StatisticsTag>,
@SerialName("countries")
val countries: List<StatisticsCountry>,
@SerialName("voiceActors")
val voiceActors: List<StatisticsVoiceActor>,
@SerialName("staff")
val staff: List<StatisticsStaff>,
@SerialName("studios")
val studios: List<StatisticsStudio>
) : java.io.Serializable
@Serializable
data class StatisticsFormat(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("format")
val format: String
) : java.io.Serializable
@Serializable
data class StatisticsStatus(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("status")
val status: String
) : java.io.Serializable
@Serializable
data class StatisticsScore(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("score")
val score: Int
) : java.io.Serializable
@Serializable
data class StatisticsLength(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("length")
val length: String? //can be null for manga
) : java.io.Serializable
@Serializable
data class StatisticsReleaseYear(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("releaseYear")
val releaseYear: Int
) : java.io.Serializable
@Serializable
data class StatisticsStartYear(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("startYear")
val startYear: Int
) : java.io.Serializable
@Serializable
data class StatisticsGenre(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("genre")
val genre: String
) : java.io.Serializable
@Serializable
data class StatisticsTag(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("tag")
val tag: Tag
) : java.io.Serializable
@Serializable
data class Tag(
@SerialName("id")
val id: Int,
@SerialName("name")
val name: String
) : java.io.Serializable
@Serializable
data class StatisticsCountry(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("country")
val country: String
) : java.io.Serializable
@Serializable
data class StatisticsVoiceActor(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("voiceActor")
val voiceActor: VoiceActor,
@SerialName("characterIds")
val characterIds: List<Int>
) : java.io.Serializable
@Serializable
data class VoiceActor(
@SerialName("id")
val id: Int,
@SerialName("name")
val name: StaffName
) : java.io.Serializable
@Serializable
data class StaffName(
@SerialName("first")
val first: String?,
@SerialName("middle")
val middle: String?,
@SerialName("last")
val last: String?,
@SerialName("full")
val full: String?,
@SerialName("native")
val native: String?,
@SerialName("alternative")
val alternative: List<String>?,
@SerialName("userPreferred")
val userPreferred: String?
) : java.io.Serializable
@Serializable
data class StatisticsStaff(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("staff")
val staff: VoiceActor
) : java.io.Serializable
@Serializable
data class StatisticsStudio(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("studio")
val studio: StatStudio
) : java.io.Serializable
@Serializable
data class StatStudio(
@SerialName("id")
val id: Int,
@SerialName("name")
val name: String,
@SerialName("isAnimationStudio")
val isAnimationStudio: Boolean
) : java.io.Serializable
} }
//data class WhaData( //data class WhaData(
@@ -169,7 +712,7 @@ class Query {
// // Activity reply query // // Activity reply query
// val ActivityReply: ActivityReply?, // val ActivityReply: ActivityReply?,
// // Comment query // // CommentNotificationWorker query
// val ThreadComment: List<ThreadComment>?, // val ThreadComment: List<ThreadComment>?,
// // Notification query // // Notification query

View File

@@ -0,0 +1,114 @@
package ani.dantotsu.connections.anilist.api
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class FeedResponse(
@SerialName("data")
val data: Data
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("Page")
val page: ActivityPage
) : java.io.Serializable
}
@Serializable
data class ActivityPage(
@SerialName("activities")
val activities: List<Activity>
) : java.io.Serializable
@Serializable
data class Activity(
@SerialName("__typename")
val typename: String,
@SerialName("id")
val id: Int,
@SerialName("recipientId")
val recipientId: Int?,
@SerialName("messengerId")
val messengerId: Int?,
@SerialName("userId")
val userId: Int?,
@SerialName("type")
val type: String,
@SerialName("replyCount")
val replyCount: Int,
@SerialName("status")
val status: String?,
@SerialName("progress")
val progress: String?,
@SerialName("text")
val text: String?,
@SerialName("message")
val message: String?,
@SerialName("siteUrl")
val siteUrl: String?,
@SerialName("isLocked")
val isLocked: Boolean,
@SerialName("isSubscribed")
val isSubscribed: Boolean,
@SerialName("likeCount")
var likeCount: Int?,
@SerialName("isLiked")
var isLiked: Boolean?,
@SerialName("isPinned")
val isPinned: Boolean?,
@SerialName("isPrivate")
val isPrivate: Boolean?,
@SerialName("createdAt")
val createdAt: Int,
@SerialName("user")
val user: User?,
@SerialName("recipient")
val recipient: User?,
@SerialName("messenger")
val messenger: User?,
@SerialName("media")
val media: Media?,
@SerialName("replies")
val replies: List<ActivityReply>?,
@SerialName("likes")
val likes: List<User>?,
) : java.io.Serializable
@Serializable
data class ActivityReply(
@SerialName("id")
val id: Int,
@SerialName("userId")
val userId: Int,
@SerialName("text")
val text: String,
@SerialName("likeCount")
val likeCount: Int,
@SerialName("isLiked")
val isLiked: Boolean,
@SerialName("createdAt")
val createdAt: Int,
@SerialName("user")
val user: User,
@SerialName("likes")
val likes: List<User>?,
) : java.io.Serializable
@Serializable
data class ToggleLike(
@SerialName("data")
val data: Data
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("ToggleLikeV2")
val toggleLike: LikeData
) : java.io.Serializable
}
@Serializable
data class LikeData(
@SerialName("__typename")
val typename: String
) : java.io.Serializable

View File

@@ -251,7 +251,7 @@ data class MediaCoverImage(
// Average #hex color of cover image // Average #hex color of cover image
@SerialName("color") var color: String?, @SerialName("color") var color: String?,
) ) : java.io.Serializable
@Serializable @Serializable
data class MediaList( data class MediaList(
@@ -490,7 +490,7 @@ data class MediaExternalLink(
// isDisabled: Boolean // isDisabled: Boolean
@SerialName("notes") var notes: String?, @SerialName("notes") var notes: String?,
) ) : java.io.Serializable
@Serializable @Serializable
enum class ExternalLinkType { enum class ExternalLinkType {
@@ -512,7 +512,13 @@ data class MediaListCollection(
// If there is another chunk // If there is another chunk
@SerialName("hasNextChunk") var hasNextChunk: Boolean?, @SerialName("hasNextChunk") var hasNextChunk: Boolean?,
) ) : java.io.Serializable
@Serializable
data class FollowData(
@SerialName("id") var id: Int,
@SerialName("isFollowing") var isFollowing: Boolean,
) : java.io.Serializable
@Serializable @Serializable
data class MediaListGroup( data class MediaListGroup(
@@ -526,4 +532,4 @@ data class MediaListGroup(
@SerialName("isSplitCompletedList") var isSplitCompletedList: Boolean?, @SerialName("isSplitCompletedList") var isSplitCompletedList: Boolean?,
@SerialName("status") var status: MediaListStatus?, @SerialName("status") var status: MediaListStatus?,
) ) : java.io.Serializable

View File

@@ -0,0 +1,122 @@
package ani.dantotsu.connections.anilist.api
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
enum class NotificationType(val value: String) {
ACTIVITY_MESSAGE("ACTIVITY_MESSAGE"),
ACTIVITY_REPLY("ACTIVITY_REPLY"),
FOLLOWING("FOLLOWING"),
ACTIVITY_MENTION("ACTIVITY_MENTION"),
THREAD_COMMENT_MENTION("THREAD_COMMENT_MENTION"),
THREAD_SUBSCRIBED("THREAD_SUBSCRIBED"),
THREAD_COMMENT_REPLY("THREAD_COMMENT_REPLY"),
AIRING("AIRING"),
ACTIVITY_LIKE("ACTIVITY_LIKE"),
ACTIVITY_REPLY_LIKE("ACTIVITY_REPLY_LIKE"),
THREAD_LIKE("THREAD_LIKE"),
THREAD_COMMENT_LIKE("THREAD_COMMENT_LIKE"),
ACTIVITY_REPLY_SUBSCRIBED("ACTIVITY_REPLY_SUBSCRIBED"),
RELATED_MEDIA_ADDITION("RELATED_MEDIA_ADDITION"),
MEDIA_DATA_CHANGE("MEDIA_DATA_CHANGE"),
MEDIA_MERGE("MEDIA_MERGE"),
MEDIA_DELETION("MEDIA_DELETION"),
//custom
COMMENT_REPLY("COMMENT_REPLY"),
}
@Serializable
data class NotificationResponse(
@SerialName("data")
val data: Data,
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("User")
val user: NotificationUser,
@SerialName("Page")
val page: NotificationPage,
) : java.io.Serializable
}
@Serializable
data class NotificationUser(
@SerialName("unreadNotificationCount")
var unreadNotificationCount: Int,
) : java.io.Serializable
@Serializable
data class NotificationPage(
@SerialName("pageInfo")
val pageInfo: PageInfo,
@SerialName("notifications")
val notifications: List<Notification>,
) : java.io.Serializable
@Serializable
data class Notification(
@SerialName("__typename")
val typename: String,
@SerialName("id")
val id: Int,
@SerialName("userId")
val userId: Int? = null,
@SerialName("CommentId")
val commentId: Int?,
@SerialName("type")
val notificationType: String,
@SerialName("activityId")
val activityId: Int? = null,
@SerialName("animeId")
val mediaId: Int? = null,
@SerialName("episode")
val episode: Int? = null,
@SerialName("contexts")
val contexts: List<String>? = null,
@SerialName("context")
val context: String? = null,
@SerialName("reason")
val reason: String? = null,
@SerialName("deletedMediaTitle")
val deletedMediaTitle: String? = null,
@SerialName("deletedMediaTitles")
val deletedMediaTitles: List<String>? = null,
@SerialName("createdAt")
val createdAt: Int,
@SerialName("media")
val media: ani.dantotsu.connections.anilist.api.Media? = null,
@SerialName("user")
val user: ani.dantotsu.connections.anilist.api.User? = null,
@SerialName("message")
val message: MessageActivity? = null,
@SerialName("activity")
val activity: ActivityUnion? = null,
@SerialName("Thread")
val thread: Thread? = null,
@SerialName("comment")
val comment: ThreadComment? = null,
) : java.io.Serializable
@Serializable
data class MessageActivity(
@SerialName("id")
val id: Int?,
) : java.io.Serializable
@Serializable
data class ActivityUnion(
@SerialName("id")
val id: Int?,
) : java.io.Serializable
@Serializable
data class Thread(
@SerialName("id")
val id: Int?,
) : java.io.Serializable
@Serializable
data class ThreadComment(
@SerialName("id")
val id: Int?,
) : java.io.Serializable

View File

@@ -15,7 +15,7 @@ data class Staff(
@SerialName("languageV2") var languageV2: String?, @SerialName("languageV2") var languageV2: String?,
// The staff images // The staff images
// @SerialName("image") var image: StaffImage?, @SerialName("image") var image: StaffImage?,
// A general description of the staff member // A general description of the staff member
@SerialName("description") var description: String?, @SerialName("description") var description: String?,
@@ -93,7 +93,14 @@ data class StaffConnection(
// The pagination information // The pagination information
// @SerialName("pageInfo") var pageInfo: PageInfo?, // @SerialName("pageInfo") var pageInfo: PageInfo?,
) )
@Serializable
data class StaffImage(
// The character's image of media at its largest size
@SerialName("large") var large: String?,
// The character's image of media at medium size
@SerialName("medium") var medium: String?,
) : java.io.Serializable
@Serializable @Serializable
data class StaffEdge( data class StaffEdge(
var role: String?, var role: String?,

View File

@@ -46,7 +46,7 @@ data class User(
@SerialName("statistics") var statistics: UserStatisticTypes?, @SerialName("statistics") var statistics: UserStatisticTypes?,
// The number of unread notifications the user has // The number of unread notifications the user has
// @SerialName("unreadNotificationCount") var unreadNotificationCount: Int?, @SerialName("unreadNotificationCount") var unreadNotificationCount: Int?,
// The url for the user page on the AniList website // The url for the user page on the AniList website
// @SerialName("siteUrl") var siteUrl: String?, // @SerialName("siteUrl") var siteUrl: String?,
@@ -111,7 +111,7 @@ data class UserAvatar(
// The avatar of user at medium size // The avatar of user at medium size
@SerialName("medium") var medium: String?, @SerialName("medium") var medium: String?,
) ): java.io.Serializable
@Serializable @Serializable
data class UserStatisticTypes( data class UserStatisticTypes(
@@ -164,7 +164,7 @@ data class Favourites(
@Serializable @Serializable
data class MediaListOptions( data class MediaListOptions(
// The score format the user is using for media lists // The score format the user is using for media lists
// @SerialName("scoreFormat") var scoreFormat: ScoreFormat?, @SerialName("scoreFormat") var scoreFormat: String?,
// The default order list rows should be displayed in // The default order list rows should be displayed in
@SerialName("rowOrder") var rowOrder: String?, @SerialName("rowOrder") var rowOrder: String?,

View File

@@ -0,0 +1,554 @@
package ani.dantotsu.connections.comments
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.toast
import com.lagradost.nicehttp.NiceResponse
import com.lagradost.nicehttp.Requests
import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okio.IOException
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
object CommentsAPI {
val address: String = "https://1224665.xyz:443"
var authToken: String? = null
var userId: String? = null
var isBanned: Boolean = false
var isAdmin: Boolean = false
var isMod: Boolean = false
var totalVotes: Int = 0
suspend fun getCommentsForId(id: Int, page: Int = 1, tag: Int?, sort: String?): CommentResponse? {
var url = "$address/comments/$id/$page"
val request = requestBuilder()
tag?.let {
url += "?tag=$it"
}
sort?.let {
url += if (tag != null) "&sort=$it" else "?sort=$it"
}
val json = try {
request.get(url)
} catch (e: IOException) {
snackString("Failed to fetch comments")
return null
}
if (!json.text.startsWith("{")) return null
val res = json.code == 200
if (!res && json.code != 404) {
errorReason(json.code, json.text)
}
val parsed = try {
Json.decodeFromString<CommentResponse>(json.text)
} catch (e: Exception) {
return null
}
return parsed
}
suspend fun getRepliesFromId(id: Int, page: Int = 1): CommentResponse? {
val url = "$address/comments/parent/$id/$page"
val request = requestBuilder()
val json = try {
request.get(url)
} catch (e: IOException) {
snackString("Failed to fetch comments")
return null
}
if (!json.text.startsWith("{")) return null
val res = json.code == 200
if (!res && json.code != 404) {
errorReason(json.code, json.text)
}
val parsed = try {
Json.decodeFromString<CommentResponse>(json.text)
} catch (e: Exception) {
return null
}
return parsed
}
suspend fun getSingleComment(id: Int): Comment? {
val url = "$address/comments/$id"
val request = requestBuilder()
val json = try {
request.get(url)
} catch (e: IOException) {
snackString("Failed to fetch comment")
return null
}
if (!json.text.startsWith("{")) return null
val res = json.code == 200
if (!res && json.code != 404) {
errorReason(json.code, json.text)
}
val parsed = try {
Json.decodeFromString<Comment>(json.text)
} catch (e: Exception) {
return null
}
return parsed
}
suspend fun vote(commentId: Int, voteType: Int): Boolean {
val url = "$address/comments/vote/$commentId/$voteType"
val request = requestBuilder()
val json = try {
request.post(url)
} catch (e: IOException) {
snackString("Failed to vote")
return false
}
val res = json.code == 200
if (!res) {
errorReason(json.code, json.text)
}
return res
}
suspend fun comment(mediaId: Int, parentCommentId: Int?, content: String, tag: Int?): Comment? {
val url = "$address/comments"
val body = FormBody.Builder()
.add("user_id", userId ?: return null)
.add("media_id", mediaId.toString())
.add("content", content)
if (tag != null) {
body.add("tag", tag.toString())
}
parentCommentId?.let {
body.add("parent_comment_id", it.toString())
}
val request = requestBuilder()
val json = try {
request.post(url, requestBody = body.build())
} catch (e: IOException) {
snackString("Failed to comment")
return null
}
val res = json.code == 200
if (!res) {
errorReason(json.code, json.text)
return null
}
val parsed = try {
Json.decodeFromString<ReturnedComment>(json.text)
} catch (e: Exception) {
snackString("Failed to parse comment")
return null
}
return Comment(
parsed.id,
parsed.userId,
parsed.mediaId,
parsed.parentCommentId,
parsed.content,
parsed.timestamp,
parsed.deleted,
parsed.tag,
0,
0,
null,
Anilist.username ?: "",
Anilist.avatar,
totalVotes = totalVotes
)
}
suspend fun deleteComment(commentId: Int): Boolean {
val url = "$address/comments/$commentId"
val request = requestBuilder()
val json = try {
request.delete(url)
} catch (e: IOException) {
snackString("Failed to delete comment")
return false
}
val res = json.code == 200
if (!res) {
errorReason(json.code, json.text)
}
return res
}
suspend fun editComment(commentId: Int, content: String): Boolean {
val url = "$address/comments/$commentId"
val body = FormBody.Builder()
.add("content", content)
.build()
val request = requestBuilder()
val json = try {
request.put(url, requestBody = body)
} catch (e: IOException) {
snackString("Failed to edit comment")
return false
}
val res = json.code == 200
if (!res) {
errorReason(json.code, json.text)
}
return res
}
suspend fun banUser(userId: String): Boolean {
val url = "$address/ban/$userId"
val request = requestBuilder()
val json = try {
request.post(url)
} catch (e: IOException) {
snackString("Failed to ban user")
return false
}
val res = json.code == 200
if (!res) {
errorReason(json.code, json.text)
}
return res
}
suspend fun reportComment(
commentId: Int,
username: String,
mediaTitle: String,
reportedId: String
): Boolean {
val url = "$address/report/$commentId"
val body = FormBody.Builder()
.add("username", username)
.add("mediaName", mediaTitle)
.add("reporter", Anilist.username ?: "unknown")
.add("reportedId", reportedId)
.build()
val request = requestBuilder()
val json = try {
request.post(url, requestBody = body)
} catch (e: IOException) {
snackString("Failed to report comment")
return false
}
val res = json.code == 200
if (!res) {
errorReason(json.code, json.text)
}
return res
}
suspend fun getNotifications(client: OkHttpClient): NotificationResponse? {
val url = "$address/notification/reply"
val request = requestBuilder(client)
val json = try {
request.get(url)
} catch (e: IOException) {
return null
}
if (!json.text.startsWith("{")) return null
val res = json.code == 200
if (!res) {
return null
}
val parsed = try {
Json.decodeFromString<NotificationResponse>(json.text)
} catch (e: Exception) {
return null
}
return parsed
}
private suspend fun getUserDetails(client: OkHttpClient? = null): User? {
val url = "$address/user"
val request = if (client != null) requestBuilder(client) else requestBuilder()
val json = try {
request.get(url)
} catch (e: IOException) {
return null
}
if (json.code == 200) {
val parsed = try {
Json.decodeFromString<UserResponse>(json.text)
} catch (e: Exception) {
e.printStackTrace()
return null
}
isBanned = parsed.user.isBanned ?: false
isAdmin = parsed.user.isAdmin ?: false
isMod = parsed.user.isMod ?: false
totalVotes = parsed.user.totalVotes
return parsed.user
}
return null
}
suspend fun fetchAuthToken(client: OkHttpClient? = null) {
if (authToken != null) return
val MAX_RETRIES = 5
val tokenLifetime: Long = 1000 * 60 * 60 * 24 * 6 // 6 days
val tokenExpiry = PrefManager.getVal<Long>(PrefName.CommentTokenExpiry)
if (tokenExpiry < System.currentTimeMillis() + tokenLifetime) {
val commentResponse =
PrefManager.getNullableVal<AuthResponse>(PrefName.CommentAuthResponse, null)
if (commentResponse != null) {
authToken = commentResponse.authToken
userId = commentResponse.user.id
isBanned = commentResponse.user.isBanned ?: false
isAdmin = commentResponse.user.isAdmin ?: false
isMod = commentResponse.user.isMod ?: false
totalVotes = commentResponse.user.totalVotes
if (getUserDetails(client) != null) return
}
}
val url = "$address/authenticate"
val token = PrefManager.getVal(PrefName.AnilistToken, null as String?) ?: return
repeat(MAX_RETRIES) {
try {
val json = authRequest(token, url, client)
if (json.code == 200) {
if (!json.text.startsWith("{")) throw IOException("Invalid response")
val parsed = try {
Json.decodeFromString<AuthResponse>(json.text)
} catch (e: Exception) {
snackString("Failed to login to comments API: ${e.printStackTrace()}")
return
}
PrefManager.setVal(PrefName.CommentAuthResponse, parsed)
PrefManager.setVal(
PrefName.CommentTokenExpiry,
System.currentTimeMillis() + tokenLifetime
)
authToken = parsed.authToken
userId = parsed.user.id
isBanned = parsed.user.isBanned ?: false
isAdmin = parsed.user.isAdmin ?: false
isMod = parsed.user.isMod ?: false
totalVotes = parsed.user.totalVotes
return
} else if (json.code != 429) {
errorReason(json.code, json.text)
return
}
} catch (e: IOException) {
snackString("Failed to login to comments API")
return
}
kotlinx.coroutines.delay(60000)
}
snackString("Failed to login after multiple attempts")
}
private suspend fun authRequest(
token: String,
url: String,
client: OkHttpClient? = null
): NiceResponse {
val body: FormBody = FormBody.Builder()
.add("token", token)
.build()
val request = if (client != null) requestBuilder(client) else requestBuilder()
return request.post(url, requestBody = body)
}
private fun headerBuilder(): Map<String, String> {
val map = mutableMapOf(
"appauth" to "6*45Qp%W2RS@t38jkXoSKY588Ynj%n"
)
if (authToken != null) {
map["Authorization"] = authToken!!
}
return map
}
private fun requestBuilder(client: OkHttpClient = Injekt.get<NetworkHelper>().client): Requests {
return Requests(
client,
headerBuilder()
)
}
private fun errorReason(code: Int, reason: String? = null) {
val error = when (code) {
429 -> "Rate limited. :("
else -> "Failed to connect"
}
val parsed = try {
Json.decodeFromString<ErrorResponse>(reason!!)
} catch (e: Exception) {
null
}
val message = parsed?.message ?: reason ?: error
val fullMessage = if(code == 500) message else "$code: $message"
toast(fullMessage)
}
}
@Serializable
data class ErrorResponse(
@SerialName("message")
val message: String
)
@Serializable
data class NotificationResponse(
@SerialName("notifications")
val notifications: List<Notification>
)
@Serializable
data class Notification(
@SerialName("username")
val username: String,
@SerialName("media_id")
val mediaId: Int,
@SerialName("comment_id")
val commentId: Int,
@SerialName("type")
val type: Int? = null,
@SerialName("content")
val content: String? = null,
@SerialName("notification_id")
val notificationId: Int
)
@Serializable
data class AuthResponse(
@SerialName("authToken")
val authToken: String,
@SerialName("user")
val user: User
) : java.io.Serializable {
companion object {
private const val serialVersionUID: Long = 1
}
}
@Serializable
data class UserResponse(
@SerialName("user")
val user: User
)
@Serializable
data class User(
@SerialName("user_id")
val id: String,
@SerialName("username")
val username: String,
@SerialName("profile_picture_url")
val profilePictureUrl: String? = null,
@SerialName("is_banned")
@Serializable(with = NumericBooleanSerializer::class)
val isBanned: Boolean? = null,
@SerialName("is_mod")
@Serializable(with = NumericBooleanSerializer::class)
val isAdmin: Boolean? = null,
@SerialName("is_admin")
@Serializable(with = NumericBooleanSerializer::class)
val isMod: Boolean? = null,
@SerialName("total_votes")
val totalVotes: Int,
@SerialName("warnings")
val warnings: Int
) : java.io.Serializable {
companion object {
private const val serialVersionUID: Long = 1
}
}
@Serializable
data class CommentResponse(
@SerialName("comments")
val comments: List<Comment>,
@SerialName("totalPages")
val totalPages: Int
)
@Serializable
data class Comment(
@SerialName("comment_id")
val commentId: Int,
@SerialName("user_id")
val userId: String,
@SerialName("media_id")
val mediaId: Int,
@SerialName("parent_comment_id")
val parentCommentId: Int?,
@SerialName("content")
var content: String,
@SerialName("timestamp")
var timestamp: String,
@SerialName("deleted")
@Serializable(with = NumericBooleanSerializer::class)
val deleted: Boolean?,
@SerialName("tag")
val tag: Int?,
@SerialName("upvotes")
var upvotes: Int,
@SerialName("downvotes")
var downvotes: Int,
@SerialName("user_vote_type")
var userVoteType: Int?,
@SerialName("username")
val username: String,
@SerialName("profile_picture_url")
val profilePictureUrl: String?,
@SerialName("is_mod")
@Serializable(with = NumericBooleanSerializer::class)
val isMod: Boolean? = null,
@SerialName("is_admin")
@Serializable(with = NumericBooleanSerializer::class)
val isAdmin: Boolean? = null,
@SerialName("reply_count")
val replyCount: Int? = null,
@SerialName("total_votes")
val totalVotes: Int
)
@Serializable
data class ReturnedComment(
@SerialName("id")
var id: Int,
@SerialName("comment_id")
var commentId: Int?,
@SerialName("user_id")
val userId: String,
@SerialName("media_id")
val mediaId: Int,
@SerialName("parent_comment_id")
val parentCommentId: Int? = null,
@SerialName("content")
val content: String,
@SerialName("timestamp")
val timestamp: String,
@SerialName("deleted")
@Serializable(with = NumericBooleanSerializer::class)
val deleted: Boolean?,
@SerialName("tag")
val tag: Int? = null,
)
object NumericBooleanSerializer : KSerializer<Boolean> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("NumericBoolean", PrimitiveKind.INT)
override fun serialize(encoder: Encoder, value: Boolean) {
encoder.encodeInt(if (value) 1 else 0)
}
override fun deserialize(decoder: Decoder): Boolean {
return decoder.decodeInt() != 0
}
}

View File

@@ -0,0 +1,12 @@
package ani.dantotsu.connections.crashlytics
import android.content.Context
interface CrashlyticsInterface {
fun initialize(context: Context)
fun logException(e: Throwable)
fun log(message: String)
fun setUserId(id: String)
fun setCustomKey(key: String, value: String)
fun setCrashlyticsCollectionEnabled(enabled: Boolean)
}

View File

@@ -0,0 +1,30 @@
package ani.dantotsu.connections.crashlytics
import android.content.Context
import ani.dantotsu.util.Logger
class CrashlyticsStub : CrashlyticsInterface {
override fun initialize(context: Context) {
//no-op
}
override fun logException(e: Throwable) {
Logger.log(e)
}
override fun log(message: String) {
Logger.log(message)
}
override fun setUserId(id: String) {
//no-op
}
override fun setCustomKey(key: String, value: String) {
//no-op
}
override fun setCrashlyticsCollectionEnabled(enabled: Boolean) {
//no-op
}
}

View File

@@ -3,9 +3,10 @@ package ani.dantotsu.connections.discord
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.widget.TextView import android.widget.TextView
import androidx.core.content.edit
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.others.CustomBottomDialog import ani.dantotsu.others.CustomBottomDialog
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.toast import ani.dantotsu.toast
import ani.dantotsu.tryWith import ani.dantotsu.tryWith
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
@@ -18,37 +19,20 @@ object Discord {
var userid: String? = null var userid: String? = null
var avatar: String? = null var avatar: String? = null
const val TOKEN = "discord_token"
fun getSavedToken(context: Context): Boolean { fun getSavedToken(context: Context): Boolean {
val sharedPref = context.getSharedPreferences( token = PrefManager.getVal(
context.getString(R.string.preference_file_key), PrefName.DiscordToken, null as String?
Context.MODE_PRIVATE
) )
token = sharedPref.getString(TOKEN, null)
return token != null return token != null
} }
fun saveToken(context: Context, token: String) { fun saveToken(context: Context, token: String) {
val sharedPref = context.getSharedPreferences( PrefManager.setVal(PrefName.DiscordToken, token)
context.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
sharedPref.edit {
putString(TOKEN, token)
commit()
}
} }
fun removeSavedToken(context: Context) { fun removeSavedToken(context: Context) {
val sharedPref = context.getSharedPreferences( PrefManager.removeVal(PrefName.DiscordToken)
context.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
sharedPref.edit {
remove(TOKEN)
commit()
}
tryWith(true) { tryWith(true) {
val dir = File(context.filesDir?.parentFile, "app_webview") val dir = File(context.filesDir?.parentFile, "app_webview")
@@ -86,19 +70,5 @@ object Discord {
const val application_Id = "1163925779692912771" const val application_Id = "1163925779692912771"
const val small_Image: String = const val small_Image: String =
"mp:attachments/1167176318266380288/1176997397797277856/logo-best_of_both.png" "mp:external/GJEe4hKzr8w56IW6ZKQz43HFVEo8pOtA_C-dJiWwxKo/https/cdn.discordapp.com/app-icons/1163925779692912771/f6b42d41dfdf0b56fcc79d4a12d2ac66.png"
/*fun defaultRPC(): RPC? {
return token?.let {
RPC(it, Dispatchers.IO).apply {
applicationId = application_Id
smallImage = RPC.Link(
"Dantotsu",
small_Image
)
buttons.add(RPC.Link("Stream on Dantotsu", "https://github.com/rebelonion/Dantotsu/"))
}
}
}*/
} }

View File

@@ -15,7 +15,6 @@ import android.os.Environment
import android.os.IBinder import android.os.IBinder
import android.os.PowerManager import android.os.PowerManager
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
@@ -24,6 +23,9 @@ import ani.dantotsu.R
import ani.dantotsu.connections.discord.serializers.Presence import ani.dantotsu.connections.discord.serializers.Presence
import ani.dantotsu.connections.discord.serializers.User import ani.dantotsu.connections.discord.serializers.User
import ani.dantotsu.isOnline import ani.dantotsu.isOnline
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.util.Logger
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonObject import com.google.gson.JsonObject
import com.google.gson.JsonParser import com.google.gson.JsonParser
@@ -149,19 +151,11 @@ class DiscordService : Service() {
} }
fun saveProfile(response: String) { fun saveProfile(response: String) {
val sharedPref = baseContext.getSharedPreferences(
baseContext.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
val user = json.decodeFromString<User.Response>(response).d.user val user = json.decodeFromString<User.Response>(response).d.user
log("User data: $user") log("User data: $user")
with(sharedPref.edit()) { PrefManager.setVal(PrefName.DiscordUserName, user.username)
putString("discord_username", user.username) PrefManager.setVal(PrefName.DiscordId, user.id)
putString("discord_id", user.id) PrefManager.setVal(PrefName.DiscordAvatar, user.avatar)
putString("discord_avatar", user.avatar)
apply()
}
} }
override fun onBind(p0: Intent?): IBinder? = null override fun onBind(p0: Intent?): IBinder? = null
@@ -280,7 +274,7 @@ class DiscordService : Service() {
return return
} }
} }
t.message?.let { Log.d("WebSocket", "onFailure() $it") } t.message?.let { Logger.log("onFailure() $it") }
log("WebSocket: Error, onFailure() reason: ${t.message}") log("WebSocket: Error, onFailure() reason: ${t.message}")
client = OkHttpClient() client = OkHttpClient()
client.newWebSocket( client.newWebSocket(
@@ -295,7 +289,7 @@ class DiscordService : Service() {
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) { override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
super.onClosing(webSocket, code, reason) super.onClosing(webSocket, code, reason)
Log.d("WebSocket", "onClosing() $code $reason") Logger.log("onClosing() $code $reason")
if (::heartbeatThread.isInitialized && !heartbeatThread.isInterrupted) { if (::heartbeatThread.isInitialized && !heartbeatThread.isInterrupted) {
heartbeatThread.interrupt() heartbeatThread.interrupt()
} }
@@ -303,7 +297,7 @@ class DiscordService : Service() {
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) { override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
super.onClosed(webSocket, code, reason) super.onClosed(webSocket, code, reason)
Log.d("WebSocket", "onClosed() $code $reason") Logger.log("onClosed() $code $reason")
if (code >= 4000) { if (code >= 4000) {
log("WebSocket: Error, code: $code reason: $reason") log("WebSocket: Error, code: $code reason: $reason")
client = OkHttpClient() client = OkHttpClient()
@@ -318,17 +312,13 @@ class DiscordService : Service() {
} }
fun getToken(context: Context): String { fun getToken(context: Context): String {
val sharedPref = context.getSharedPreferences( val token = PrefManager.getVal(PrefName.DiscordToken, null as String?)
context.getString(R.string.preference_file_key), return if (token == null) {
Context.MODE_PRIVATE
)
val token = sharedPref.getString(Discord.TOKEN, null)
if (token == null) {
log("WebSocket: Token not found") log("WebSocket: Token not found")
errorNotification("Could not set the presence", "token not found") errorNotification("Could not set the presence", "token not found")
return "" ""
} else { } else {
return token token
} }
} }
@@ -392,52 +382,7 @@ class DiscordService : Service() {
} }
fun log(string: String) { fun log(string: String) {
Log.d("WebSocket_Discord", string) //Logger.log(string)
//log += "${SimpleDateFormat("HH:mm:ss").format(Calendar.getInstance().time)} $string\n"
}
fun saveLogToFile() {
val fileName = "log_${System.currentTimeMillis()}.txt"
// ContentValues to store file metadata
val values = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.MediaColumns.RELATIVE_PATH, "Download/")
}
}
// Inserting the file in the MediaStore
val resolver = baseContext.contentResolver
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
} else {
val directory =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val file = File(directory, fileName)
// Make sure the Downloads directory exists
if (!directory.exists()) {
directory.mkdirs()
}
// Use FileProvider to get the URI for the file
val authority =
"${baseContext.packageName}.provider" // Adjust with your app's package name
Uri.fromFile(file)
}
// Writing to the file
uri?.let {
resolver.openOutputStream(it).use { outputStream ->
OutputStreamWriter(outputStream).use { writer ->
writer.write(log)
}
}
} ?: run {
log("Error saving log file")
}
} }
fun resume() { fun resume() {

View File

@@ -11,7 +11,6 @@ import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.discord.Discord.saveToken import ani.dantotsu.connections.discord.Discord.saveToken
import ani.dantotsu.others.LangSet
import ani.dantotsu.startMainActivity import ani.dantotsu.startMainActivity
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
@@ -20,7 +19,7 @@ class Login : AppCompatActivity() {
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val process = getProcessName() val process = getProcessName()

View File

@@ -2,6 +2,8 @@ package ani.dantotsu.connections.discord
import ani.dantotsu.connections.discord.serializers.Activity import ani.dantotsu.connections.discord.serializers.Activity
import ani.dantotsu.connections.discord.serializers.Presence import ani.dantotsu.connections.discord.serializers.Presence
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@@ -81,7 +83,7 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
), ),
afk = true, afk = true,
since = data.startTimestamp, since = data.startTimestamp,
status = data.status status = PrefManager.getVal(PrefName.DiscordStatus)
) )
)) ))
} }

View File

@@ -8,9 +8,9 @@ import ani.dantotsu.R
import ani.dantotsu.client import ani.dantotsu.client
import ani.dantotsu.connections.mal.MAL.clientId import ani.dantotsu.connections.mal.MAL.clientId
import ani.dantotsu.connections.mal.MAL.saveResponse import ani.dantotsu.connections.mal.MAL.saveResponse
import ani.dantotsu.loadData
import ani.dantotsu.logError import ani.dantotsu.logError
import ani.dantotsu.others.LangSet import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.startMainActivity import ani.dantotsu.startMainActivity
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
@@ -21,12 +21,12 @@ import kotlinx.coroutines.launch
class Login : AppCompatActivity() { class Login : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
try { try {
val data: Uri = intent?.data val data: Uri = intent?.data
?: throw Exception(getString(R.string.mal_login_uri_not_found)) ?: throw Exception(getString(R.string.mal_login_uri_not_found))
val codeChallenge: String = loadData("malCodeChallenge", this) val codeChallenge = PrefManager.getVal(PrefName.MALCodeChallenge, null as String?)
?: throw Exception(getString(R.string.mal_login_code_challenge_not_found)) ?: throw Exception(getString(R.string.mal_login_code_challenge_not_found))
val code = data.getQueryParameter("code") val code = data.getQueryParameter("code")
?: throw Exception(getString(R.string.mal_login_code_not_present)) ?: throw Exception(getString(R.string.mal_login_code_not_present))

View File

@@ -9,13 +9,12 @@ import androidx.fragment.app.FragmentActivity
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.client import ani.dantotsu.client
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.loadData
import ani.dantotsu.openLinkInBrowser import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.saveData import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.tryWithSuspend import ani.dantotsu.tryWithSuspend
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.io.File
import java.security.SecureRandom import java.security.SecureRandom
object MAL { object MAL {
@@ -34,7 +33,7 @@ object MAL {
.replace("/", "_") .replace("/", "_")
.replace("\n", "") .replace("\n", "")
saveData("malCodeChallenge", codeChallenge, context) PrefManager.setVal(PrefName.MALCodeChallenge, codeChallenge)
val request = val request =
"https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id=$clientId&code_challenge=$codeChallenge" "https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id=$clientId&code_challenge=$codeChallenge"
try { try {
@@ -47,11 +46,9 @@ object MAL {
} }
} }
private const val MAL_TOKEN = "malToken"
private suspend fun refreshToken(): ResponseToken? { private suspend fun refreshToken(): ResponseToken? {
return tryWithSuspend { return tryWithSuspend {
val token = loadData<ResponseToken>(MAL_TOKEN) val token = PrefManager.getNullableVal<ResponseToken>(PrefName.MALToken, null)
?: throw Exception(currContext()?.getString(R.string.refresh_token_load_failed)) ?: throw Exception(currContext()?.getString(R.string.refresh_token_load_failed))
val res = client.post( val res = client.post(
"https://myanimelist.net/v1/oauth2/token", "https://myanimelist.net/v1/oauth2/token",
@@ -69,8 +66,9 @@ object MAL {
suspend fun getSavedToken(context: FragmentActivity): Boolean { suspend fun getSavedToken(context: FragmentActivity): Boolean {
return tryWithSuspend(false) { return tryWithSuspend(false) {
var res: ResponseToken = loadData(MAL_TOKEN, context) var res: ResponseToken =
?: return@tryWithSuspend false PrefManager.getNullableVal<ResponseToken>(PrefName.MALToken, null)
?: return@tryWithSuspend false
if (System.currentTimeMillis() > res.expiresIn) if (System.currentTimeMillis() > res.expiresIn)
res = refreshToken() res = refreshToken()
?: throw Exception(currContext()?.getString(R.string.refreshing_token_failed)) ?: throw Exception(currContext()?.getString(R.string.refreshing_token_failed))
@@ -84,14 +82,12 @@ object MAL {
username = null username = null
userid = null userid = null
avatar = null avatar = null
if (MAL_TOKEN in context.fileList()) { PrefManager.removeVal(PrefName.MALToken)
File(context.filesDir, MAL_TOKEN).delete()
}
} }
fun saveResponse(res: ResponseToken) { fun saveResponse(res: ResponseToken) {
res.expiresIn += System.currentTimeMillis() res.expiresIn += System.currentTimeMillis()
saveData(MAL_TOKEN, res) PrefManager.setVal(PrefName.MALToken, res)
} }
@Serializable @Serializable
@@ -100,6 +96,10 @@ object MAL {
@SerialName("expires_in") var expiresIn: Long, @SerialName("expires_in") var expiresIn: Long,
@SerialName("access_token") val accessToken: String, @SerialName("access_token") val accessToken: String,
@SerialName("refresh_token") val refreshToken: String, @SerialName("refresh_token") val refreshToken: String,
) : java.io.Serializable ) : java.io.Serializable {
companion object {
private const val serialVersionUID = 1L
}
}
} }

View File

@@ -1,25 +0,0 @@
package ani.dantotsu.download
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import ani.dantotsu.R
import ani.dantotsu.others.LangSet
import ani.dantotsu.themes.ThemeManager
class DownloadContainerActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
setContentView(R.layout.activity_container)
val fragmentClassName = intent.getStringExtra("FRAGMENT_CLASS_NAME")
val fragment = Class.forName(fragmentClassName).newInstance() as Fragment
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, fragment)
.commit()
}
}

View File

@@ -1,57 +1,56 @@
package ani.dantotsu.download package ani.dantotsu.download
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import android.os.Environment import android.os.Environment
import android.widget.Toast import android.widget.Toast
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import java.io.File import java.io.File
import java.io.Serializable import java.io.Serializable
class DownloadsManager(private val context: Context) { class DownloadsManager(private val context: Context) {
private val prefs: SharedPreferences =
context.getSharedPreferences("downloads_pref", Context.MODE_PRIVATE)
private val gson = Gson() private val gson = Gson()
private val downloadsList = loadDownloads().toMutableList() private val downloadsList = loadDownloads().toMutableList()
val mangaDownloads: List<Download> val mangaDownloadedTypes: List<DownloadedType>
get() = downloadsList.filter { it.type == Download.Type.MANGA } get() = downloadsList.filter { it.type == DownloadedType.Type.MANGA }
val animeDownloads: List<Download> val animeDownloadedTypes: List<DownloadedType>
get() = downloadsList.filter { it.type == Download.Type.ANIME } get() = downloadsList.filter { it.type == DownloadedType.Type.ANIME }
val novelDownloads: List<Download> val novelDownloadedTypes: List<DownloadedType>
get() = downloadsList.filter { it.type == Download.Type.NOVEL } get() = downloadsList.filter { it.type == DownloadedType.Type.NOVEL }
private fun saveDownloads() { private fun saveDownloads() {
val jsonString = gson.toJson(downloadsList) val jsonString = gson.toJson(downloadsList)
prefs.edit().putString("downloads_key", jsonString).apply() PrefManager.setVal(PrefName.DownloadsKeys, jsonString)
} }
private fun loadDownloads(): List<Download> { private fun loadDownloads(): List<DownloadedType> {
val jsonString = prefs.getString("downloads_key", null) val jsonString = PrefManager.getVal(PrefName.DownloadsKeys, null as String?)
return if (jsonString != null) { return if (jsonString != null) {
val type = object : TypeToken<List<Download>>() {}.type val type = object : TypeToken<List<DownloadedType>>() {}.type
gson.fromJson(jsonString, type) gson.fromJson(jsonString, type)
} else { } else {
emptyList() emptyList()
} }
} }
fun addDownload(download: Download) { fun addDownload(downloadedType: DownloadedType) {
downloadsList.add(download) downloadsList.add(downloadedType)
saveDownloads() saveDownloads()
} }
fun removeDownload(download: Download) { fun removeDownload(downloadedType: DownloadedType) {
downloadsList.remove(download) downloadsList.remove(downloadedType)
removeDirectory(download) removeDirectory(downloadedType)
saveDownloads() saveDownloads()
} }
fun removeMedia(title: String, type: Download.Type) { fun removeMedia(title: String, type: DownloadedType.Type) {
val subDirectory = if (type == Download.Type.MANGA) { val subDirectory = if (type == DownloadedType.Type.MANGA) {
"Manga" "Manga"
} else if (type == Download.Type.ANIME) { } else if (type == DownloadedType.Type.ANIME) {
"Anime" "Anime"
} else { } else {
"Novel" "Novel"
@@ -71,21 +70,33 @@ class DownloadsManager(private val context: Context) {
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show() Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
cleanDownloads() cleanDownloads()
} }
downloadsList.removeAll { it.title == title } when (type) {
DownloadedType.Type.MANGA -> {
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.MANGA }
}
DownloadedType.Type.ANIME -> {
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.ANIME }
}
DownloadedType.Type.NOVEL -> {
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.NOVEL }
}
}
saveDownloads() saveDownloads()
} }
private fun cleanDownloads() { private fun cleanDownloads() {
cleanDownload(Download.Type.MANGA) cleanDownload(DownloadedType.Type.MANGA)
cleanDownload(Download.Type.ANIME) cleanDownload(DownloadedType.Type.ANIME)
cleanDownload(Download.Type.NOVEL) cleanDownload(DownloadedType.Type.NOVEL)
} }
private fun cleanDownload(type: Download.Type) { private fun cleanDownload(type: DownloadedType.Type) {
// remove all folders that are not in the downloads list // remove all folders that are not in the downloads list
val subDirectory = if (type == Download.Type.MANGA) { val subDirectory = if (type == DownloadedType.Type.MANGA) {
"Manga" "Manga"
} else if (type == Download.Type.ANIME) { } else if (type == DownloadedType.Type.ANIME) {
"Anime" "Anime"
} else { } else {
"Novel" "Novel"
@@ -94,18 +105,18 @@ class DownloadsManager(private val context: Context) {
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$subDirectory" "Dantotsu/$subDirectory"
) )
val downloadsSubList = if (type == Download.Type.MANGA) { val downloadsSubLists = if (type == DownloadedType.Type.MANGA) {
mangaDownloads mangaDownloadedTypes
} else if (type == Download.Type.ANIME) { } else if (type == DownloadedType.Type.ANIME) {
animeDownloads animeDownloadedTypes
} else { } else {
novelDownloads novelDownloadedTypes
} }
if (directory.exists()) { if (directory.exists()) {
val files = directory.listFiles() val files = directory.listFiles()
if (files != null) { if (files != null) {
for (file in files) { for (file in files) {
if (!downloadsSubList.any { it.title == file.name }) { if (!downloadsSubLists.any { it.title == file.name }) {
val deleted = file.deleteRecursively() val deleted = file.deleteRecursively()
} }
} }
@@ -122,11 +133,11 @@ class DownloadsManager(private val context: Context) {
} }
} }
fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List<Download>) //for debugging fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List<DownloadedType>) //for debugging
{ {
val jsonString = gson.toJson(downloadsList) val jsonString = gson.toJson(downloadsList)
val file = File( val file = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/downloads.json" "Dantotsu/downloads.json"
) )
if (file.parentFile?.exists() == false) { if (file.parentFile?.exists() == false) {
@@ -138,25 +149,33 @@ class DownloadsManager(private val context: Context) {
file.writeText(jsonString) file.writeText(jsonString)
} }
fun queryDownload(download: Download): Boolean { fun queryDownload(downloadedType: DownloadedType): Boolean {
return downloadsList.contains(download) return downloadsList.contains(downloadedType)
} }
private fun removeDirectory(download: Download) { fun queryDownload(title: String, chapter: String, type: DownloadedType.Type? = null): Boolean {
val directory = if (download.type == Download.Type.MANGA) { return if (type == null) {
downloadsList.any { it.title == title && it.chapter == chapter }
} else {
downloadsList.any { it.title == title && it.chapter == chapter && it.type == type }
}
}
private fun removeDirectory(downloadedType: DownloadedType) {
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
File( File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${download.title}/${download.chapter}" "Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
) )
} else if (download.type == Download.Type.ANIME) { } else if (downloadedType.type == DownloadedType.Type.ANIME) {
File( File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Anime/${download.title}/${download.chapter}" "Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
) )
} else { } else {
File( File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Novel/${download.title}/${download.chapter}" "Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
) )
} }
@@ -173,26 +192,26 @@ class DownloadsManager(private val context: Context) {
} }
} }
fun exportDownloads(download: Download) { //copies to the downloads folder available to the user fun exportDownloads(downloadedType: DownloadedType) { //copies to the downloads folder available to the user
val directory = if (download.type == Download.Type.MANGA) { val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
File( File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${download.title}/${download.chapter}" "Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
) )
} else if (download.type == Download.Type.ANIME) { } else if (downloadedType.type == DownloadedType.Type.ANIME) {
File( File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Anime/${download.title}/${download.chapter}" "Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
) )
} else { } else {
File( File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Novel/${download.title}/${download.chapter}" "Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
) )
} }
val destination = File( val destination = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/${download.title}/${download.chapter}" "Dantotsu/${downloadedType.title}/${downloadedType.chapter}"
) )
if (directory.exists()) { if (directory.exists()) {
val copied = directory.copyRecursively(destination, true) val copied = directory.copyRecursively(destination, true)
@@ -206,10 +225,10 @@ class DownloadsManager(private val context: Context) {
} }
} }
fun purgeDownloads(type: Download.Type) { fun purgeDownloads(type: DownloadedType.Type) {
val directory = if (type == Download.Type.MANGA) { val directory = if (type == DownloadedType.Type.MANGA) {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga") File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga")
} else if (type == Download.Type.ANIME) { } else if (type == DownloadedType.Type.ANIME) {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime") File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
} else { } else {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Novel") File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Novel")
@@ -233,11 +252,56 @@ class DownloadsManager(private val context: Context) {
const val novelLocation = "Dantotsu/Novel" const val novelLocation = "Dantotsu/Novel"
const val mangaLocation = "Dantotsu/Manga" const val mangaLocation = "Dantotsu/Manga"
const val animeLocation = "Dantotsu/Anime" const val animeLocation = "Dantotsu/Anime"
fun getDirectory(
context: Context,
type: DownloadedType.Type,
title: String,
chapter: String? = null
): File {
return if (type == DownloadedType.Type.MANGA) {
if (chapter != null) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$mangaLocation/$title/$chapter"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$mangaLocation/$title"
)
}
} else if (type == DownloadedType.Type.ANIME) {
if (chapter != null) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$animeLocation/$title/$chapter"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$animeLocation/$title"
)
}
} else {
if (chapter != null) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$novelLocation/$title/$chapter"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$novelLocation/$title"
)
}
}
}
} }
} }
data class Download(val title: String, val chapter: String, val type: Type) : Serializable { data class DownloadedType(val title: String, val chapter: String, val type: Type) : Serializable {
enum class Type { enum class Type {
MANGA, MANGA,
ANIME, ANIME,

View File

@@ -0,0 +1,519 @@
package ani.dantotsu.download.anime
import android.Manifest
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.Environment
import android.os.IBinder
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.DownloadManager
import androidx.media3.exoplayer.offline.DownloadService
import ani.dantotsu.FileUrl
import ani.dantotsu.R
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.currActivity
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.video.ExoplayerDownloadService
import ani.dantotsu.download.video.Helper
import ani.dantotsu.util.Logger
import ani.dantotsu.media.Media
import ani.dantotsu.media.SubtitleDownloader
import ani.dantotsu.media.anime.AnimeWatchFragment
import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.Video
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.snackString
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SAnimeImpl
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SChapterImpl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import tachiyomi.core.util.lang.launchIO
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.util.Queue
import java.util.concurrent.ConcurrentLinkedQueue
class AnimeDownloaderService : Service() {
private lateinit var notificationManager: NotificationManagerCompat
private lateinit var builder: NotificationCompat.Builder
private val downloadsManager: DownloadsManager = Injekt.get<DownloadsManager>()
private val downloadJobs = mutableMapOf<String, Job>()
private val mutex = Mutex()
private var isCurrentlyProcessing = false
private var currentTasks: MutableList<AnimeDownloadTask> = mutableListOf()
override fun onBind(intent: Intent?): IBinder? {
// This is only required for bound services.
return null
}
override fun onCreate() {
super.onCreate()
notificationManager = NotificationManagerCompat.from(this)
builder =
NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER_PROGRESS).apply {
setContentTitle("Anime Download Progress")
setSmallIcon(R.drawable.ic_download_24)
priority = NotificationCompat.PRIORITY_DEFAULT
setOnlyAlertOnce(true)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(
NOTIFICATION_ID,
builder.build(),
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
)
} else {
startForeground(NOTIFICATION_ID, builder.build())
}
ContextCompat.registerReceiver(
this,
cancelReceiver,
IntentFilter(ACTION_CANCEL_DOWNLOAD),
ContextCompat.RECEIVER_EXPORTED
)
}
override fun onDestroy() {
super.onDestroy()
AnimeServiceDataSingleton.downloadQueue.clear()
downloadJobs.clear()
AnimeServiceDataSingleton.isServiceRunning = false
unregisterReceiver(cancelReceiver)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
snackString("Download started")
val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
serviceScope.launch {
mutex.withLock {
if (!isCurrentlyProcessing) {
isCurrentlyProcessing = true
processQueue()
isCurrentlyProcessing = false
}
}
}
return START_NOT_STICKY
}
private fun processQueue() {
CoroutineScope(Dispatchers.Default).launch {
while (AnimeServiceDataSingleton.downloadQueue.isNotEmpty()) {
val task = AnimeServiceDataSingleton.downloadQueue.poll()
if (task != null) {
val job = launch { download(task) }
currentTasks.add(task)
mutex.withLock {
downloadJobs[task.getTaskName()] = job
}
job.join() // Wait for the job to complete before continuing to the next task
mutex.withLock {
downloadJobs.remove(task.getTaskName())
}
updateNotification() // Update the notification after each task is completed
}
if (AnimeServiceDataSingleton.downloadQueue.isEmpty()) {
withContext(Dispatchers.Main) {
stopSelf() // Stop the service when the queue is empty
}
}
}
}
}
@UnstableApi
fun cancelDownload(taskName: String) {
val url =
AnimeServiceDataSingleton.downloadQueue.find { it.getTaskName() == taskName }?.video?.file?.url
?: currentTasks.find { it.getTaskName() == taskName }?.video?.file?.url ?: ""
if (url.isEmpty()) {
snackString("Failed to cancel download")
return
}
currentTasks.removeAll { it.getTaskName() == taskName }
DownloadService.sendSetStopReason(
this@AnimeDownloaderService,
ExoplayerDownloadService::class.java,
url,
androidx.media3.exoplayer.offline.Download.STATE_STOPPED,
false
)
DownloadService.sendRemoveDownload(
this@AnimeDownloaderService,
ExoplayerDownloadService::class.java,
url,
false
)
CoroutineScope(Dispatchers.Default).launch {
mutex.withLock {
downloadJobs[taskName]?.cancel()
downloadJobs.remove(taskName)
AnimeServiceDataSingleton.downloadQueue.removeAll { it.getTaskName() == taskName }
updateNotification() // Update the notification after cancellation
}
}
}
private fun updateNotification() {
// Update the notification to reflect the current state of the queue
val pendingDownloads = AnimeServiceDataSingleton.downloadQueue.size
val text = if (pendingDownloads > 0) {
"Pending downloads: $pendingDownloads"
} else {
"All downloads completed"
}
builder.setContentText(text)
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
return
}
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
@androidx.annotation.OptIn(UnstableApi::class)
suspend fun download(task: AnimeDownloadTask) {
try {
val downloadManager = Helper.downloadManager(this@AnimeDownloaderService)
withContext(Dispatchers.Main) {
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(
this@AnimeDownloaderService,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
} else {
true
}
builder.setContentText("Downloading ${task.title} - ${task.episode}")
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
currActivity()?.let {
Helper.downloadVideo(
it,
task.video,
task.subtitle
)
}
saveMediaInfo(task)
task.subtitle?.let {
SubtitleDownloader.downloadSubtitle(
this@AnimeDownloaderService,
it.file.url,
DownloadedType(
task.title,
task.episode,
DownloadedType.Type.ANIME,
)
)
}
val downloadStarted =
hasDownloadStarted(downloadManager, task, 30000) // 30 seconds timeout
if (!downloadStarted) {
Logger.log("Download failed to start")
builder.setContentText("${task.title} - ${task.episode} Download failed to start")
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${task.title} - ${task.episode} Download failed to start")
broadcastDownloadFailed(task.episode)
return@withContext
}
// periodically check if the download is complete
while (downloadManager.downloadIndex.getDownload(task.video.file.url) != null) {
val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
if (download != null) {
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_FAILED) {
Logger.log("Download failed")
builder.setContentText("${task.title} - ${task.episode} Download failed")
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${task.title} - ${task.episode} Download failed")
Logger.log("Download failed: ${download.failureReason}")
downloadsManager.removeDownload(
DownloadedType(
task.title,
task.episode,
DownloadedType.Type.ANIME,
)
)
Injekt.get<CrashlyticsInterface>().logException(
Exception(
"Anime Download failed:" +
" ${download.failureReason}" +
" url: ${task.video.file.url}" +
" title: ${task.title}" +
" episode: ${task.episode}"
)
)
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
broadcastDownloadFailed(task.episode)
break
}
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_COMPLETED) {
Logger.log("Download completed")
builder.setContentText("${task.title} - ${task.episode} Download completed")
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${task.title} - ${task.episode} Download completed")
PrefManager.getAnimeDownloadPreferences().edit().putString(
task.getTaskName(),
task.video.file.url
).apply()
downloadsManager.addDownload(
DownloadedType(
task.title,
task.episode,
DownloadedType.Type.ANIME,
)
)
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
broadcastDownloadFinished(task.episode)
break
}
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_STOPPED) {
Logger.log("Download stopped")
builder.setContentText("${task.title} - ${task.episode} Download stopped")
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${task.title} - ${task.episode} Download stopped")
break
}
broadcastDownloadProgress(
task.episode,
download.percentDownloaded.toInt()
)
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
}
kotlinx.coroutines.delay(2000)
}
}
} catch (e: Exception) {
if (e.message?.contains("Coroutine was cancelled") == false) { //wut
Logger.log("Exception while downloading file: ${e.message}")
snackString("Exception while downloading file: ${e.message}")
e.printStackTrace()
Injekt.get<CrashlyticsInterface>().logException(e)
}
broadcastDownloadFailed(task.episode)
}
}
@androidx.annotation.OptIn(UnstableApi::class)
suspend fun hasDownloadStarted(
downloadManager: DownloadManager,
task: AnimeDownloadTask,
timeout: Long
): Boolean {
val startTime = System.currentTimeMillis()
while (System.currentTimeMillis() - startTime < timeout) {
val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
if (download != null) {
return true
}
// Delay between each poll
kotlinx.coroutines.delay(500)
}
return false
}
private fun saveMediaInfo(task: AnimeDownloadTask) {
launchIO {
val directory = File(
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"${DownloadsManager.animeLocation}/${task.title}"
)
val episodeDirectory = File(directory, task.episode)
if (!episodeDirectory.exists()) episodeDirectory.mkdirs()
val file = File(directory, "media.json")
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
})
.registerTypeAdapter(SAnime::class.java, InstanceCreator<SAnime> {
SAnimeImpl() // Provide an instance of SAnimeImpl
})
.registerTypeAdapter(SEpisode::class.java, InstanceCreator<SEpisode> {
SEpisodeImpl() // Provide an instance of SEpisodeImpl
})
.create()
val mediaJson = gson.toJson(task.sourceMedia)
val media = gson.fromJson(mediaJson, Media::class.java)
if (media != null) {
media.cover = media.cover?.let { downloadImage(it, directory, "cover.jpg") }
media.banner = media.banner?.let { downloadImage(it, directory, "banner.jpg") }
if (task.episodeImage != null) {
media.anime?.episodes?.get(task.episode)?.let { episode ->
episode.thumb = downloadImage(
task.episodeImage,
episodeDirectory,
"episodeImage.jpg"
)?.let {
FileUrl(
it
)
}
}
downloadImage(task.episodeImage, episodeDirectory, "episodeImage.jpg")
}
val jsonString = gson.toJson(media)
withContext(Dispatchers.Main) {
file.writeText(jsonString)
}
}
}
}
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
withContext(Dispatchers.IO) {
var connection: HttpURLConnection? = null
println("Downloading url $url")
try {
connection = URL(url).openConnection() as HttpURLConnection
connection.connect()
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
}
val file = File(directory, name)
FileOutputStream(file).use { output ->
connection.inputStream.use { input ->
input.copyTo(output)
}
}
return@withContext file.absolutePath
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
Toast.makeText(
this@AnimeDownloaderService,
"Exception while saving ${name}: ${e.message}",
Toast.LENGTH_LONG
).show()
}
null
} finally {
connection?.disconnect()
}
}
private fun broadcastDownloadStarted(episodeNumber: String) {
val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_STARTED).apply {
putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, episodeNumber)
}
sendBroadcast(intent)
}
private fun broadcastDownloadFinished(episodeNumber: String) {
val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_FINISHED).apply {
putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, episodeNumber)
}
sendBroadcast(intent)
}
private fun broadcastDownloadFailed(episodeNumber: String) {
val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_FAILED).apply {
putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, episodeNumber)
}
sendBroadcast(intent)
}
private fun broadcastDownloadProgress(episodeNumber: String, progress: Int) {
val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_PROGRESS).apply {
putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, episodeNumber)
putExtra("progress", progress)
}
sendBroadcast(intent)
}
private val cancelReceiver = object : BroadcastReceiver() {
@androidx.annotation.OptIn(UnstableApi::class)
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == ACTION_CANCEL_DOWNLOAD) {
val taskName = intent.getStringExtra(EXTRA_TASK_NAME)
taskName?.let {
cancelDownload(it)
}
}
}
}
data class AnimeDownloadTask(
val title: String,
val episode: String,
val video: Video,
val subtitle: Subtitle? = null,
val sourceMedia: Media? = null,
val episodeImage: String? = null,
val retries: Int = 2,
val simultaneousDownloads: Int = 2,
) {
fun getTaskName(): String {
return "$title - $episode"
}
companion object {
fun getTaskName(title: String, episode: String): String {
return "$title - $episode"
}
}
}
companion object {
private const val NOTIFICATION_ID = 1103
const val ACTION_CANCEL_DOWNLOAD = "action_cancel_download"
const val EXTRA_TASK_NAME = "extra_task_name"
}
}
object AnimeServiceDataSingleton {
var video: Video? = null
var sourceMedia: Media? = null
var downloadQueue: Queue<AnimeDownloaderService.AnimeDownloadTask> = ConcurrentLinkedQueue()
@Volatile
var isServiceRunning: Boolean = false
}

View File

@@ -0,0 +1,112 @@
package ani.dantotsu.download.anime
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.cardview.widget.CardView
import ani.dantotsu.R
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
class OfflineAnimeAdapter(
private val context: Context,
private var items: List<OfflineAnimeModel>,
private val searchListener: OfflineAnimeSearchListener
) : BaseAdapter() {
private val inflater: LayoutInflater =
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
private var originalItems: List<OfflineAnimeModel> = items
private var style: Int = PrefManager.getVal(PrefName.OfflineView)
override fun getCount(): Int {
return items.size
}
override fun getItem(position: Int): Any {
return items[position]
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
@SuppressLint("SetTextI18n")
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view: View = convertView ?: when (style) {
0 -> inflater.inflate(R.layout.item_media_large, parent, false) // large view
1 -> inflater.inflate(R.layout.item_media_compact, parent, false) // compact view
else -> inflater.inflate(R.layout.item_media_compact, parent, false) // compact view
}
val item = getItem(position) as OfflineAnimeModel
val imageView = view.findViewById<ImageView>(R.id.itemCompactImage)
val titleTextView = view.findViewById<TextView>(R.id.itemCompactTitle)
val itemScore = view.findViewById<TextView>(R.id.itemCompactScore)
val itemScoreBG = view.findViewById<View>(R.id.itemCompactScoreBG)
val ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing)
val totalepisodes = view.findViewById<TextView>(R.id.itemCompactTotal)
val typeimage = view.findViewById<ImageView>(R.id.itemCompactTypeImage)
val type = view.findViewById<TextView>(R.id.itemCompactRelation)
val typeView = view.findViewById<LinearLayout>(R.id.itemCompactType)
if (style == 0) {
val bannerView = view.findViewById<ImageView>(R.id.itemCompactBanner) // for large view
val episodes = view.findViewById<TextView>(R.id.itemTotal)
episodes.text = " Episodes"
bannerView.setImageURI(item.banner)
totalepisodes.text = item.totalEpisodeList
} else if (style == 1) {
val watchedEpisodes =
view.findViewById<TextView>(R.id.itemCompactUserProgress) // for compact view
watchedEpisodes.text = item.watchedEpisode
totalepisodes.text = " | " + item.totalEpisode
}
// Bind item data to the views
typeimage.setImageResource(R.drawable.ic_round_movie_filter_24)
type.text = item.type
typeView.visibility = View.VISIBLE
imageView.setImageURI(item.image)
titleTextView.text = item.title
itemScore.text = item.score
if (item.isOngoing) {
ongoing.visibility = View.VISIBLE
} else {
ongoing.visibility = View.GONE
}
return view
}
fun onSearchQuery(query: String) {
// Implement the filtering logic here, for example:
items = if (query.isEmpty()) {
// Return the original list if the query is empty
originalItems
} else {
// Filter the list based on the query
originalItems.filter { it.title.contains(query, ignoreCase = true) }
}
notifyDataSetChanged() // Notify the adapter that the data set has changed
}
fun setItems(items: List<OfflineAnimeModel>) {
this.items = items
this.originalItems = items
notifyDataSetChanged()
}
fun notifyNewGrid() {
style = PrefManager.getVal(PrefName.OfflineView)
notifyDataSetChanged()
}
}

View File

@@ -0,0 +1,399 @@
package ani.dantotsu.download.anime
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.text.Editable
import android.text.TextWatcher
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.AlphaAnimation
import android.view.animation.LayoutAnimationController
import android.widget.AbsListView
import android.widget.AutoCompleteTextView
import android.widget.GridView
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.core.view.marginBottom
import androidx.fragment.app.Fragment
import androidx.media3.common.util.UnstableApi
import ani.dantotsu.R
import ani.dantotsu.bottomBar
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.currActivity
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.initActivity
import ani.dantotsu.util.Logger
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.SettingsDialogFragment
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import com.google.android.material.card.MaterialCardView
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.textfield.TextInputLayout
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.SAnimeImpl
import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SChapterImpl
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
private val downloadManager = Injekt.get<DownloadsManager>()
private var downloads: List<OfflineAnimeModel> = listOf()
private lateinit var gridView: GridView
private lateinit var adapter: OfflineAnimeAdapter
private lateinit var total: TextView
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_offline_page, container, false)
val textInputLayout = view.findViewById<TextInputLayout>(R.id.offlineMangaSearchBar)
textInputLayout.hint = "Anime"
val currentColor = textInputLayout.boxBackgroundColor
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
textInputLayout.boxBackgroundColor = semiTransparentColor
val materialCardView = view.findViewById<MaterialCardView>(R.id.offlineMangaAvatarContainer)
materialCardView.setCardBackgroundColor(semiTransparentColor)
val typedValue = TypedValue()
requireContext().theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
val color = typedValue.data
val animeUserAvatar = view.findViewById<ShapeableImageView>(R.id.offlineMangaUserAvatar)
animeUserAvatar.setSafeOnClickListener {
val dialogFragment =
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.OfflineANIME)
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
}
if (!(PrefManager.getVal(PrefName.ImmersiveMode) as Boolean)) {
view.rootView.fitsSystemWindows = true
}
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
val searchView = view.findViewById<AutoCompleteTextView>(R.id.animeSearchBarText)
searchView.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
onSearchQuery(s.toString())
}
})
var style: Int = PrefManager.getVal(PrefName.OfflineView)
val layoutList = view.findViewById<ImageView>(R.id.downloadedList)
val layoutcompact = view.findViewById<ImageView>(R.id.downloadedGrid)
var selected = when (style) {
0 -> layoutList
1 -> layoutcompact
else -> layoutList
}
selected.alpha = 1f
fun selected(it: ImageView) {
selected.alpha = 0.33f
selected = it
selected.alpha = 1f
}
layoutList.setOnClickListener {
selected(it as ImageView)
style = 0
PrefManager.setVal(PrefName.OfflineView, style)
gridView.visibility = View.GONE
gridView = view.findViewById(R.id.gridView)
adapter.notifyNewGrid()
grid()
}
layoutcompact.setOnClickListener {
selected(it as ImageView)
style = 1
PrefManager.setVal(PrefName.OfflineView, style)
gridView.visibility = View.GONE
gridView = view.findViewById(R.id.gridView1)
adapter.notifyNewGrid()
grid()
}
gridView =
if (style == 0) view.findViewById(R.id.gridView) else view.findViewById(R.id.gridView1)
total = view.findViewById(R.id.total)
grid()
return view
}
@OptIn(UnstableApi::class)
private fun grid() {
gridView.visibility = View.VISIBLE
getDownloads()
val fadeIn = AlphaAnimation(0f, 1f)
fadeIn.duration = 300 // animations pog
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
adapter = OfflineAnimeAdapter(requireContext(), downloads, this)
gridView.adapter = adapter
gridView.scheduleLayoutAnimation()
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
gridView.setOnItemClickListener { _, _, position, _ ->
// Get the OfflineAnimeModel that was clicked
val item = adapter.getItem(position) as OfflineAnimeModel
val media =
downloadManager.animeDownloadedTypes.firstOrNull { it.title == item.title }
media?.let {
val mediaModel = getMedia(it)
if (mediaModel == null) {
snackString("Error loading media.json")
return@let
}
MediaDetailsActivity.mediaSingleton = mediaModel
ContextCompat.startActivity(
requireActivity(),
Intent(requireContext(), MediaDetailsActivity::class.java)
.putExtra("download", true),
null
)
} ?: run {
snackString("no media found")
}
}
gridView.setOnItemLongClickListener { _, _, position, _ ->
// Get the OfflineAnimeModel that was clicked
val item = adapter.getItem(position) as OfflineAnimeModel
val type: DownloadedType.Type =
DownloadedType.Type.ANIME
// Alert dialog to confirm deletion
val builder =
androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup)
builder.setTitle("Delete ${item.title}?")
builder.setMessage("Are you sure you want to delete ${item.title}?")
builder.setPositiveButton("Yes") { _, _ ->
downloadManager.removeMedia(item.title, type)
val mediaIds =
PrefManager.getAnimeDownloadPreferences().all?.filter { it.key.contains(item.title) }?.values
?: emptySet()
if (mediaIds.isEmpty()) {
snackString("No media found") // if this happens, terrible things have happened
}
for (mediaId in mediaIds) {
ani.dantotsu.download.video.Helper.downloadManager(requireContext())
.removeDownload(mediaId.toString())
}
getDownloads()
adapter.setItems(downloads)
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
}
builder.setNegativeButton("No") { _, _ ->
// Do nothing
}
val dialog = builder.show()
dialog.window?.setDimAmount(0.8f)
true
}
}
override fun onSearchQuery(query: String) {
adapter.onSearchQuery(query)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val scrollTop = view.findViewById<CardView>(R.id.mangaPageScrollTop)
scrollTop.setOnClickListener {
gridView.smoothScrollToPositionFromTop(0, 0)
}
// Assuming 'scrollTop' is a view that you want to hide/show
scrollTop.visibility = View.GONE
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
// Implement behavior for different scroll states if needed
}
override fun onScroll(
view: AbsListView,
firstVisibleItem: Int,
visibleItemCount: Int,
totalItemCount: Int
) {
val first = view.getChildAt(0)
val visibility = first != null && first.top < 0
scrollTop.translationY =
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE
}
})
initActivity(requireActivity())
}
override fun onResume() {
super.onResume()
getDownloads()
adapter.notifyDataSetChanged()
}
override fun onPause() {
super.onPause()
downloads = listOf()
}
override fun onDestroy() {
super.onDestroy()
downloads = listOf()
}
override fun onStop() {
super.onStop()
downloads = listOf()
}
private fun getDownloads() {
downloads = listOf()
val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
for (title in animeTitles) {
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
val download = tDownloads.first()
val offlineAnimeModel = loadOfflineAnimeModel(download)
newAnimeDownloads += offlineAnimeModel
}
downloads = newAnimeDownloads
}
private fun getMedia(downloadedType: DownloadedType): Media? {
val type = when (downloadedType.type) {
DownloadedType.Type.MANGA -> "Manga"
DownloadedType.Type.ANIME -> "Anime"
else -> "Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
return try {
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
})
.registerTypeAdapter(SAnime::class.java, InstanceCreator<SAnime> {
SAnimeImpl() // Provide an instance of SAnimeImpl
})
.registerTypeAdapter(SEpisode::class.java, InstanceCreator<SEpisode> {
SEpisodeImpl() // Provide an instance of SEpisodeImpl
})
.create()
val media = File(directory, "media.json")
val mediaJson = media.readText()
gson.fromJson(mediaJson, Media::class.java)
} catch (e: Exception) {
Logger.log("Error loading media.json: ${e.message}")
Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e)
null
}
}
private fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel {
val type = when (downloadedType.type) {
DownloadedType.Type.MANGA -> "Manga"
DownloadedType.Type.ANIME -> "Anime"
else -> "Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
try {
val mediaModel = getMedia(downloadedType)!!
val cover = File(directory, "cover.jpg")
val coverUri: Uri? = if (cover.exists()) {
Uri.fromFile(cover)
} else null
val banner = File(directory, "banner.jpg")
val bannerUri: Uri? = if (banner.exists()) {
Uri.fromFile(banner)
} else null
val title = mediaModel.mainName()
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
?: 0) else mediaModel.userScore) / 10.0).toString()
val isOngoing =
mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
val isUserScored = mediaModel.userScore != 0
val watchedEpisodes = (mediaModel.userProgress ?: "~").toString()
val totalEpisode =
if (mediaModel.anime?.nextAiringEpisode != null) (mediaModel.anime.nextAiringEpisode.toString() + " | " + (mediaModel.anime.totalEpisodes
?: "~").toString()) else (mediaModel.anime?.totalEpisodes ?: "~").toString()
val chapters = " Chapters"
val totalEpisodesList =
if (mediaModel.anime?.nextAiringEpisode != null) (mediaModel.anime.nextAiringEpisode.toString()) else (mediaModel.anime?.totalEpisodes
?: "~").toString()
return OfflineAnimeModel(
title,
score,
totalEpisode,
totalEpisodesList,
watchedEpisodes,
type,
chapters,
isOngoing,
isUserScored,
coverUri,
bannerUri
)
} catch (e: Exception) {
Logger.log("Error loading media.json: ${e.message}")
Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e)
return OfflineAnimeModel(
"unknown",
"0",
"??",
"??",
"??",
"movie",
"hmm",
isOngoing = false,
isUserScored = false,
null,
null
)
}
}
}
interface OfflineAnimeSearchListener {
fun onSearchQuery(query: String)
}

View File

@@ -0,0 +1,17 @@
package ani.dantotsu.download.anime
import android.net.Uri
data class OfflineAnimeModel(
val title: String,
val score: String,
val totalEpisode: String,
val totalEpisodeList: String,
val watchedEpisode: String,
val type: String,
val episodes: String,
val isOngoing: Boolean,
val isUserScored: Boolean,
val image: Uri?,
val banner: Uri?,
)

View File

@@ -18,9 +18,10 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.download.Download import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger import ani.dantotsu.util.Logger
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.manga.ImageData import ani.dantotsu.media.manga.ImageData
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FAILED import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FAILED
@@ -29,7 +30,6 @@ import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_PROG
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STARTED import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STARTED
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
import ani.dantotsu.snackString import ani.dantotsu.snackString
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PROGRESS import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PROGRESS
@@ -37,6 +37,7 @@ import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SChapterImpl import eu.kanade.tachiyomi.source.model.SChapterImpl
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -47,6 +48,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import tachiyomi.core.util.lang.launchIO
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File import java.io.File
@@ -76,7 +78,7 @@ class MangaDownloaderService : Service() {
notificationManager = NotificationManagerCompat.from(this) notificationManager = NotificationManagerCompat.from(this)
builder = NotificationCompat.Builder(this, CHANNEL_DOWNLOADER_PROGRESS).apply { builder = NotificationCompat.Builder(this, CHANNEL_DOWNLOADER_PROGRESS).apply {
setContentTitle("Manga Download Progress") setContentTitle("Manga Download Progress")
setSmallIcon(R.drawable.ic_round_download_24) setSmallIcon(R.drawable.ic_download_24)
priority = NotificationCompat.PRIORITY_DEFAULT priority = NotificationCompat.PRIORITY_DEFAULT
setOnlyAlertOnce(true) setOnlyAlertOnce(true)
setProgress(0, 0, false) setProgress(0, 0, false)
@@ -187,7 +189,8 @@ class MangaDownloaderService : Service() {
true true
} }
val deferredList = mutableListOf<Deferred<Bitmap?>>() //val deferredList = mutableListOf<Deferred<Bitmap?>>()
val deferredMap = mutableMapOf<Int, Deferred<Bitmap?>>()
builder.setContentText("Downloading ${task.title} - ${task.chapter}") builder.setContentText("Downloading ${task.title} - ${task.chapter}")
if (notifi) { if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build()) notificationManager.notify(NOTIFICATION_ID, builder.build())
@@ -196,15 +199,12 @@ class MangaDownloaderService : Service() {
// Loop through each ImageData object from the task // Loop through each ImageData object from the task
var farthest = 0 var farthest = 0
for ((index, image) in task.imageData.withIndex()) { for ((index, image) in task.imageData.withIndex()) {
// Limit the number of simultaneous downloads from the task if (deferredMap.size >= task.simultaneousDownloads) {
if (deferredList.size >= task.simultaneousDownloads) { deferredMap.values.awaitAll()
// Wait for all deferred to complete and clear the list deferredMap.clear()
deferredList.awaitAll()
deferredList.clear()
} }
// Download the image and add to deferred list deferredMap[index] = async(Dispatchers.IO) {
val deferred = async(Dispatchers.IO) {
var bitmap: Bitmap? = null var bitmap: Bitmap? = null
var retryCount = 0 var retryCount = 0
@@ -217,7 +217,6 @@ class MangaDownloaderService : Service() {
retryCount++ retryCount++
} }
// Cache the image if successful
if (bitmap != null) { if (bitmap != null) {
saveToDisk("$index.jpg", bitmap, task.title, task.chapter) saveToDisk("$index.jpg", bitmap, task.title, task.chapter)
} }
@@ -233,12 +232,10 @@ class MangaDownloaderService : Service() {
bitmap bitmap
} }
deferredList.add(deferred)
} }
// Wait for any remaining deferred to complete // Wait for any remaining deferred to complete
deferredList.awaitAll() deferredMap.values.awaitAll()
builder.setContentText("${task.title} - ${task.chapter} Download complete") builder.setContentText("${task.title} - ${task.chapter} Download complete")
.setProgress(0, 0, false) .setProgress(0, 0, false)
@@ -246,19 +243,19 @@ class MangaDownloaderService : Service() {
saveMediaInfo(task) saveMediaInfo(task)
downloadsManager.addDownload( downloadsManager.addDownload(
Download( DownloadedType(
task.title, task.title,
task.chapter, task.chapter,
Download.Type.MANGA DownloadedType.Type.MANGA
) )
) )
broadcastDownloadFinished(task.chapter) broadcastDownloadFinished(task.chapter)
snackString("${task.title} - ${task.chapter} Download finished") snackString("${task.title} - ${task.chapter} Download finished")
} }
} catch (e: Exception) { } catch (e: Exception) {
logger("Exception while downloading file: ${e.message}") Logger.log("Exception while downloading file: ${e.message}")
snackString("Exception while downloading file: ${e.message}") snackString("Exception while downloading file: ${e.message}")
FirebaseCrashlytics.getInstance().recordException(e) Injekt.get<CrashlyticsInterface>().logException(e)
broadcastDownloadFailed(task.chapter) broadcastDownloadFailed(task.chapter)
} }
} }
@@ -288,12 +285,13 @@ class MangaDownloaderService : Service() {
} catch (e: Exception) { } catch (e: Exception) {
println("Exception while saving image: ${e.message}") println("Exception while saving image: ${e.message}")
snackString("Exception while saving image: ${e.message}") snackString("Exception while saving image: ${e.message}")
FirebaseCrashlytics.getInstance().recordException(e) Injekt.get<CrashlyticsInterface>().logException(e)
} }
} }
@OptIn(DelicateCoroutinesApi::class)
private fun saveMediaInfo(task: DownloadTask) { private fun saveMediaInfo(task: DownloadTask) {
GlobalScope.launch(Dispatchers.IO) { launchIO {
val directory = File( val directory = File(
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${task.title}" "Dantotsu/Manga/${task.title}"
@@ -314,7 +312,16 @@ class MangaDownloaderService : Service() {
val jsonString = gson.toJson(media) val jsonString = gson.toJson(media)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
file.writeText(jsonString) try {
file.writeText(jsonString)
} catch (e: android.system.ErrnoException) {
e.printStackTrace()
Toast.makeText(
this@MangaDownloaderService,
"Error while saving: ${e.localizedMessage}",
Toast.LENGTH_LONG
).show()
}
} }
} }
} }

View File

@@ -1,14 +1,18 @@
package ani.dantotsu.download.manga package ani.dantotsu.download.manga
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
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 android.widget.BaseAdapter import android.widget.BaseAdapter
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
class OfflineMangaAdapter( class OfflineMangaAdapter(
@@ -19,6 +23,7 @@ class OfflineMangaAdapter(
private val inflater: LayoutInflater = private val inflater: LayoutInflater =
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
private var originalItems: List<OfflineMangaModel> = items private var originalItems: List<OfflineMangaModel> = items
private var style: Int = PrefManager.getVal(PrefName.OfflineView)
override fun getCount(): Int { override fun getCount(): Int {
return items.size return items.size
@@ -32,23 +37,47 @@ class OfflineMangaAdapter(
return position.toLong() return position.toLong()
} }
@SuppressLint("SetTextI18n")
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
var view = convertView
if (view == null) { val view: View = convertView ?: when (style) {
view = inflater.inflate(R.layout.item_media_compact, parent, false) 0 -> inflater.inflate(R.layout.item_media_large, parent, false) // large view
1 -> inflater.inflate(R.layout.item_media_compact, parent, false) // compact view
else -> inflater.inflate(R.layout.item_media_compact, parent, false) // compact view
} }
val item = getItem(position) as OfflineMangaModel val item = getItem(position) as OfflineMangaModel
val imageView = view!!.findViewById<ImageView>(R.id.itemCompactImage) val imageView = view.findViewById<ImageView>(R.id.itemCompactImage)
val titleTextView = view.findViewById<TextView>(R.id.itemCompactTitle) val titleTextView = view.findViewById<TextView>(R.id.itemCompactTitle)
val itemScore = view.findViewById<TextView>(R.id.itemCompactScore) val itemScore = view.findViewById<TextView>(R.id.itemCompactScore)
val itemScoreBG = view.findViewById<View>(R.id.itemCompactScoreBG) val itemScoreBG = view.findViewById<View>(R.id.itemCompactScoreBG)
val ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing) val ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing)
val totalChapter = view.findViewById<TextView>(R.id.itemCompactTotal)
val typeImage = view.findViewById<ImageView>(R.id.itemCompactTypeImage)
val type = view.findViewById<TextView>(R.id.itemCompactRelation)
val typeView = view.findViewById<LinearLayout>(R.id.itemCompactType)
if (style == 0) {
val bannerView = view.findViewById<ImageView>(R.id.itemCompactBanner) // for large view
val chapters = view.findViewById<TextView>(R.id.itemTotal)
chapters.text = " Chapters"
bannerView.setImageURI(item.banner)
totalChapter.text = item.totalChapter
} else if (style == 1) {
val readChapter =
view.findViewById<TextView>(R.id.itemCompactUserProgress) // for compact view
readChapter.text = item.readChapter
totalChapter.text = " | " + item.totalChapter
}
// Bind item data to the views // Bind item data to the views
// For example: typeImage.setImageResource(if (item.type == "Novel") R.drawable.ic_round_book_24 else R.drawable.ic_round_import_contacts_24)
type.text = item.type
typeView.visibility = View.VISIBLE
imageView.setImageURI(item.image) imageView.setImageURI(item.image)
titleTextView.text = item.title titleTextView.text = item.title
itemScore.text = item.score itemScore.text = item.score
if (item.isOngoing) { if (item.isOngoing) {
ongoing.visibility = View.VISIBLE ongoing.visibility = View.VISIBLE
} else { } else {
@@ -74,4 +103,9 @@ class OfflineMangaAdapter(
this.originalItems = items this.originalItems = items
notifyDataSetChanged() notifyDataSetChanged()
} }
fun notifyNewGrid() {
style = PrefManager.getVal(PrefName.OfflineView)
notifyDataSetChanged()
}
} }

View File

@@ -1,10 +1,7 @@
package ani.dantotsu.download.manga package ani.dantotsu.download.manga
import android.animation.ObjectAnimator
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.text.Editable import android.text.Editable
@@ -13,27 +10,38 @@ import android.util.TypedValue
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 android.view.animation.OvershootInterpolator import android.view.animation.AlphaAnimation
import android.view.animation.LayoutAnimationController
import android.widget.AbsListView
import android.widget.AutoCompleteTextView import android.widget.AutoCompleteTextView
import android.widget.GridView import android.widget.GridView
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.core.view.marginBottom
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.bottomBar
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.currActivity
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.download.Download import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger import ani.dantotsu.initActivity
import ani.dantotsu.util.Logger
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.setSafeOnClickListener import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.SettingsDialogFragment import ani.dantotsu.settings.SettingsDialogFragment
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.google.android.material.imageview.ShapeableImageView import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
@@ -41,23 +49,24 @@ import eu.kanade.tachiyomi.source.model.SChapterImpl
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File import java.io.File
import kotlin.math.max
import kotlin.math.min
class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private val downloadManager = Injekt.get<DownloadsManager>() private val downloadManager = Injekt.get<DownloadsManager>()
private var downloads: List<OfflineMangaModel> = listOf() private var downloads: List<OfflineMangaModel> = listOf()
private lateinit var gridView: GridView private lateinit var gridView: GridView
private lateinit var adapter: OfflineMangaAdapter private lateinit var adapter: OfflineMangaAdapter
private lateinit var total: TextView
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
val view = inflater.inflate(R.layout.fragment_manga_offline, container, false) val view = inflater.inflate(R.layout.fragment_offline_page, container, false)
val textInputLayout = view.findViewById<TextInputLayout>(R.id.offlineMangaSearchBar) val textInputLayout = view.findViewById<TextInputLayout>(R.id.offlineMangaSearchBar)
textInputLayout.hint = "Manga"
val currentColor = textInputLayout.boxBackgroundColor val currentColor = textInputLayout.boxBackgroundColor
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt() val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
textInputLayout.boxBackgroundColor = semiTransparentColor textInputLayout.boxBackgroundColor = semiTransparentColor
@@ -69,22 +78,16 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
val animeUserAvatar = view.findViewById<ShapeableImageView>(R.id.offlineMangaUserAvatar) val animeUserAvatar = view.findViewById<ShapeableImageView>(R.id.offlineMangaUserAvatar)
animeUserAvatar.setSafeOnClickListener { animeUserAvatar.setSafeOnClickListener {
animeUserAvatar.setSafeOnClickListener { val dialogFragment =
val dialogFragment = SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.HOME) SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.OfflineMANGA)
dialogFragment.show( dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
(it.context as AppCompatActivity).supportFragmentManager, }
"dialog" if (!(PrefManager.getVal(PrefName.ImmersiveMode) as Boolean)) {
) view.rootView.fitsSystemWindows = true
}
} }
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
?.getBoolean("colorOverflow", false) ?: false materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
if (!colorOverflow) {
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
}
val searchView = view.findViewById<AutoCompleteTextView>(R.id.animeSearchBarText) val searchView = view.findViewById<AutoCompleteTextView>(R.id.animeSearchBarText)
searchView.addTextChangedListener(object : TextWatcher { searchView.addTextChangedListener(object : TextWatcher {
@@ -98,54 +101,108 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
onSearchQuery(s.toString()) onSearchQuery(s.toString())
} }
}) })
var style: Int = PrefManager.getVal(PrefName.OfflineView)
val layoutList = view.findViewById<ImageView>(R.id.downloadedList)
val layoutcompact = view.findViewById<ImageView>(R.id.downloadedGrid)
var selected = when (style) {
0 -> layoutList
1 -> layoutcompact
else -> layoutList
}
selected.alpha = 1f
gridView = view.findViewById(R.id.gridView) fun selected(it: ImageView) {
selected.alpha = 0.33f
selected = it
selected.alpha = 1f
}
layoutList.setOnClickListener {
selected(it as ImageView)
style = 0
PrefManager.setVal(PrefName.OfflineView, style)
gridView.visibility = View.GONE
gridView = view.findViewById(R.id.gridView)
adapter.notifyNewGrid()
grid()
}
layoutcompact.setOnClickListener {
selected(it as ImageView)
style = 1
PrefManager.setVal(PrefName.OfflineView, style)
gridView.visibility = View.GONE
gridView = view.findViewById(R.id.gridView1)
adapter.notifyNewGrid()
grid()
}
gridView =
if (style == 0) view.findViewById(R.id.gridView) else view.findViewById(R.id.gridView1)
total = view.findViewById(R.id.total)
grid()
return view
}
private fun grid() {
gridView.visibility = View.VISIBLE
getDownloads() getDownloads()
val fadeIn = AlphaAnimation(0f, 1f)
fadeIn.duration = 300 // animations pog
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
adapter = OfflineMangaAdapter(requireContext(), downloads, this) adapter = OfflineMangaAdapter(requireContext(), downloads, this)
gridView.adapter = adapter gridView.adapter = adapter
gridView.setOnItemClickListener { parent, view, position, id -> gridView.scheduleLayoutAnimation()
total.text =
if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
gridView.setOnItemClickListener { _, _, position, _ ->
// Get the OfflineMangaModel that was clicked // Get the OfflineMangaModel that was clicked
val item = adapter.getItem(position) as OfflineMangaModel val item = adapter.getItem(position) as OfflineMangaModel
val media = val media =
downloadManager.mangaDownloads.firstOrNull { it.title == item.title } downloadManager.mangaDownloadedTypes.firstOrNull { it.title == item.title }
?: downloadManager.novelDownloads.firstOrNull { it.title == item.title } ?: downloadManager.novelDownloadedTypes.firstOrNull { it.title == item.title }
media?.let { media?.let {
startActivity(
ContextCompat.startActivity(
requireActivity(),
Intent(requireContext(), MediaDetailsActivity::class.java) Intent(requireContext(), MediaDetailsActivity::class.java)
.putExtra("media", getMedia(it)) .putExtra("media", getMedia(it))
.putExtra("download", true) .putExtra("download", true),
null
) )
} ?: run { } ?: run {
snackString("no media found") snackString("no media found")
} }
} }
gridView.setOnItemLongClickListener { parent, view, position, id -> gridView.setOnItemLongClickListener { _, _, position, _ ->
// Get the OfflineMangaModel that was clicked // Get the OfflineMangaModel that was clicked
val item = adapter.getItem(position) as OfflineMangaModel val item = adapter.getItem(position) as OfflineMangaModel
val type: Download.Type = if (downloadManager.mangaDownloads.any { it.title == item.title }) { val type: DownloadedType.Type =
Download.Type.MANGA if (downloadManager.mangaDownloadedTypes.any { it.title == item.title }) {
} else { DownloadedType.Type.MANGA
Download.Type.NOVEL } else {
} DownloadedType.Type.NOVEL
}
// Alert dialog to confirm deletion // Alert dialog to confirm deletion
val builder = androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup) val builder =
androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup)
builder.setTitle("Delete ${item.title}?") builder.setTitle("Delete ${item.title}?")
builder.setMessage("Are you sure you want to delete ${item.title}?") builder.setMessage("Are you sure you want to delete ${item.title}?")
builder.setPositiveButton("Yes") { _, _ -> builder.setPositiveButton("Yes") { _, _ ->
downloadManager.removeMedia(item.title, type) downloadManager.removeMedia(item.title, type)
getDownloads() getDownloads()
adapter.setItems(downloads) adapter.setItems(downloads)
total.text =
if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
} }
builder.setNegativeButton("No") { _, _ -> builder.setNegativeButton("No") { _, _ ->
// Do nothing // Do nothing
} }
val dialog = builder.show() val dialog = builder.show()
dialog.window?.setDimAmount(0.8f) dialog.window?.setDimAmount(0.8f)
true true
} }
return view
} }
override fun onSearchQuery(query: String) { override fun onSearchQuery(query: String) {
@@ -154,41 +211,35 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
var height = statusBarHeight initActivity(requireActivity())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val displayCutout = activity?.window?.decorView?.rootWindowInsets?.displayCutout
if (displayCutout != null) {
if (displayCutout.boundingRects.size > 0) {
height = max(
statusBarHeight,
min(
displayCutout.boundingRects[0].width(),
displayCutout.boundingRects[0].height()
)
)
}
}
}
val scrollTop = view.findViewById<CardView>(R.id.mangaPageScrollTop) val scrollTop = view.findViewById<CardView>(R.id.mangaPageScrollTop)
var visible = false scrollTop.setOnClickListener {
fun animate() { gridView.smoothScrollToPositionFromTop(0, 0)
val start = if (visible) 0f else 1f
val end = if (!visible) 0f else 1f
ObjectAnimator.ofFloat(scrollTop, "scaleX", start, end).apply {
duration = 300
interpolator = OvershootInterpolator(2f)
start()
}
ObjectAnimator.ofFloat(scrollTop, "scaleY", start, end).apply {
duration = 300
interpolator = OvershootInterpolator(2f)
start()
}
} }
scrollTop.setOnClickListener { // Assuming 'scrollTop' is a view that you want to hide/show
//TODO: scroll to top scrollTop.visibility = View.GONE
}
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
// Implement behavior for different scroll states if needed
}
override fun onScroll(
view: AbsListView,
firstVisibleItem: Int,
visibleItemCount: Int,
totalItemCount: Int
) {
val first = view.getChildAt(0)
val visibility = first != null && first.top < 0
scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE
scrollTop.translationY =
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
}
})
} }
@@ -215,20 +266,20 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private fun getDownloads() { private fun getDownloads() {
downloads = listOf() downloads = listOf()
val mangaTitles = downloadManager.mangaDownloads.map { it.title }.distinct() val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
val newMangaDownloads = mutableListOf<OfflineMangaModel>() val newMangaDownloads = mutableListOf<OfflineMangaModel>()
for (title in mangaTitles) { for (title in mangaTitles) {
val _downloads = downloadManager.mangaDownloads.filter { it.title == title } val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
val download = _downloads.first() val download = tDownloads.first()
val offlineMangaModel = loadOfflineMangaModel(download) val offlineMangaModel = loadOfflineMangaModel(download)
newMangaDownloads += offlineMangaModel newMangaDownloads += offlineMangaModel
} }
downloads = newMangaDownloads downloads = newMangaDownloads
val novelTitles = downloadManager.novelDownloads.map { it.title }.distinct() val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
val newNovelDownloads = mutableListOf<OfflineMangaModel>() val newNovelDownloads = mutableListOf<OfflineMangaModel>()
for (title in novelTitles) { for (title in novelTitles) {
val _downloads = downloadManager.novelDownloads.filter { it.title == title } val tDownloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
val download = _downloads.first() val download = tDownloads.first()
val offlineMangaModel = loadOfflineMangaModel(download) val offlineMangaModel = loadOfflineMangaModel(download)
newNovelDownloads += offlineMangaModel newNovelDownloads += offlineMangaModel
} }
@@ -236,17 +287,15 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
} }
private fun getMedia(download: Download): Media? { private fun getMedia(downloadedType: DownloadedType): Media? {
val type = if (download.type == Download.Type.MANGA) { val type = when (downloadedType.type) {
"Manga" DownloadedType.Type.MANGA -> "Manga"
} else if (download.type == Download.Type.ANIME) { DownloadedType.Type.ANIME -> "Anime"
"Anime" else -> "Novel"
} else {
"Novel"
} }
val directory = File( val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${download.title}" "Dantotsu/$type/${downloadedType.title}"
) )
//load media.json and convert to media class with gson //load media.json and convert to media class with gson
return try { return try {
@@ -259,51 +308,75 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
val mediaJson = media.readText() val mediaJson = media.readText()
gson.fromJson(mediaJson, Media::class.java) gson.fromJson(mediaJson, Media::class.java)
} catch (e: Exception) { } catch (e: Exception) {
logger("Error loading media.json: ${e.message}") Logger.log("Error loading media.json: ${e.message}")
logger(e.printStackTrace()) Logger.log(e)
FirebaseCrashlytics.getInstance().recordException(e) Injekt.get<CrashlyticsInterface>().logException(e)
null null
} }
} }
private fun loadOfflineMangaModel(download: Download): OfflineMangaModel { private fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
val type = if (download.type == Download.Type.MANGA) { val type = when (downloadedType.type) {
"Manga" DownloadedType.Type.MANGA -> "Manga"
} else if (download.type == Download.Type.ANIME) { DownloadedType.Type.ANIME -> "Anime"
"Anime" else -> "Novel"
} else {
"Novel"
} }
val directory = File( val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${download.title}" "Dantotsu/$type/${downloadedType.title}"
) )
//load media.json and convert to media class with gson //load media.json and convert to media class with gson
try { try {
val media = File(directory, "media.json") val mediaModel = getMedia(downloadedType)!!
val mediaJson = media.readText()
val mediaModel = getMedia(download)!!
val cover = File(directory, "cover.jpg") val cover = File(directory, "cover.jpg")
val coverUri: Uri? = if (cover.exists()) { val coverUri: Uri? = if (cover.exists()) {
Uri.fromFile(cover) Uri.fromFile(cover)
} else { } else null
null val banner = File(directory, "banner.jpg")
} val bannerUri: Uri? = if (banner.exists()) {
val title = mediaModel.nameMAL ?: mediaModel.nameRomaji Uri.fromFile(banner)
} else null
val title = mediaModel.mainName()
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
?: 0) else mediaModel.userScore) / 10.0).toString() ?: 0) else mediaModel.userScore) / 10.0).toString()
val isOngoing = false val isOngoing =
mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
val isUserScored = mediaModel.userScore != 0 val isUserScored = mediaModel.userScore != 0
return OfflineMangaModel(title, score, isOngoing, isUserScored, coverUri) val readchapter = (mediaModel.userProgress ?: "~").toString()
val totalchapter = "${mediaModel.manga?.totalChapters ?: "??"}"
val chapters = " Chapters"
return OfflineMangaModel(
title,
score,
totalchapter,
readchapter,
type,
chapters,
isOngoing,
isUserScored,
coverUri,
bannerUri
)
} catch (e: Exception) { } catch (e: Exception) {
logger("Error loading media.json: ${e.message}") Logger.log("Error loading media.json: ${e.message}")
logger(e.printStackTrace()) Logger.log(e)
FirebaseCrashlytics.getInstance().recordException(e) Injekt.get<CrashlyticsInterface>().logException(e)
return OfflineMangaModel("unknown", "0", false, false, null) return OfflineMangaModel(
"unknown",
"0",
"??",
"??",
"movie",
"hmm",
isOngoing = false,
isUserScored = false,
null,
null
)
} }
} }
} }
interface OfflineMangaSearchListener { interface OfflineMangaSearchListener {
fun onSearchQuery(query: String) fun onSearchQuery(query: String)
} }

View File

@@ -5,7 +5,12 @@ import android.net.Uri
data class OfflineMangaModel( data class OfflineMangaModel(
val title: String, val title: String,
val score: String, val score: String,
val totalChapter: String,
val readChapter: String,
val type: String,
val chapters: String,
val isOngoing: Boolean, val isOngoing: Boolean,
val isUserScored: Boolean, val isUserScored: Boolean,
val image: Uri? val image: Uri?,
val banner: Uri?
) )

View File

@@ -17,13 +17,13 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.download.Download import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger import ani.dantotsu.util.Logger
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.novel.NovelReadFragment import ani.dantotsu.media.novel.NovelReadFragment
import ani.dantotsu.snackString import ani.dantotsu.snackString
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
@@ -31,6 +31,7 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SChapterImpl import eu.kanade.tachiyomi.source.model.SChapterImpl
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -42,6 +43,7 @@ import kotlinx.coroutines.withContext
import okhttp3.Request import okhttp3.Request
import okio.buffer import okio.buffer
import okio.sink import okio.sink
import tachiyomi.core.util.lang.launchIO
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File import java.io.File
@@ -75,7 +77,7 @@ class NovelDownloaderService : Service() {
builder = builder =
NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER_PROGRESS).apply { NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER_PROGRESS).apply {
setContentTitle("Novel Download Progress") setContentTitle("Novel Download Progress")
setSmallIcon(R.drawable.ic_round_download_24) setSmallIcon(R.drawable.ic_download_24)
priority = NotificationCompat.PRIORITY_DEFAULT priority = NotificationCompat.PRIORITY_DEFAULT
setOnlyAlertOnce(true) setOnlyAlertOnce(true)
setProgress(0, 0, false) setProgress(0, 0, false)
@@ -186,15 +188,15 @@ class NovelDownloaderService : Service() {
val contentType = response.header("Content-Type") val contentType = response.header("Content-Type")
val contentDisposition = response.header("Content-Disposition") val contentDisposition = response.header("Content-Disposition")
logger("Content-Type: $contentType") Logger.log("Content-Type: $contentType")
logger("Content-Disposition: $contentDisposition") Logger.log("Content-Disposition: $contentDisposition")
// Return true if the Content-Type or Content-Disposition indicates an EPUB file // Return true if the Content-Type or Content-Disposition indicates an EPUB file
contentType == "application/epub+zip" || contentType == "application/epub+zip" ||
(contentDisposition?.contains(".epub") == true) (contentDisposition?.contains(".epub") == true)
} }
} catch (e: Exception) { } catch (e: Exception) {
logger("Error checking file type: ${e.message}") Logger.log("Error checking file type: ${e.message}")
false false
} }
} }
@@ -225,12 +227,12 @@ class NovelDownloaderService : Service() {
if (!isEpubFile(task.downloadLink)) { if (!isEpubFile(task.downloadLink)) {
if (isAlreadyDownloaded(task.originalLink)) { if (isAlreadyDownloaded(task.originalLink)) {
logger("Already downloaded") Logger.log("Already downloaded")
broadcastDownloadFinished(task.originalLink) broadcastDownloadFinished(task.originalLink)
snackString("Already downloaded") snackString("Already downloaded")
return@withContext return@withContext
} }
logger("Download link is not an .epub file") Logger.log("Download link is not an .epub file")
broadcastDownloadFailed(task.originalLink) broadcastDownloadFailed(task.originalLink)
snackString("Download link is not an .epub file") snackString("Download link is not an .epub file")
return@withContext return@withContext
@@ -301,7 +303,7 @@ class NovelDownloaderService : Service() {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
val progress = val progress =
(downloadedBytes * 100 / totalBytes).toInt() (downloadedBytes * 100 / totalBytes).toInt()
logger("Download progress: $progress") Logger.log("Download progress: $progress")
broadcastDownloadProgress(task.originalLink, progress) broadcastDownloadProgress(task.originalLink, progress)
} }
lastBroadcastUpdate = downloadedBytes lastBroadcastUpdate = downloadedBytes
@@ -316,7 +318,7 @@ class NovelDownloaderService : Service() {
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
logger("Exception while downloading .epub inside request: ${e.message}") Logger.log("Exception while downloading .epub inside request: ${e.message}")
throw e throw e
} }
} }
@@ -330,25 +332,26 @@ class NovelDownloaderService : Service() {
saveMediaInfo(task) saveMediaInfo(task)
downloadsManager.addDownload( downloadsManager.addDownload(
Download( DownloadedType(
task.title, task.title,
task.chapter, task.chapter,
Download.Type.NOVEL DownloadedType.Type.NOVEL
) )
) )
broadcastDownloadFinished(task.originalLink) broadcastDownloadFinished(task.originalLink)
snackString("${task.title} - ${task.chapter} Download finished") snackString("${task.title} - ${task.chapter} Download finished")
} }
} catch (e: Exception) { } catch (e: Exception) {
logger("Exception while downloading .epub: ${e.message}") Logger.log("Exception while downloading .epub: ${e.message}")
snackString("Exception while downloading .epub: ${e.message}") snackString("Exception while downloading .epub: ${e.message}")
FirebaseCrashlytics.getInstance().recordException(e) Injekt.get<CrashlyticsInterface>().logException(e)
broadcastDownloadFailed(task.originalLink) broadcastDownloadFailed(task.originalLink)
} }
} }
@OptIn(DelicateCoroutinesApi::class)
private fun saveMediaInfo(task: DownloadTask) { private fun saveMediaInfo(task: DownloadTask) {
GlobalScope.launch(Dispatchers.IO) { launchIO {
val directory = File( val directory = File(
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Novel/${task.title}" "Dantotsu/Novel/${task.title}"

View File

@@ -11,7 +11,8 @@ import androidx.media3.exoplayer.scheduler.Scheduler
import ani.dantotsu.R import ani.dantotsu.R
@UnstableApi @UnstableApi
class MyDownloadService : DownloadService(1, 1, "download_service", R.string.downloads, 0) { class ExoplayerDownloadService :
DownloadService(1, 2000, "download_service", R.string.downloads, 0) {
companion object { companion object {
private const val JOB_ID = 1 private const val JOB_ID = 1
private const val FOREGROUND_NOTIFICATION_ID = 1 private const val FOREGROUND_NOTIFICATION_ID = 1

View File

@@ -1,12 +1,21 @@
package ani.dantotsu.download.video package ani.dantotsu.download.video
import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Build
import android.util.Log
import androidx.annotation.OptIn
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.MimeTypes import androidx.media3.common.MimeTypes
import androidx.media3.common.TrackSelectionParameters
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.database.StandaloneDatabaseProvider import androidx.media3.database.StandaloneDatabaseProvider
import androidx.media3.datasource.DataSource import androidx.media3.datasource.DataSource
@@ -15,19 +24,26 @@ import androidx.media3.datasource.cache.NoOpCacheEvictor
import androidx.media3.datasource.cache.SimpleCache import androidx.media3.datasource.cache.SimpleCache
import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.DefaultRenderersFactory import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.offline.Download
import androidx.media3.exoplayer.offline.DownloadHelper import androidx.media3.exoplayer.offline.DownloadHelper
import androidx.media3.exoplayer.offline.DownloadManager import androidx.media3.exoplayer.offline.DownloadManager
import androidx.media3.exoplayer.offline.DownloadService import androidx.media3.exoplayer.offline.DownloadService
import androidx.media3.exoplayer.scheduler.Requirements import androidx.media3.exoplayer.scheduler.Requirements
import androidx.media3.ui.TrackSelectionDialogBuilder
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.defaultHeaders import ani.dantotsu.defaultHeaders
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.anime.AnimeDownloaderService
import ani.dantotsu.download.anime.AnimeServiceDataSingleton
import ani.dantotsu.logError import ani.dantotsu.logError
import ani.dantotsu.media.Media
import ani.dantotsu.okHttpClient import ani.dantotsu.okHttpClient
import ani.dantotsu.parsers.Subtitle import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.SubtitleType import ani.dantotsu.parsers.SubtitleType
import ani.dantotsu.parsers.Video import ani.dantotsu.parsers.Video
import ani.dantotsu.parsers.VideoType import ani.dantotsu.parsers.VideoType
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@@ -37,6 +53,8 @@ import java.util.concurrent.*
object Helper { object Helper {
private var simpleCache: SimpleCache? = null
@SuppressLint("UnsafeOptInUsageError") @SuppressLint("UnsafeOptInUsageError")
fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) { fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) {
val dataSourceFactory = DataSource.Factory { val dataSourceFactory = DataSource.Factory {
@@ -82,27 +100,13 @@ object Helper {
) )
downloadHelper.prepare(object : DownloadHelper.Callback { downloadHelper.prepare(object : DownloadHelper.Callback {
override fun onPrepared(helper: DownloadHelper) { override fun onPrepared(helper: DownloadHelper) {
TrackSelectionDialogBuilder( helper.getDownloadRequest(null).let {
context, "Select thingy", helper.getTracks(0).groups
) { _, overrides ->
val params = TrackSelectionParameters.Builder(context)
overrides.forEach {
params.addOverride(it.value)
}
helper.addTrackSelection(0, params.build())
MyDownloadService
DownloadService.sendAddDownload( DownloadService.sendAddDownload(
context, context,
MyDownloadService::class.java, ExoplayerDownloadService::class.java,
helper.getDownloadRequest(null), it,
false false
) )
}.apply {
setTheme(R.style.DialogTheme)
setTrackNameProvider {
if (it.frameRate > 0f) it.height.toString() + "p" else it.height.toString() + "p (fps : N/A)"
}
build().show()
} }
} }
@@ -114,13 +118,13 @@ object Helper {
private var download: DownloadManager? = null private var download: DownloadManager? = null
private const val DOWNLOAD_CONTENT_DIRECTORY = "downloads" private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads"
@Synchronized @Synchronized
@UnstableApi @UnstableApi
fun downloadManager(context: Context): DownloadManager { fun downloadManager(context: Context): DownloadManager {
return download ?: let { return download ?: let {
val database = StandaloneDatabaseProvider(context) val database = Injekt.get<StandaloneDatabaseProvider>()
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY) val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
val dataSourceFactory = DataSource.Factory { val dataSourceFactory = DataSource.Factory {
//val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource() //val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource()
@@ -133,17 +137,42 @@ object Helper {
} }
dataSource dataSource
} }
DownloadManager( val threadPoolSize = Runtime.getRuntime().availableProcessors()
val executorService = Executors.newFixedThreadPool(threadPoolSize)
val downloadManager = DownloadManager(
context, context,
database, database,
SimpleCache(downloadDirectory, NoOpCacheEvictor(), database), getSimpleCache(context),
dataSourceFactory, dataSourceFactory,
Executor(Runnable::run) executorService
).apply { ).apply {
requirements = requirements =
Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW) Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW)
maxParallelDownloads = 3 maxParallelDownloads = 3
} }
downloadManager.addListener( //for testing
object : DownloadManager.Listener {
override fun onDownloadChanged(
downloadManager: DownloadManager,
download: Download,
finalException: Exception?
) {
if (download.state == Download.STATE_COMPLETED) {
Logger.log("Download Completed")
} else if (download.state == Download.STATE_FAILED) {
Logger.log("Download Failed")
} else if (download.state == Download.STATE_STOPPED) {
Logger.log("Download Stopped")
} else if (download.state == Download.STATE_QUEUED) {
Logger.log("Download Queued")
} else if (download.state == Download.STATE_DOWNLOADING) {
Logger.log("Download Downloading")
}
}
}
)
downloadManager
} }
} }
@@ -159,4 +188,102 @@ object Helper {
} }
return downloadDirectory!! return downloadDirectory!!
} }
@OptIn(UnstableApi::class)
fun startAnimeDownloadService(
context: Context,
title: String,
episode: String,
video: Video,
subtitle: Subtitle? = null,
sourceMedia: Media? = null,
episodeImage: String? = null
) {
if (!isNotificationPermissionGranted(context)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ActivityCompat.requestPermissions(
context as Activity,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
1
)
}
}
val animeDownloadTask = AnimeDownloaderService.AnimeDownloadTask(
title,
episode,
video,
subtitle,
sourceMedia,
episodeImage
)
val downloadsManger = Injekt.get<DownloadsManager>()
val downloadCheck = downloadsManger
.queryDownload(title, episode, DownloadedType.Type.ANIME)
if (downloadCheck) {
AlertDialog.Builder(context, R.style.MyPopup)
.setTitle("Download Exists")
.setMessage("A download for this episode already exists. Do you want to overwrite it?")
.setPositiveButton("Yes") { _, _ ->
DownloadService.sendRemoveDownload(
context,
ExoplayerDownloadService::class.java,
PrefManager.getAnimeDownloadPreferences().getString(
animeDownloadTask.getTaskName(),
""
) ?: "",
false
)
PrefManager.getAnimeDownloadPreferences().edit()
.remove(animeDownloadTask.getTaskName())
.apply()
downloadsManger.removeDownload(
DownloadedType(
title,
episode,
DownloadedType.Type.ANIME
)
)
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
if (!AnimeServiceDataSingleton.isServiceRunning) {
val intent = Intent(context, AnimeDownloaderService::class.java)
ContextCompat.startForegroundService(context, intent)
AnimeServiceDataSingleton.isServiceRunning = true
}
}
.setNegativeButton("No") { _, _ -> }
.show()
} else {
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
if (!AnimeServiceDataSingleton.isServiceRunning) {
val intent = Intent(context, AnimeDownloaderService::class.java)
ContextCompat.startForegroundService(context, intent)
AnimeServiceDataSingleton.isServiceRunning = true
}
}
}
@OptIn(UnstableApi::class)
fun getSimpleCache(context: Context): SimpleCache {
return if (simpleCache == null) {
val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY)
val database = Injekt.get<StandaloneDatabaseProvider>()
simpleCache = SimpleCache(downloadDirectory, NoOpCacheEvictor(), database)
simpleCache!!
} else {
simpleCache!!
}
}
private fun isNotificationPermissionGranted(context: Context): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
return ActivityCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
}
return true
}
} }

View File

@@ -27,13 +27,13 @@ import ani.dantotsu.connections.anilist.AnilistAnimeViewModel
import ani.dantotsu.connections.anilist.SearchResults import ani.dantotsu.connections.anilist.SearchResults
import ani.dantotsu.connections.anilist.getUserId import ani.dantotsu.connections.anilist.getUserId
import ani.dantotsu.databinding.FragmentAnimeBinding import ani.dantotsu.databinding.FragmentAnimeBinding
import ani.dantotsu.loadData
import ani.dantotsu.media.MediaAdaptor import ani.dantotsu.media.MediaAdaptor
import ani.dantotsu.media.ProgressAdapter import ani.dantotsu.media.ProgressAdapter
import ani.dantotsu.media.SearchActivity import ani.dantotsu.media.SearchActivity
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.px import ani.dantotsu.px
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -47,9 +47,7 @@ import kotlin.math.min
class AnimeFragment : Fragment() { class AnimeFragment : Fragment() {
private var _binding: FragmentAnimeBinding? = null private var _binding: FragmentAnimeBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var animePageAdapter: AnimePageAdapter
private var uiSettings: UserInterfaceSettings =
loadData("ui_settings") ?: UserInterfaceSettings()
val model: AnilistAnimeViewModel by activityViewModels() val model: AnilistAnimeViewModel by activityViewModels()
@@ -95,7 +93,7 @@ class AnimeFragment : Fragment() {
binding.animePageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px) binding.animePageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px)
val animePageAdapter = AnimePageAdapter() animePageAdapter = AnimePageAdapter()
var loading = true var loading = true
if (model.notSet) { if (model.notSet) {
@@ -215,7 +213,7 @@ class AnimeFragment : Fragment() {
if (it != null) { if (it != null) {
animePageAdapter.updateTrending( animePageAdapter.updateTrending(
MediaAdaptor( MediaAdaptor(
if (uiSettings.smallView) 3 else 2, if (PrefManager.getVal(PrefName.SmallView)) 3 else 2,
it, it,
requireActivity(), requireActivity(),
viewPager = animePageAdapter.trendingViewPager viewPager = animePageAdapter.trendingViewPager
@@ -266,7 +264,11 @@ class AnimeFragment : Fragment() {
model.loaded = true model.loaded = true
model.loadTrending(1) model.loadTrending(1)
model.loadUpdated() model.loadUpdated()
model.loadPopular("ANIME", sort = Anilist.sortBy[1]) model.loadPopular(
"ANIME", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
PrefName.PopularAnimeList
)
)
} }
live.postValue(false) live.postValue(false)
_binding?.animeRefresh?.isRefreshing = false _binding?.animeRefresh?.isRefreshing = false
@@ -277,6 +279,13 @@ class AnimeFragment : Fragment() {
override fun onResume() { override fun onResume() {
if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true) if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true)
if (animePageAdapter.trendingViewPager != null) {
binding.root.requestApplyInsets()
binding.root.requestLayout()
}
if (this::animePageAdapter.isInitialized && _binding != null) {
animePageAdapter.updateNotificationCount()
}
super.onResume() super.onResume()
} }
} }

View File

@@ -1,6 +1,5 @@
package ani.dantotsu.home package ani.dantotsu.home
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
@@ -22,18 +21,19 @@ import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemAnimePageBinding import ani.dantotsu.databinding.ItemAnimePageBinding
import ani.dantotsu.loadData
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
import ani.dantotsu.media.CalendarActivity import ani.dantotsu.media.CalendarActivity
import ani.dantotsu.media.GenreActivity import ani.dantotsu.media.GenreActivity
import ani.dantotsu.media.MediaAdaptor import ani.dantotsu.media.MediaAdaptor
import ani.dantotsu.media.SearchActivity import ani.dantotsu.media.SearchActivity
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.px import ani.dantotsu.px
import ani.dantotsu.setSafeOnClickListener import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.setSlideIn import ani.dantotsu.setSlideIn
import ani.dantotsu.setSlideUp import ani.dantotsu.setSlideUp
import ani.dantotsu.settings.SettingsDialogFragment import ani.dantotsu.settings.SettingsDialogFragment
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
@@ -44,8 +44,6 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
private var trendHandler: Handler? = null private var trendHandler: Handler? = null
private lateinit var trendRun: Runnable private lateinit var trendRun: Runnable
var trendingViewPager: ViewPager2? = null var trendingViewPager: ViewPager2? = null
private var uiSettings: UserInterfaceSettings =
loadData("ui_settings") ?: UserInterfaceSettings()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnimePageViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnimePageViewHolder {
val binding = val binding =
@@ -68,17 +66,12 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true) currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
val color = typedValue.data val color = typedValue.data
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
?.getBoolean("colorOverflow", false) ?: false
if (!colorOverflow) {
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
}
binding.animeTitleContainer.updatePadding(top = statusBarHeight) binding.animeTitleContainer.updatePadding(top = statusBarHeight)
if (uiSettings.smallView) binding.animeTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { if (PrefManager.getVal(PrefName.SmallView)) binding.animeTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = (-108f).px bottomMargin = (-108f).px
} }
@@ -102,6 +95,17 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.ANIME) SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.ANIME)
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog") dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
} }
binding.animeUserAvatar.setOnLongClickListener { view ->
ContextCompat.startActivity(
view.context,
Intent(view.context, ProfileActivity::class.java)
.putExtra("userId", Anilist.userid),null
)
false
}
binding.animeNotificationCount.visibility = if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
binding.animeNotificationCount.text = Anilist.unreadNotificationCount.toString()
listOf( listOf(
binding.animePreviousSeason, binding.animePreviousSeason,
@@ -132,8 +136,13 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
binding.animeIncludeList.visibility = binding.animeIncludeList.visibility =
if (Anilist.userid != null) View.VISIBLE else View.GONE if (Anilist.userid != null) View.VISIBLE else View.GONE
binding.animeIncludeList.isChecked = PrefManager.getVal(PrefName.PopularAnimeList)
binding.animeIncludeList.setOnCheckedChangeListener { _, isChecked -> binding.animeIncludeList.setOnCheckedChangeListener { _, isChecked ->
onIncludeListClick.invoke(isChecked) onIncludeListClick.invoke(isChecked)
PrefManager.setVal(PrefName.PopularAnimeList, isChecked)
} }
if (ready.value == false) if (ready.value == false)
ready.postValue(true) ready.postValue(true)
@@ -172,12 +181,12 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
) )
binding.animeTrendingViewPager.layoutAnimation = binding.animeTrendingViewPager.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f) LayoutAnimationController(setSlideIn(), 0.25f)
binding.animeTitleContainer.startAnimation(setSlideUp(uiSettings)) binding.animeTitleContainer.startAnimation(setSlideUp())
binding.animeListContainer.layoutAnimation = binding.animeListContainer.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f) LayoutAnimationController(setSlideIn(), 0.25f)
binding.animeSeasonsCont.layoutAnimation = binding.animeSeasonsCont.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f) LayoutAnimationController(setSlideIn(), 0.25f)
} }
fun updateRecent(adaptor: MediaAdaptor) { fun updateRecent(adaptor: MediaAdaptor) {
@@ -192,11 +201,11 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
binding.animeUpdatedRecyclerView.visibility = View.VISIBLE binding.animeUpdatedRecyclerView.visibility = View.VISIBLE
binding.animeRecently.visibility = View.VISIBLE binding.animeRecently.visibility = View.VISIBLE
binding.animeRecently.startAnimation(setSlideUp(uiSettings)) binding.animeRecently.startAnimation(setSlideUp())
binding.animeUpdatedRecyclerView.layoutAnimation = binding.animeUpdatedRecyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f) LayoutAnimationController(setSlideIn(), 0.25f)
binding.animePopular.visibility = View.VISIBLE binding.animePopular.visibility = View.VISIBLE
binding.animePopular.startAnimation(setSlideUp(uiSettings)) binding.animePopular.startAnimation(setSlideUp())
} }
fun updateAvatar() { fun updateAvatar() {
@@ -206,6 +215,14 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
} }
} }
fun updateNotificationCount() {
if (this::binding.isInitialized) {
binding.animeNotificationCount.visibility =
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
binding.animeNotificationCount.text = Anilist.unreadNotificationCount.toString()
}
}
inner class AnimePageViewHolder(val binding: ItemAnimePageBinding) : inner class AnimePageViewHolder(val binding: ItemAnimePageBinding) :
RecyclerView.ViewHolder(binding.root) RecyclerView.ViewHolder(binding.root)
} }

View File

@@ -21,23 +21,25 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.Refresh import ani.dantotsu.Refresh
import ani.dantotsu.blurImage
import ani.dantotsu.bottomBar import ani.dantotsu.bottomBar
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistHomeViewModel import ani.dantotsu.connections.anilist.AnilistHomeViewModel
import ani.dantotsu.connections.anilist.getUserId import ani.dantotsu.connections.anilist.getUserId
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.databinding.FragmentHomeBinding import ani.dantotsu.databinding.FragmentHomeBinding
import ani.dantotsu.loadData
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaAdaptor import ani.dantotsu.media.MediaAdaptor
import ani.dantotsu.media.user.ListActivity import ani.dantotsu.media.user.ListActivity
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.setSafeOnClickListener import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.setSlideIn import ani.dantotsu.setSlideIn
import ani.dantotsu.setSlideUp import ani.dantotsu.setSlideUp
import ani.dantotsu.settings.SettingsDialogFragment import ani.dantotsu.settings.SettingsDialogFragment
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -70,16 +72,17 @@ class HomeFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val scope = lifecycleScope val scope = lifecycleScope
var uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
fun load() { fun load() {
if (activity != null && _binding != null) lifecycleScope.launch(Dispatchers.Main) { if (activity != null && _binding != null) lifecycleScope.launch(Dispatchers.Main) {
binding.homeUserName.text = Anilist.username binding.homeUserName.text = Anilist.username
binding.homeUserEpisodesWatched.text = Anilist.episodesWatched.toString() binding.homeUserEpisodesWatched.text = Anilist.episodesWatched.toString()
binding.homeUserChaptersRead.text = Anilist.chapterRead.toString() binding.homeUserChaptersRead.text = Anilist.chapterRead.toString()
binding.homeUserAvatar.loadImage(Anilist.avatar) binding.homeUserAvatar.loadImage(Anilist.avatar)
if (!uiSettings.bannerAnimations) binding.homeUserBg.pause() if (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean)) binding.homeUserBg.pause()
binding.homeUserBg.loadImage(Anilist.bg) blurImage(binding.homeUserBg, Anilist.bg)
binding.homeUserDataProgressBar.visibility = View.GONE binding.homeUserDataProgressBar.visibility = View.GONE
binding.homeNotificationCount.visibility = if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
binding.homeAnimeList.setOnClickListener { binding.homeAnimeList.setOnClickListener {
ContextCompat.startActivity( ContextCompat.startActivity(
@@ -98,20 +101,19 @@ class HomeFragment : Fragment() {
) )
} }
binding.homeUserAvatarContainer.startAnimation(setSlideUp(uiSettings)) binding.homeUserAvatarContainer.startAnimation(setSlideUp())
binding.homeUserDataContainer.visibility = View.VISIBLE binding.homeUserDataContainer.visibility = View.VISIBLE
binding.homeUserDataContainer.layoutAnimation = binding.homeUserDataContainer.layoutAnimation =
LayoutAnimationController(setSlideUp(uiSettings), 0.25f) LayoutAnimationController(setSlideUp(), 0.25f)
binding.homeAnimeList.visibility = View.VISIBLE binding.homeAnimeList.visibility = View.VISIBLE
binding.homeMangaList.visibility = View.VISIBLE binding.homeMangaList.visibility = View.VISIBLE
binding.homeListContainer.layoutAnimation = binding.homeListContainer.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f) LayoutAnimationController(setSlideIn(), 0.25f)
} }
else { else {
snackString(currContext()?.getString(R.string.please_reload)) snackString(currContext()?.getString(R.string.please_reload))
} }
} }
binding.homeUserAvatarContainer.setSafeOnClickListener { binding.homeUserAvatarContainer.setSafeOnClickListener {
val dialogFragment = val dialogFragment =
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.HOME) SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.HOME)
@@ -120,6 +122,13 @@ class HomeFragment : Fragment() {
"dialog" "dialog"
) )
} }
binding.homeUserAvatarContainer.setOnLongClickListener {
ContextCompat.startActivity(
requireContext(), Intent(requireContext(), ProfileActivity::class.java)
.putExtra("userId", Anilist.userid),null
)
false
}
binding.homeContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { binding.homeContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight bottomMargin = navBarHeight
@@ -128,18 +137,21 @@ class HomeFragment : Fragment() {
binding.homeTopContainer.updatePadding(top = statusBarHeight) binding.homeTopContainer.updatePadding(top = statusBarHeight)
var reached = false var reached = false
val duration = (uiSettings.animationSpeed * 200).toLong() val duration = ((PrefManager.getVal(PrefName.AnimationSpeed) as Float) * 200).toLong()
binding.homeScroll.setOnScrollChangeListener { _, _, _, _, _ ->
if (!binding.homeScroll.canScrollVertically(1)) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
reached = true binding.homeScroll.setOnScrollChangeListener { _, _, _, _, _ ->
bottomBar.animate().translationZ(0f).setDuration(duration).start() if (!binding.homeScroll.canScrollVertically(1)) {
ObjectAnimator.ofFloat(bottomBar, "elevation", 4f, 0f).setDuration(duration) reached = true
.start() bottomBar.animate().translationZ(0f).setDuration(duration).start()
} else { ObjectAnimator.ofFloat(bottomBar, "elevation", 4f, 0f).setDuration(duration)
if (reached) {
bottomBar.animate().translationZ(12f).setDuration(duration).start()
ObjectAnimator.ofFloat(bottomBar, "elevation", 0f, 4f).setDuration(duration)
.start() .start()
} else {
if (reached) {
bottomBar.animate().translationZ(12f).setDuration(duration).start()
ObjectAnimator.ofFloat(bottomBar, "elevation", 0f, 4f).setDuration(duration)
.start()
}
} }
} }
} }
@@ -207,13 +219,13 @@ class HomeFragment : Fragment() {
) )
recyclerView.visibility = View.VISIBLE recyclerView.visibility = View.VISIBLE
recyclerView.layoutAnimation = recyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f) LayoutAnimationController(setSlideIn(), 0.25f)
} else { } else {
empty.visibility = View.VISIBLE empty.visibility = View.VISIBLE
} }
title.visibility = View.VISIBLE title.visibility = View.VISIBLE
title.startAnimation(setSlideUp(uiSettings)) title.startAnimation(setSlideUp())
progress.visibility = View.GONE progress.visibility = View.GONE
} }
} }
@@ -296,25 +308,26 @@ class HomeFragment : Fragment() {
binding.homeRecommended binding.homeRecommended
) )
binding.homeUserAvatarContainer.startAnimation(setSlideUp(uiSettings)) binding.homeUserAvatarContainer.startAnimation(setSlideUp())
model.empty.observe(viewLifecycleOwner) { model.empty.observe(viewLifecycleOwner) {
binding.homeDantotsuContainer.visibility = if (it == true) View.VISIBLE else View.GONE binding.homeDantotsuContainer.visibility = if (it == true) View.VISIBLE else View.GONE
(binding.homeDantotsuIcon.drawable as Animatable).start() (binding.homeDantotsuIcon.drawable as Animatable).start()
binding.homeDantotsuContainer.startAnimation(setSlideUp(uiSettings)) binding.homeDantotsuContainer.startAnimation(setSlideUp())
binding.homeDantotsuIcon.setSafeOnClickListener { binding.homeDantotsuIcon.setSafeOnClickListener {
(binding.homeDantotsuIcon.drawable as Animatable).start() (binding.homeDantotsuIcon.drawable as Animatable).start()
} }
} }
val array = arrayOf( val array = arrayOf(
Runnable { runBlocking { model.setAnimeContinue() } }, "AnimeContinue",
Runnable { runBlocking { model.setAnimeFav() } }, "AnimeFav",
Runnable { runBlocking { model.setAnimePlanned() } }, "AnimePlanned",
Runnable { runBlocking { model.setMangaContinue() } }, "MangaContinue",
Runnable { runBlocking { model.setMangaFav() } }, "MangaFav",
Runnable { runBlocking { model.setMangaPlanned() } }, "MangaPlanned",
Runnable { runBlocking { model.setRecommendation() } } "Recommendation"
) )
val containers = arrayOf( val containers = arrayOf(
@@ -331,8 +344,6 @@ class HomeFragment : Fragment() {
live.observe(viewLifecycleOwner) { live.observe(viewLifecycleOwner) {
if (it) { if (it) {
scope.launch { scope.launch {
uiSettings =
loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
//Get userData First //Get userData First
getUserId(requireContext()) { getUserId(requireContext()) {
@@ -341,9 +352,13 @@ class HomeFragment : Fragment() {
model.loaded = true model.loaded = true
model.setListImages() model.setListImages()
var empty = true var empty = true
val homeLayoutShow: List<Boolean> =
PrefManager.getVal(PrefName.HomeLayoutShow)
runBlocking {
model.initHomePage()
}
(array.indices).forEach { i -> (array.indices).forEach { i ->
if (uiSettings.homeLayoutShow[i]) { if (homeLayoutShow.elementAt(i)) {
array[i].run()
empty = false empty = false
} else withContext(Dispatchers.Main) { } else withContext(Dispatchers.Main) {
containers[i].visibility = View.GONE containers[i].visibility = View.GONE
@@ -357,9 +372,12 @@ class HomeFragment : Fragment() {
} }
} }
} }
override fun onResume() { override fun onResume() {
if (!model.loaded) Refresh.activity[1]!!.postValue(true) if (!model.loaded) Refresh.activity[1]!!.postValue(true)
if (_binding != null) {
binding.homeNotificationCount.visibility = if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
}
super.onResume() super.onResume()
} }
} }

View File

@@ -1,14 +1,24 @@
package ani.dantotsu.home package ani.dantotsu.home
import android.app.AlertDialog
import android.content.Intent
import android.os.Bundle 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 android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.FragmentLoginBinding import ani.dantotsu.databinding.FragmentLoginBinding
import ani.dantotsu.openLinkInBrowser import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferencePackager
import ani.dantotsu.toast
import ani.dantotsu.util.Logger
import com.google.android.material.textfield.TextInputEditText
class LoginFragment : Fragment() { class LoginFragment : Fragment() {
@@ -28,5 +38,100 @@ class LoginFragment : Fragment() {
binding.loginButton.setOnClickListener { Anilist.loginIntent(requireActivity()) } binding.loginButton.setOnClickListener { Anilist.loginIntent(requireActivity()) }
binding.loginDiscord.setOnClickListener { openLinkInBrowser(getString(R.string.discord)) } binding.loginDiscord.setOnClickListener { openLinkInBrowser(getString(R.string.discord)) }
binding.loginGithub.setOnClickListener { openLinkInBrowser(getString(R.string.github)) } binding.loginGithub.setOnClickListener { openLinkInBrowser(getString(R.string.github)) }
binding.loginTelegram.setOnClickListener { openLinkInBrowser(getString(R.string.telegram)) }
val openDocumentLauncher =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
if (uri != null) {
try {
val jsonString =
requireActivity().contentResolver.openInputStream(uri)?.readBytes()
?: throw Exception("Error reading file")
val name =
DocumentFile.fromSingleUri(requireActivity(), uri)?.name ?: "settings"
//.sani is encrypted, .ani is not
if (name.endsWith(".sani")) {
passwordAlertDialog { password ->
if (password != null) {
val salt = jsonString.copyOfRange(0, 16)
val encrypted = jsonString.copyOfRange(16, jsonString.size)
val decryptedJson = try {
PreferenceKeystore.decryptWithPassword(
password,
encrypted,
salt
)
} catch (e: Exception) {
toast("Incorrect password")
return@passwordAlertDialog
}
if (PreferencePackager.unpack(decryptedJson))
restartApp()
} else {
toast("Password cannot be empty")
}
}
} else if (name.endsWith(".ani")) {
val decryptedJson = jsonString.toString(Charsets.UTF_8)
if (PreferencePackager.unpack(decryptedJson))
restartApp()
} else {
toast("Invalid file type")
}
} catch (e: Exception) {
Logger.log(e)
toast("Error importing settings")
}
}
}
binding.importSettingsButton.setOnClickListener {
openDocumentLauncher.launch(arrayOf("*/*"))
}
} }
private fun passwordAlertDialog(callback: (CharArray?) -> Unit) {
val password = CharArray(16).apply { fill('0') }
// Inflate the dialog layout
val dialogView =
LayoutInflater.from(requireActivity()).inflate(R.layout.dialog_user_agent, null)
dialogView.findViewById<TextInputEditText>(R.id.userAgentTextBox)?.hint = "Password"
val subtitleTextView = dialogView.findViewById<TextView>(R.id.subtitle)
subtitleTextView?.visibility = View.VISIBLE
subtitleTextView?.text = "Enter your password to decrypt the file"
val dialog = AlertDialog.Builder(requireActivity(), R.style.MyPopup)
.setTitle("Enter Password")
.setView(dialogView)
.setPositiveButton("OK", null)
.setNegativeButton("Cancel") { dialog, _ ->
password.fill('0')
dialog.dismiss()
callback(null)
}
.create()
dialog.window?.setDimAmount(0.8f)
dialog.show()
// Override the positive button here
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val editText = dialog.findViewById<TextInputEditText>(R.id.userAgentTextBox)
if (editText?.text?.isNotBlank() == true) {
editText.text?.toString()?.trim()?.toCharArray(password)
dialog.dismiss()
callback(password)
} else {
toast("Password cannot be empty")
}
}
}
private fun restartApp() {
val intent = Intent(requireActivity(), requireActivity().javaClass)
requireActivity().finish()
startActivity(intent)
}
} }

View File

@@ -25,12 +25,12 @@ import ani.dantotsu.connections.anilist.AnilistMangaViewModel
import ani.dantotsu.connections.anilist.SearchResults import ani.dantotsu.connections.anilist.SearchResults
import ani.dantotsu.connections.anilist.getUserId import ani.dantotsu.connections.anilist.getUserId
import ani.dantotsu.databinding.FragmentMangaBinding import ani.dantotsu.databinding.FragmentMangaBinding
import ani.dantotsu.loadData
import ani.dantotsu.media.MediaAdaptor import ani.dantotsu.media.MediaAdaptor
import ani.dantotsu.media.ProgressAdapter import ani.dantotsu.media.ProgressAdapter
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.px import ani.dantotsu.px
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -43,9 +43,7 @@ import kotlin.math.min
class MangaFragment : Fragment() { class MangaFragment : Fragment() {
private var _binding: FragmentMangaBinding? = null private var _binding: FragmentMangaBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var mangaPageAdapter: MangaPageAdapter
private var uiSettings: UserInterfaceSettings =
loadData("ui_settings") ?: UserInterfaceSettings()
val model: AnilistMangaViewModel by activityViewModels() val model: AnilistMangaViewModel by activityViewModels()
@@ -90,7 +88,7 @@ class MangaFragment : Fragment() {
binding.mangaPageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px) binding.mangaPageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px)
val mangaPageAdapter = MangaPageAdapter() mangaPageAdapter = MangaPageAdapter()
var loading = true var loading = true
if (model.notSet) { if (model.notSet) {
model.notSet = false model.notSet = false
@@ -173,7 +171,7 @@ class MangaFragment : Fragment() {
if (it != null) { if (it != null) {
mangaPageAdapter.updateTrending( mangaPageAdapter.updateTrending(
MediaAdaptor( MediaAdaptor(
if (uiSettings.smallView) 3 else 2, if (PrefManager.getVal(PrefName.SmallView)) 3 else 2,
it, it,
requireActivity(), requireActivity(),
viewPager = mangaPageAdapter.trendingViewPager viewPager = mangaPageAdapter.trendingViewPager
@@ -240,7 +238,11 @@ class MangaFragment : Fragment() {
model.loaded = true model.loaded = true
model.loadTrending() model.loadTrending()
model.loadTrendingNovel() model.loadTrendingNovel()
model.loadPopular("MANGA", sort = Anilist.sortBy[1]) model.loadPopular(
"MANGA", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
PrefName.PopularMangaList
)
)
} }
live.postValue(false) live.postValue(false)
_binding?.mangaRefresh?.isRefreshing = false _binding?.mangaRefresh?.isRefreshing = false
@@ -251,6 +253,14 @@ class MangaFragment : Fragment() {
override fun onResume() { override fun onResume() {
if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true) if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true)
//make sure mangaPageAdapter is initialized
if (mangaPageAdapter.trendingViewPager != null) {
binding.root.requestApplyInsets()
binding.root.requestLayout()
}
if (this::mangaPageAdapter.isInitialized && _binding != null) {
mangaPageAdapter.updateNotificationCount()
}
super.onResume() super.onResume()
} }

View File

@@ -1,6 +1,5 @@
package ani.dantotsu.home package ani.dantotsu.home
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
@@ -22,17 +21,18 @@ import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemMangaPageBinding import ani.dantotsu.databinding.ItemMangaPageBinding
import ani.dantotsu.loadData
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
import ani.dantotsu.media.GenreActivity import ani.dantotsu.media.GenreActivity
import ani.dantotsu.media.MediaAdaptor import ani.dantotsu.media.MediaAdaptor
import ani.dantotsu.media.SearchActivity import ani.dantotsu.media.SearchActivity
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.px import ani.dantotsu.px
import ani.dantotsu.setSafeOnClickListener import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.setSlideIn import ani.dantotsu.setSlideIn
import ani.dantotsu.setSlideUp import ani.dantotsu.setSlideUp
import ani.dantotsu.settings.SettingsDialogFragment import ani.dantotsu.settings.SettingsDialogFragment
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
@@ -43,8 +43,6 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
private var trendHandler: Handler? = null private var trendHandler: Handler? = null
private lateinit var trendRun: Runnable private lateinit var trendRun: Runnable
var trendingViewPager: ViewPager2? = null var trendingViewPager: ViewPager2? = null
private var uiSettings: UserInterfaceSettings =
loadData("ui_settings") ?: UserInterfaceSettings()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MangaPageViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MangaPageViewHolder {
val binding = val binding =
@@ -67,22 +65,18 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true) currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
val color = typedValue.data val color = typedValue.data
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
?.getBoolean("colorOverflow", false) ?: false
if (!colorOverflow) {
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
}
binding.mangaTitleContainer.updatePadding(top = statusBarHeight) binding.mangaTitleContainer.updatePadding(top = statusBarHeight)
if (uiSettings.smallView) binding.mangaTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { if (PrefManager.getVal(PrefName.SmallView)) binding.mangaTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = (-108f).px bottomMargin = (-108f).px
} }
updateAvatar() updateAvatar()
binding.mangaNotificationCount.visibility = if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
binding.mangaNotificationCount.text = Anilist.unreadNotificationCount.toString()
binding.mangaSearchBar.hint = "MANGA" binding.mangaSearchBar.hint = "MANGA"
binding.mangaSearchBarText.setOnClickListener { binding.mangaSearchBarText.setOnClickListener {
ContextCompat.startActivity( ContextCompat.startActivity(
@@ -97,6 +91,14 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.MANGA) SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.MANGA)
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog") dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
} }
binding.mangaUserAvatar.setOnLongClickListener { view ->
ContextCompat.startActivity(
view.context,
Intent(view.context, ProfileActivity::class.java)
.putExtra("userId", Anilist.userid),null
)
false
}
binding.mangaSearchBar.setEndIconOnClickListener { binding.mangaSearchBar.setEndIconOnClickListener {
binding.mangaSearchBarText.performClick() binding.mangaSearchBarText.performClick()
@@ -125,10 +127,14 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
binding.mangaIncludeList.visibility = binding.mangaIncludeList.visibility =
if (Anilist.userid != null) View.VISIBLE else View.GONE if (Anilist.userid != null) View.VISIBLE else View.GONE
binding.mangaIncludeList.isChecked = PrefManager.getVal(PrefName.PopularMangaList)
binding.mangaIncludeList.setOnCheckedChangeListener { _, isChecked -> binding.mangaIncludeList.setOnCheckedChangeListener { _, isChecked ->
onIncludeListClick.invoke(isChecked) onIncludeListClick.invoke(isChecked)
}
PrefManager.setVal(PrefName.PopularMangaList, isChecked)
}
if (ready.value == false) if (ready.value == false)
ready.postValue(true) ready.postValue(true)
} }
@@ -149,8 +155,7 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
binding.mangaTrendingViewPager.setPageTransformer(MediaPageTransformer()) binding.mangaTrendingViewPager.setPageTransformer(MediaPageTransformer())
trendHandler = Handler(Looper.getMainLooper()) trendHandler = Handler(Looper.getMainLooper())
trendRun = Runnable { trendRun = Runnable {
binding.mangaTrendingViewPager.currentItem = binding.mangaTrendingViewPager.currentItem += 1
binding.mangaTrendingViewPager.currentItem + 1
} }
binding.mangaTrendingViewPager.registerOnPageChangeCallback( binding.mangaTrendingViewPager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() { object : ViewPager2.OnPageChangeCallback() {
@@ -163,10 +168,10 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
) )
binding.mangaTrendingViewPager.layoutAnimation = binding.mangaTrendingViewPager.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f) LayoutAnimationController(setSlideIn(), 0.25f)
binding.mangaTitleContainer.startAnimation(setSlideUp(uiSettings)) binding.mangaTitleContainer.startAnimation(setSlideUp())
binding.mangaListContainer.layoutAnimation = binding.mangaListContainer.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f) LayoutAnimationController(setSlideIn(), 0.25f)
} }
fun updateNovel(adaptor: MediaAdaptor) { fun updateNovel(adaptor: MediaAdaptor) {
@@ -181,11 +186,11 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
binding.mangaNovelRecyclerView.visibility = View.VISIBLE binding.mangaNovelRecyclerView.visibility = View.VISIBLE
binding.mangaNovel.visibility = View.VISIBLE binding.mangaNovel.visibility = View.VISIBLE
binding.mangaNovel.startAnimation(setSlideUp(uiSettings)) binding.mangaNovel.startAnimation(setSlideUp())
binding.mangaNovelRecyclerView.layoutAnimation = binding.mangaNovelRecyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f) LayoutAnimationController(setSlideIn(), 0.25f)
binding.mangaPopular.visibility = View.VISIBLE binding.mangaPopular.visibility = View.VISIBLE
binding.mangaPopular.startAnimation(setSlideUp(uiSettings)) binding.mangaPopular.startAnimation(setSlideUp())
} }
fun updateAvatar() { fun updateAvatar() {
@@ -195,6 +200,14 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
} }
} }
fun updateNotificationCount() {
if (this::binding.isInitialized) {
binding.mangaNotificationCount.visibility =
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
binding.mangaNotificationCount.text = Anilist.unreadNotificationCount.toString()
}
}
inner class MangaPageViewHolder(val binding: ItemMangaPageBinding) : inner class MangaPageViewHolder(val binding: ItemMangaPageBinding) :
RecyclerView.ViewHolder(binding.root) RecyclerView.ViewHolder(binding.root)
} }

View File

@@ -1,11 +1,13 @@
package ani.dantotsu.home package ani.dantotsu.home
import android.content.Context
import android.graphics.drawable.GradientDrawable import android.graphics.drawable.GradientDrawable
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.doOnAttach import androidx.core.view.doOnAttach
@@ -17,49 +19,57 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.ZoomOutPageTransformer import ani.dantotsu.ZoomOutPageTransformer
import ani.dantotsu.databinding.ActivityNoInternetBinding import ani.dantotsu.databinding.ActivityNoInternetBinding
import ani.dantotsu.download.anime.OfflineAnimeFragment
import ani.dantotsu.download.manga.OfflineMangaFragment import ani.dantotsu.download.manga.OfflineMangaFragment
import ani.dantotsu.initActivity import ani.dantotsu.initActivity
import ani.dantotsu.loadData
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.offline.OfflineFragment import ani.dantotsu.offline.OfflineFragment
import ani.dantotsu.others.LangSet
import ani.dantotsu.selectedOption import ani.dantotsu.selectedOption
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import nl.joery.animatedbottombar.AnimatedBottomBar import nl.joery.animatedbottombar.AnimatedBottomBar
class NoInternet : AppCompatActivity() { class NoInternet : AppCompatActivity() {
private lateinit var binding: ActivityNoInternetBinding private lateinit var binding: ActivityNoInternetBinding
lateinit var bottomBar: AnimatedBottomBar
private var uiSettings = UserInterfaceSettings()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
binding = ActivityNoInternetBinding.inflate(layoutInflater) binding = ActivityNoInternetBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar) val bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val backgroundDrawable = _bottomBar.background as GradientDrawable val backgroundDrawable = bottomBar.background as GradientDrawable
val currentColor = backgroundDrawable.color?.defaultColor ?: 0 val currentColor = backgroundDrawable.color?.defaultColor ?: 0
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xE8000000.toInt() val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xE8000000.toInt()
backgroundDrawable.setColor(semiTransparentColor) backgroundDrawable.setColor(semiTransparentColor)
_bottomBar.background = backgroundDrawable bottomBar.background = backgroundDrawable
} }
val colorOverflow = this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
.getBoolean("colorOverflow", false)
if (!colorOverflow) {
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
var doubleBackToExitPressedOnce = false
onBackPressedDispatcher.addCallback(this) {
if (doubleBackToExitPressedOnce) {
finishAffinity()
}
doubleBackToExitPressedOnce = true
snackString(this@NoInternet.getString(R.string.back_to_exit))
Handler(Looper.getMainLooper()).postDelayed(
{ doubleBackToExitPressedOnce = false },
2000
)
} }
binding.root.doOnAttach { binding.root.doOnAttach {
initActivity(this) initActivity(this)
uiSettings = loadData("ui_settings") ?: uiSettings selectedOption = PrefManager.getVal(PrefName.DefaultStartUpTab)
selectedOption = uiSettings.defaultStartUpTab
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight bottomMargin = navBarHeight
} }
@@ -70,7 +80,7 @@ class NoInternet : AppCompatActivity() {
val mainViewPager = binding.viewpager val mainViewPager = binding.viewpager
mainViewPager.isUserInputEnabled = false mainViewPager.isUserInputEnabled = false
mainViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle) mainViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle)
mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings)) mainViewPager.setPageTransformer(ZoomOutPageTransformer())
navbar.setOnTabSelectListener(object : navbar.setOnTabSelectListener(object :
AnimatedBottomBar.OnTabSelectListener { AnimatedBottomBar.OnTabSelectListener {
override fun onTabSelected( override fun onTabSelected(
@@ -96,12 +106,11 @@ class NoInternet : AppCompatActivity() {
override fun getItemCount(): Int = 3 override fun getItemCount(): Int = 3
override fun createFragment(position: Int): Fragment { override fun createFragment(position: Int): Fragment {
when (position) { return when (position) {
0 -> return OfflineFragment() 0 -> OfflineAnimeFragment()
1 -> return OfflineFragment() 2 -> OfflineMangaFragment()
2 -> return OfflineMangaFragment() else -> OfflineFragment()
} }
return LoginFragment()
} }
} }
} }

View File

@@ -3,7 +3,9 @@ package ani.dantotsu.media
import java.io.Serializable import java.io.Serializable
data class Author( data class Author(
val id: String, val id: Int,
val name: String, val name: String?,
val image: String?,
val role: String?,
var yearMedia: MutableMap<String, ArrayList<Media>>? = null var yearMedia: MutableMap<String, ArrayList<Media>>? = null
) : Serializable ) : Serializable

View File

@@ -18,7 +18,6 @@ import ani.dantotsu.Refresh
import ani.dantotsu.databinding.ActivityAuthorBinding import ani.dantotsu.databinding.ActivityAuthorBinding
import ani.dantotsu.initActivity import ani.dantotsu.initActivity
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.others.LangSet
import ani.dantotsu.others.getSerialized import ani.dantotsu.others.getSerialized
import ani.dantotsu.px import ani.dantotsu.px
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
@@ -36,7 +35,7 @@ class AuthorActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
binding = ActivityAuthorBinding.inflate(layoutInflater) binding = ActivityAuthorBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)

View File

@@ -0,0 +1,60 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
import androidx.core.util.Pair
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.databinding.ItemCharacterBinding
import ani.dantotsu.loadImage
import ani.dantotsu.setAnimation
import java.io.Serializable
class AuthorAdapter(
private val authorList: ArrayList<Author>
) : RecyclerView.Adapter<AuthorAdapter.AuthorViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AuthorViewHolder {
val binding =
ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return AuthorViewHolder(binding)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder:AuthorViewHolder, position: Int) {
val binding = holder.binding
setAnimation(binding.root.context, holder.binding.root)
val author = authorList[position]
binding.itemCompactRelation.text = author.role
binding.itemCompactImage.loadImage(author.image)
binding.itemCompactTitle.text = author.name
}
override fun getItemCount(): Int = authorList.size
inner class AuthorViewHolder(val binding: ItemCharacterBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
itemView.setOnClickListener {
val author = authorList[bindingAdapterPosition]
ContextCompat.startActivity(
itemView.context,
Intent(
itemView.context,
AuthorActivity::class.java
).putExtra("author", author as Serializable),
ActivityOptionsCompat.makeSceneTransitionAnimation(
itemView.context as Activity,
Pair.create(
binding.itemCompactImage,
ViewCompat.getTransitionName(binding.itemCompactImage)!!
),
).toBundle()
)
}
}
}
}

View File

@@ -4,20 +4,23 @@ import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.util.TypedValue import android.util.TypedValue
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.Window import android.view.Window
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.Refresh import ani.dantotsu.Refresh
import ani.dantotsu.databinding.ActivityListBinding import ani.dantotsu.databinding.ActivityListBinding
import ani.dantotsu.loadData
import ani.dantotsu.media.user.ListViewPagerAdapter import ani.dantotsu.media.user.ListViewPagerAdapter
import ani.dantotsu.others.LangSet import ani.dantotsu.navBarHeight
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
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
@@ -34,7 +37,7 @@ class CalendarActivity : AppCompatActivity() {
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
binding = ActivityListBinding.inflate(layoutInflater) binding = ActivityListBinding.inflate(layoutInflater)
@@ -63,8 +66,7 @@ class CalendarActivity : AppCompatActivity() {
binding.listTitle.setTextColor(primaryTextColor) binding.listTitle.setTextColor(primaryTextColor)
binding.listTabLayout.setTabTextColors(secondaryTextColor, primaryTextColor) binding.listTabLayout.setTabTextColors(secondaryTextColor, primaryTextColor)
binding.listTabLayout.setSelectedTabIndicatorColor(primaryTextColor) binding.listTabLayout.setSelectedTabIndicatorColor(primaryTextColor)
val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings() if (!(PrefManager.getVal(PrefName.ImmersiveMode) as Boolean)) {
if (!uiSettings.immersiveMode) {
this.window.statusBarColor = this.window.statusBarColor =
ContextCompat.getColor(this, R.color.nav_bg_inv) ContextCompat.getColor(this, R.color.nav_bg_inv)
binding.root.fitsSystemWindows = true binding.root.fitsSystemWindows = true
@@ -76,12 +78,15 @@ class CalendarActivity : AppCompatActivity() {
WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN WindowManager.LayoutParams.FLAG_FULLSCREEN
) )
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight
}
} }
setContentView(binding.root) setContentView(binding.root)
binding.listTitle.setText(R.string.release_calendar) binding.listTitle.setText(R.string.release_calendar)
binding.listSort.visibility = View.GONE binding.listSort.visibility = View.GONE
binding.random.visibility = View.GONE
binding.listTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { binding.listTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) { override fun onTabSelected(tab: TabLayout.Tab?) {
this@CalendarActivity.selectedTabIdx = tab?.position ?: 1 this@CalendarActivity.selectedTabIdx = tab?.position ?: 1

View File

@@ -9,7 +9,7 @@ data class Character(
val image: String?, val image: String?,
val banner: String?, val banner: String?,
val role: String, val role: String,
var isFav: Boolean,
var description: String? = null, var description: String? = null,
var age: String? = null, var age: String? = null,
var gender: String? = null, var gender: String? = null,

View File

@@ -11,10 +11,8 @@ import androidx.core.util.Pair
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.databinding.ItemCharacterBinding import ani.dantotsu.databinding.ItemCharacterBinding
import ani.dantotsu.loadData
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
import ani.dantotsu.setAnimation import ani.dantotsu.setAnimation
import ani.dantotsu.settings.UserInterfaceSettings
import java.io.Serializable import java.io.Serializable
class CharacterAdapter( class CharacterAdapter(
@@ -26,13 +24,10 @@ class CharacterAdapter(
return CharacterViewHolder(binding) return CharacterViewHolder(binding)
} }
private val uiSettings =
loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) { override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) {
val binding = holder.binding val binding = holder.binding
setAnimation(binding.root.context, holder.binding.root, uiSettings) setAnimation(binding.root.context, holder.binding.root)
val character = characterList[position] val character = characterList[position]
binding.itemCompactRelation.text = character.role + " " binding.itemCompactRelation.text = character.role + " "
binding.itemCompactImage.loadImage(character.image) binding.itemCompactImage.loadImage(character.image)

View File

@@ -1,5 +1,6 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@@ -15,21 +16,25 @@ import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.Refresh import ani.dantotsu.Refresh
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistMutations
import ani.dantotsu.databinding.ActivityCharacterBinding import ani.dantotsu.databinding.ActivityCharacterBinding
import ani.dantotsu.initActivity import ani.dantotsu.initActivity
import ani.dantotsu.loadData
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.ImageViewDialog import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.others.LangSet
import ani.dantotsu.others.getSerialized import ani.dantotsu.others.getSerialized
import ani.dantotsu.px import ani.dantotsu.px
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.math.abs import kotlin.math.abs
class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener { class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
@@ -38,22 +43,21 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
private val model: OtherDetailsViewModel by viewModels() private val model: OtherDetailsViewModel by viewModels()
private lateinit var character: Character private lateinit var character: Character
private var loaded = false private var loaded = false
val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
binding = ActivityCharacterBinding.inflate(layoutInflater) binding = ActivityCharacterBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
initActivity(this) initActivity(this)
screenWidth = resources.displayMetrics.run { widthPixels / density } screenWidth = resources.displayMetrics.run { widthPixels / density }
if (uiSettings.immersiveMode) this.window.statusBarColor = if (PrefManager.getVal(PrefName.ImmersiveMode)) this.window.statusBarColor =
ContextCompat.getColor(this, R.color.status) ContextCompat.getColor(this, R.color.transparent)
val banner = val banner =
if (uiSettings.bannerAnimations) binding.characterBanner else binding.characterBannerNoKen if (PrefManager.getVal(PrefName.BannerAnimations)) binding.characterBanner else binding.characterBannerNoKen
banner.updateLayoutParams { height += statusBarHeight } banner.updateLayoutParams { height += statusBarHeight }
binding.characterClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight } binding.characterClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
@@ -77,7 +81,39 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
character.image character.image
) )
} }
val link = "https://anilist.co/character/${character.id}"
binding.characterShare.setOnClickListener {
val i = Intent(Intent.ACTION_SEND)
i.type = "text/plain"
i.putExtra(Intent.EXTRA_TEXT, link)
startActivity(Intent.createChooser(i, character.name))
}
binding.characterShare.setOnLongClickListener {
openLinkInBrowser(link)
true
}
lifecycleScope.launch {
withContext(Dispatchers.IO) {
character.isFav = Anilist.query.isUserFav(AnilistMutations.FavType.CHARACTER, character.id)
}
withContext(Dispatchers.Main) {
binding.characterFav.setImageResource(
if (character.isFav) R.drawable.ic_round_favorite_24 else R.drawable.ic_round_favorite_border_24
)
}
}
binding.characterFav.setOnClickListener {
lifecycleScope.launch {
if (Anilist.mutation.toggleFav(AnilistMutations.FavType.CHARACTER, character.id)) {
character.isFav = !character.isFav
binding.characterFav.setImageResource(
if (character.isFav) R.drawable.ic_round_favorite_24 else R.drawable.ic_round_favorite_border_24
)
} else {
snackString("Failed to toggle favorite")
}
}
}
model.getCharacter().observe(this) { model.getCharacter().observe(this) {
if (it != null && !loaded) { if (it != null && !loaded) {
character = it character = it
@@ -136,18 +172,16 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
binding.characterCover.visibility = binding.characterCover.visibility =
if (binding.characterCover.scaleX == 0f) View.GONE else View.VISIBLE if (binding.characterCover.scaleX == 0f) View.GONE else View.VISIBLE
val immersiveMode: Boolean = PrefManager.getVal(PrefName.ImmersiveMode)
if (percentage >= percent && !isCollapsed) { if (percentage >= percent && !isCollapsed) {
isCollapsed = true isCollapsed = true
if (uiSettings.immersiveMode) this.window.statusBarColor = if (immersiveMode) this.window.statusBarColor =
ContextCompat.getColor(this, R.color.nav_bg) ContextCompat.getColor(this, R.color.nav_bg)
binding.characterAppBar.setBackgroundResource(R.color.nav_bg)
} }
if (percentage <= percent && isCollapsed) { if (percentage <= percent && isCollapsed) {
isCollapsed = false isCollapsed = false
if (uiSettings.immersiveMode) this.window.statusBarColor = if (immersiveMode) this.window.statusBarColor =
ContextCompat.getColor(this, R.color.status) ContextCompat.getColor(this, R.color.transparent)
binding.characterAppBar.setBackgroundResource(R.color.bg)
} }
} }
} }

View File

@@ -35,7 +35,7 @@ class CharacterDetailsAdapter(private val character: Character, private val acti
binding.characterDesc.isTextSelectable binding.characterDesc.isTextSelectable
val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create()) val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create())
.usePlugin(SpoilerPlugin()).build() .usePlugin(SpoilerPlugin()).build()
markWon.setMarkdown(binding.characterDesc, desc) markWon.setMarkdown(binding.characterDesc, desc.replace("~!", "||").replace("!~", "||"))
} }

View File

@@ -12,9 +12,9 @@ import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.GenresViewModel import ani.dantotsu.connections.anilist.GenresViewModel
import ani.dantotsu.databinding.ActivityGenreBinding import ani.dantotsu.databinding.ActivityGenreBinding
import ani.dantotsu.initActivity import ani.dantotsu.initActivity
import ani.dantotsu.loadData
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.others.LangSet import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -27,7 +27,7 @@ class GenreActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
binding = ActivityGenreBinding.inflate(layoutInflater) binding = ActivityGenreBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
@@ -54,7 +54,9 @@ class GenreActivity : AppCompatActivity() {
GridLayoutManager(this, (screenWidth / 156f).toInt()) GridLayoutManager(this, (screenWidth / 156f).toInt())
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
model.loadGenres(Anilist.genres ?: loadData("genres_list") ?: arrayListOf()) { model.loadGenres(
Anilist.genres ?: loadLocalGenres() ?: arrayListOf()
) {
MainScope().launch { MainScope().launch {
adapter.addGenre(it) adapter.addGenre(it)
} }
@@ -62,4 +64,15 @@ class GenreActivity : AppCompatActivity() {
} }
} }
} }
private fun loadLocalGenres(): ArrayList<String>? {
val genres = PrefManager.getVal<Set<String>>(PrefName.GenresList)
.toMutableList()
return if (genres.isEmpty()) {
null
} else {
//sort alphabetically
genres.sort().let { genres as ArrayList<String> }
}
}
} }

View File

@@ -58,6 +58,7 @@ data class Media(
var endDate: FuzzyDate? = null, var endDate: FuzzyDate? = null,
var characters: ArrayList<Character>? = null, var characters: ArrayList<Character>? = null,
var staff: ArrayList<Author>? = null,
var prequel: Media? = null, var prequel: Media? = null,
var sequel: Media? = null, var sequel: Media? = null,
var relations: ArrayList<Media>? = null, var relations: ArrayList<Media>? = null,
@@ -108,6 +109,7 @@ data class Media(
this.userScore = mediaList.score?.toInt() ?: 0 this.userScore = mediaList.score?.toInt() ?: 0
this.userStatus = mediaList.status?.toString() this.userStatus = mediaList.status?.toString()
this.userUpdatedAt = mediaList.updatedAt?.toLong() this.userUpdatedAt = mediaList.updatedAt?.toLong()
this.genres = mediaList.media?.genres?.toMutableList() as? ArrayList<String>? ?: arrayListOf()
} }
constructor(mediaEdge: MediaEdge) : this(mediaEdge.node!!) { constructor(mediaEdge: MediaEdge) : this(mediaEdge.node!!) {
@@ -118,6 +120,19 @@ data class Media(
fun mangaName() = if (countryOfOrigin != "JP") mainName() else nameRomaji fun mangaName() = if (countryOfOrigin != "JP") mainName() else nameRomaji
} }
fun emptyMedia() = Media(
id = 0,
name = "No media found",
nameRomaji = "No media found",
userPreferredName = "",
isAdult = false,
isFav = false,
isListPrivate = false,
userScore = 0,
userStatus = "",
format = "",
)
object MediaSingleton { object MediaSingleton {
var media: Media? = null var media: Media? = null
var bitmap: Bitmap? = null var bitmap: Bitmap? = null

View File

@@ -13,7 +13,10 @@ import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.ImageView import android.widget.ImageView
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.util.Pair
import androidx.core.view.ViewCompat
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@@ -23,7 +26,8 @@ import ani.dantotsu.databinding.ItemMediaCompactBinding
import ani.dantotsu.databinding.ItemMediaLargeBinding import ani.dantotsu.databinding.ItemMediaLargeBinding
import ani.dantotsu.databinding.ItemMediaPageBinding import ani.dantotsu.databinding.ItemMediaPageBinding
import ani.dantotsu.databinding.ItemMediaPageSmallBinding import ani.dantotsu.databinding.ItemMediaPageSmallBinding
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.GlideUrl
@@ -39,11 +43,9 @@ class MediaAdaptor(
private val activity: FragmentActivity, private val activity: FragmentActivity,
private val matchParent: Boolean = false, private val matchParent: Boolean = false,
private val viewPager: ViewPager2? = null, private val viewPager: ViewPager2? = null,
private val fav: Boolean = false,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val uiSettings =
loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (type) { return when (type) {
0 -> MediaViewHolder( 0 -> MediaViewHolder(
@@ -88,7 +90,7 @@ class MediaAdaptor(
when (type) { when (type) {
0 -> { 0 -> {
val b = (holder as MediaViewHolder).binding val b = (holder as MediaViewHolder).binding
setAnimation(activity, b.root, uiSettings) setAnimation(activity, b.root)
val media = mediaList?.getOrNull(position) val media = mediaList?.getOrNull(position)
if (media != null) { if (media != null) {
b.itemCompactImage.loadImage(media.cover) b.itemCompactImage.loadImage(media.cover)
@@ -127,16 +129,17 @@ class MediaAdaptor(
) )
b.itemCompactTotal.text = " | ${media.manga.totalChapters ?: "~"}" b.itemCompactTotal.text = " | ${media.manga.totalChapters ?: "~"}"
} }
b.itemCompactProgressContainer.visibility = if (fav) View.GONE else View.VISIBLE
} }
} }
1 -> { 1 -> {
val b = (holder as MediaLargeViewHolder).binding val b = (holder as MediaLargeViewHolder).binding
setAnimation(activity, b.root, uiSettings) setAnimation(activity, b.root)
val media = mediaList?.get(position) val media = mediaList?.get(position)
if (media != null) { if (media != null) {
b.itemCompactImage.loadImage(media.cover) b.itemCompactImage.loadImage(media.cover)
b.itemCompactBanner.loadImage(media.banner ?: media.cover, 400) blurImage(b.itemCompactBanner, media.banner ?: media.cover)
b.itemCompactOngoing.visibility = b.itemCompactOngoing.visibility =
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
b.itemCompactTitle.text = media.userPreferredName b.itemCompactTitle.text = media.userPreferredName
@@ -175,23 +178,16 @@ class MediaAdaptor(
val b = (holder as MediaPageViewHolder).binding val b = (holder as MediaPageViewHolder).binding
val media = mediaList?.get(position) val media = mediaList?.get(position)
if (media != null) { if (media != null) {
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
b.itemCompactImage.loadImage(media.cover) b.itemCompactImage.loadImage(media.cover)
if (uiSettings.bannerAnimations) if (bannerAnimations)
b.itemCompactBanner.setTransitionGenerator( b.itemCompactBanner.setTransitionGenerator(
RandomTransitionGenerator( RandomTransitionGenerator(
(10000 + 15000 * (uiSettings.animationSpeed)).toLong(), (10000 + 15000 * ((PrefManager.getVal(PrefName.AnimationSpeed)) as Float)).toLong(),
AccelerateDecelerateInterpolator() AccelerateDecelerateInterpolator()
) )
) )
val banner = blurImage(b.itemCompactBanner, media.banner ?: media.cover)
if (uiSettings.bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
val context = b.itemCompactBanner.context
if (!(context as Activity).isDestroyed)
Glide.with(context as Context)
.load(GlideUrl(media.banner ?: media.cover))
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
.into(banner)
b.itemCompactOngoing.visibility = b.itemCompactOngoing.visibility =
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
b.itemCompactTitle.text = media.userPreferredName b.itemCompactTitle.text = media.userPreferredName
@@ -231,23 +227,16 @@ class MediaAdaptor(
val b = (holder as MediaPageSmallViewHolder).binding val b = (holder as MediaPageSmallViewHolder).binding
val media = mediaList?.get(position) val media = mediaList?.get(position)
if (media != null) { if (media != null) {
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
b.itemCompactImage.loadImage(media.cover) b.itemCompactImage.loadImage(media.cover)
if (uiSettings.bannerAnimations) if (bannerAnimations)
b.itemCompactBanner.setTransitionGenerator( b.itemCompactBanner.setTransitionGenerator(
RandomTransitionGenerator( RandomTransitionGenerator(
(10000 + 15000 * (uiSettings.animationSpeed)).toLong(), (10000 + 15000 * ((PrefManager.getVal(PrefName.AnimationSpeed) as Float))).toLong(),
AccelerateDecelerateInterpolator() AccelerateDecelerateInterpolator()
) )
) )
val banner = blurImage(b.itemCompactBanner, media.banner ?: media.cover)
if (uiSettings.bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
val context = b.itemCompactBanner.context
if (!(context as Activity).isDestroyed)
Glide.with(context as Context)
.load(GlideUrl(media.banner ?: media.cover))
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
.into(banner)
b.itemCompactOngoing.visibility = b.itemCompactOngoing.visibility =
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
b.itemCompactTitle.text = media.userPreferredName b.itemCompactTitle.text = media.userPreferredName
@@ -319,6 +308,7 @@ class MediaAdaptor(
itemView.setSafeOnClickListener { itemView.setSafeOnClickListener {
clicked( clicked(
bindingAdapterPosition, bindingAdapterPosition,
binding.itemCompactImage,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100) resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
) )
} }
@@ -332,6 +322,7 @@ class MediaAdaptor(
itemView.setSafeOnClickListener { itemView.setSafeOnClickListener {
clicked( clicked(
bindingAdapterPosition, bindingAdapterPosition,
binding.itemCompactImage,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100) resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
) )
} }
@@ -346,6 +337,7 @@ class MediaAdaptor(
binding.itemCompactImage.setSafeOnClickListener { binding.itemCompactImage.setSafeOnClickListener {
clicked( clicked(
bindingAdapterPosition, bindingAdapterPosition,
binding.itemCompactImage,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100) resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
) )
} }
@@ -361,12 +353,14 @@ class MediaAdaptor(
binding.itemCompactImage.setSafeOnClickListener { binding.itemCompactImage.setSafeOnClickListener {
clicked( clicked(
bindingAdapterPosition, bindingAdapterPosition,
binding.itemCompactImage,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100) resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
) )
} }
binding.itemCompactTitleContainer.setSafeOnClickListener { binding.itemCompactTitleContainer.setSafeOnClickListener {
clicked( clicked(
bindingAdapterPosition, bindingAdapterPosition,
binding.itemCompactImage,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100) resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
) )
} }
@@ -375,7 +369,7 @@ class MediaAdaptor(
} }
} }
fun clicked(position: Int, bitmap: Bitmap? = null) { fun clicked(position: Int, itemCompactImage: ImageView?, bitmap: Bitmap? = null) {
if ((mediaList?.size ?: 0) > position && position != -1) { if ((mediaList?.size ?: 0) > position && position != -1) {
val media = mediaList?.get(position) val media = mediaList?.get(position)
if (bitmap != null) MediaSingleton.bitmap = bitmap if (bitmap != null) MediaSingleton.bitmap = bitmap
@@ -384,11 +378,21 @@ class MediaAdaptor(
Intent(activity, MediaDetailsActivity::class.java).putExtra( Intent(activity, MediaDetailsActivity::class.java).putExtra(
"media", "media",
media as Serializable media as Serializable
), null ),
if (itemCompactImage != null) {
ActivityOptionsCompat.makeSceneTransitionAnimation(
activity,
itemCompactImage,
ViewCompat.getTransitionName(itemCompactImage)!!
).toBundle()
} else {
null
}
) )
} }
} }
fun longClicked(position: Int): Boolean { fun longClicked(position: Int): Boolean {
if ((mediaList?.size ?: 0) > position && position != -1) { if ((mediaList?.size ?: 0) > position && position != -1) {
val media = mediaList?.get(position) ?: return false val media = mediaList?.get(position) ?: return false

View File

@@ -2,7 +2,9 @@ package ani.dantotsu.media
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.graphics.Rect
import android.os.Bundle import android.os.Bundle
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.util.TypedValue import android.util.TypedValue
@@ -10,7 +12,9 @@ import android.view.GestureDetector
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowManager
import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.FrameLayout
import android.widget.ImageView import android.widget.ImageView
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@@ -18,6 +22,7 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.text.bold import androidx.core.text.bold
import androidx.core.text.color import androidx.core.text.color
import androidx.core.view.marginBottom
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
@@ -25,78 +30,102 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import ani.dantotsu.CustomBottomNavBar
import ani.dantotsu.GesturesListener import ani.dantotsu.GesturesListener
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.Refresh import ani.dantotsu.Refresh
import ani.dantotsu.ZoomOutPageTransformer import ani.dantotsu.ZoomOutPageTransformer
import ani.dantotsu.blurImage
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.copyToClipboard import ani.dantotsu.copyToClipboard
import ani.dantotsu.databinding.ActivityMediaBinding import ani.dantotsu.databinding.ActivityMediaBinding
import ani.dantotsu.initActivity import ani.dantotsu.initActivity
import ani.dantotsu.loadData
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
import ani.dantotsu.media.anime.AnimeWatchFragment import ani.dantotsu.media.anime.AnimeWatchFragment
import ani.dantotsu.media.comments.CommentsFragment
import ani.dantotsu.media.manga.MangaReadFragment import ani.dantotsu.media.manga.MangaReadFragment
import ani.dantotsu.media.novel.NovelReadFragment import ani.dantotsu.media.novel.NovelReadFragment
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.openLinkInBrowser import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.AndroidBug5497Workaround
import ani.dantotsu.others.ImageViewDialog import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.others.LangSet
import ani.dantotsu.others.getSerialized import ani.dantotsu.others.getSerialized
import ani.dantotsu.saveData import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import com.flaviofaria.kenburnsview.RandomTransitionGenerator import com.flaviofaria.kenburnsview.RandomTransitionGenerator
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.navigation.NavigationBarView
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlin.math.abs import kotlin.math.abs
class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener { class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
private lateinit var binding: ActivityMediaBinding lateinit var binding: ActivityMediaBinding
private val scope = lifecycleScope private val scope = lifecycleScope
private val model: MediaDetailsViewModel by viewModels() private val model: MediaDetailsViewModel by viewModels()
private lateinit var tabLayout: NavigationBarView lateinit var tabLayout: TripleNavAdapter
private lateinit var uiSettings: UserInterfaceSettings
var selected = 0 var selected = 0
var anime = true var anime = true
private var adult = false private var adult = false
@SuppressLint("SetTextI18n", "ClickableViewAccessibility") @SuppressLint("SetTextI18n", "ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
LangSet.setLocale(this) super.onCreate(savedInstanceState)
var media: Media = intent.getSerialized("media") ?: return var media: Media = intent.getSerialized("media") ?: mediaSingleton ?: emptyMedia()
val id = intent.getIntExtra("mediaId", -1)
if (id != -1) {
runBlocking {
withContext(Dispatchers.IO) {
media =
Anilist.query.getMedia(id, false) ?: emptyMedia()
}
}
}
if (media.name == "No media found") {
snackString(media.name)
onBackPressedDispatcher.onBackPressed()
return
}
mediaSingleton = null
ThemeManager(this).applyTheme(MediaSingleton.bitmap) ThemeManager(this).applyTheme(MediaSingleton.bitmap)
MediaSingleton.bitmap = null MediaSingleton.bitmap = null
super.onCreate(savedInstanceState)
binding = ActivityMediaBinding.inflate(layoutInflater) binding = ActivityMediaBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
screenWidth = resources.displayMetrics.widthPixels.toFloat() screenWidth = resources.displayMetrics.widthPixels.toFloat()
val isVertical = resources.configuration.orientation
//Ui init //Ui init
initActivity(this) initActivity(this)
uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings() binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
if (!uiSettings.immersiveMode) this.window.statusBarColor = val oldMargin = binding.mediaViewPager.marginBottom
ContextCompat.getColor(this, R.color.nav_bg_inv) AndroidBug5497Workaround.assistActivity(this) {
if (it) {
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = 0
}
binding.mediaTabContainer.visibility = View.GONE
} else {
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = oldMargin
}
binding.mediaTabContainer.visibility = View.VISIBLE
}
}
binding.mediaBanner.updateLayoutParams { height += statusBarHeight } binding.mediaBanner.updateLayoutParams { height += statusBarHeight }
binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight } binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight }
binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight } binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.incognito.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.mediaCollapsing.minimumHeight = statusBarHeight binding.mediaCollapsing.minimumHeight = statusBarHeight
if (binding.mediaTab is CustomBottomNavBar) binding.mediaTab.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
binding.mediaTitle.isSelected = true binding.mediaTitle.isSelected = true
mMaxScrollSize = binding.mediaAppBar.totalScrollRange mMaxScrollSize = binding.mediaAppBar.totalScrollRange
@@ -106,20 +135,21 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
} }
if (uiSettings.bannerAnimations) { val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
if (bannerAnimations) {
val adi = AccelerateDecelerateInterpolator() val adi = AccelerateDecelerateInterpolator()
val generator = RandomTransitionGenerator( val generator = RandomTransitionGenerator(
(10000 + 15000 * (uiSettings.animationSpeed)).toLong(), (10000 + 15000 * ((PrefManager.getVal(PrefName.AnimationSpeed) as Float))).toLong(),
adi adi
) )
binding.mediaBanner.setTransitionGenerator(generator) binding.mediaBanner.setTransitionGenerator(generator)
} }
val banner = val banner =
if (uiSettings.bannerAnimations) binding.mediaBanner else binding.mediaBannerNoKen if (bannerAnimations) binding.mediaBanner else binding.mediaBannerNoKen
val viewPager = binding.mediaViewPager val viewPager = binding.mediaViewPager
tabLayout = binding.mediaTab as NavigationBarView //tabLayout = binding.mediaTab as AnimatedBottomBar
viewPager.isUserInputEnabled = false viewPager.isUserInputEnabled = false
viewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings)) viewPager.setPageTransformer(ZoomOutPageTransformer())
val isDownload = intent.getBooleanExtra("download", false) val isDownload = intent.getBooleanExtra("download", false)
@@ -133,10 +163,11 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
media.cover media.cover
) )
} }
banner.loadImage(media.banner ?: media.cover, 400)
blurImage(banner, media.banner ?: media.cover)
val gestureDetector = GestureDetector(this, object : GesturesListener() { val gestureDetector = GestureDetector(this, object : GesturesListener() {
override fun onDoubleClick(event: MotionEvent) { override fun onDoubleClick(event: MotionEvent) {
if (!uiSettings.bannerAnimations) if (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean))
snackString(getString(R.string.enable_banner_animations)) snackString(getString(R.string.enable_banner_animations))
else { else {
binding.mediaBanner.restart() binding.mediaBanner.restart()
@@ -154,7 +185,12 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
} }
}) })
banner.setOnTouchListener { _, motionEvent -> gestureDetector.onTouchEvent(motionEvent);true } banner.setOnTouchListener { _, motionEvent -> gestureDetector.onTouchEvent(motionEvent);true }
binding.mediaTitle.text = media.userPreferredName if (PrefManager.getVal(PrefName.Incognito)) {
binding.mediaTitle.text = " ${media.userPreferredName}"
binding.incognito.visibility = View.VISIBLE
} else {
binding.mediaTitle.text = media.userPreferredName
}
binding.mediaTitle.setOnLongClickListener { binding.mediaTitle.setOnLongClickListener {
copyToClipboard(media.userPreferredName) copyToClipboard(media.userPreferredName)
true true
@@ -273,7 +309,10 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
} else snackString(getString(R.string.please_login_anilist)) } else snackString(getString(R.string.please_login_anilist))
} }
binding.mediaAddToList.setOnLongClickListener { binding.mediaAddToList.setOnLongClickListener {
saveData("${media.id}_progressDialog", true) PrefManager.setCustomVal(
"${media.id}_progressDialog",
true,
)
snackString(getString(R.string.auto_update_reset)) snackString(getString(R.string.auto_update_reset))
true true
} }
@@ -303,46 +342,54 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
progress() progress()
} }
} }
tabLayout = TripleNavAdapter(
binding.mediaTab1,
binding.mediaTab2,
binding.mediaTab3,
media.anime != null,
media.format ?: "",
isVertical == 1
)
adult = media.isAdult adult = media.isAdult
tabLayout.menu.clear()
if (media.anime != null) { if (media.anime != null) {
viewPager.adapter = viewPager.adapter =
ViewPagerAdapter(supportFragmentManager, lifecycle, SupportedMedia.ANIME) ViewPagerAdapter(supportFragmentManager, lifecycle, SupportedMedia.ANIME, media, intent.getIntExtra("commentId", -1))
tabLayout.inflateMenu(R.menu.anime_menu_detail)
} else if (media.manga != null) { } else if (media.manga != null) {
viewPager.adapter = ViewPagerAdapter( viewPager.adapter = ViewPagerAdapter(
supportFragmentManager, supportFragmentManager,
lifecycle, lifecycle,
if (media.format == "NOVEL") SupportedMedia.NOVEL else SupportedMedia.MANGA if (media.format == "NOVEL") SupportedMedia.NOVEL else SupportedMedia.MANGA,
media,
intent.getIntExtra("commentId", -1)
) )
tabLayout.inflateMenu(R.menu.manga_menu_detail)
anime = false anime = false
} }
selected = media.selected!!.window selected = media.selected!!.window
binding.mediaTitle.translationX = -screenWidth binding.mediaTitle.translationX = -screenWidth
tabLayout.visibility = View.VISIBLE
tabLayout.setOnItemSelectedListener { item -> tabLayout.selectionListener = { selected, newId ->
selectFromID(item.itemId) binding.commentInputLayout.visibility = if (selected == 2) View.VISIBLE else View.GONE
this.selected = selected
selectFromID(newId)
viewPager.setCurrentItem(selected, false) viewPager.setCurrentItem(selected, false)
val sel = model.loadSelected(media, isDownload) val sel = model.loadSelected(media, isDownload)
sel.window = selected sel.window = selected
model.saveSelected(media.id, sel, this) model.saveSelected(media.id, sel)
true
} }
tabLayout.selectTab(selected)
selectFromID(tabLayout.selected)
tabLayout.selectedItemId = idFromSelect()
viewPager.setCurrentItem(selected, false) viewPager.setCurrentItem(selected, false)
if (model.continueMedia == null && media.cameFromContinue) { if (model.continueMedia == null && media.cameFromContinue) {
model.continueMedia = loadData("continue_media") ?: true model.continueMedia = PrefManager.getVal(PrefName.ContinueMedia)
selected = 1 selected = 1
} }
val frag = intent.getStringExtra("FRAGMENT_TO_LOAD")
if (frag != null) {
selected = 2
}
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) } val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) }
live.observe(this) { live.observe(this) {
@@ -355,7 +402,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
} }
} }
private fun selectFromID(id: Int) { private fun selectFromID(id: Int) {
when (id) { when (id) {
R.id.info -> { R.id.info -> {
@@ -365,6 +411,10 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
R.id.watch, R.id.read -> { R.id.watch, R.id.read -> {
selected = 1 selected = 1
} }
R.id.comment -> {
selected = 2
}
} }
} }
@@ -372,16 +422,20 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
if (anime) when (selected) { if (anime) when (selected) {
0 -> return R.id.info 0 -> return R.id.info
1 -> return R.id.watch 1 -> return R.id.watch
2 -> return R.id.comment
} }
else when (selected) { else when (selected) {
0 -> return R.id.info 0 -> return R.id.info
1 -> return R.id.read 1 -> return R.id.read
2 -> return R.id.comment
} }
return R.id.info return R.id.info
} }
override fun onResume() { override fun onResume() {
tabLayout.selectedItemId = idFromSelect() if (this::tabLayout.isInitialized) {
tabLayout.selectTab(selected)
}
super.onResume() super.onResume()
} }
@@ -393,19 +447,30 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
private class ViewPagerAdapter( private class ViewPagerAdapter(
fragmentManager: FragmentManager, fragmentManager: FragmentManager,
lifecycle: Lifecycle, lifecycle: Lifecycle,
private val media: SupportedMedia private val mediaType: SupportedMedia,
private val media: Media,
private val commentId: Int
) : ) :
FragmentStateAdapter(fragmentManager, lifecycle) { FragmentStateAdapter(fragmentManager, lifecycle) {
override fun getItemCount(): Int = 2 override fun getItemCount(): Int = 3
override fun createFragment(position: Int): Fragment = when (position) { override fun createFragment(position: Int): Fragment = when (position) {
0 -> MediaInfoFragment() 0 -> MediaInfoFragment()
1 -> when (media) { 1 -> when (mediaType) {
SupportedMedia.ANIME -> AnimeWatchFragment() SupportedMedia.ANIME -> AnimeWatchFragment()
SupportedMedia.MANGA -> MangaReadFragment() SupportedMedia.MANGA -> MangaReadFragment()
SupportedMedia.NOVEL -> NovelReadFragment() SupportedMedia.NOVEL -> NovelReadFragment()
} }
2 -> {
val fragment = CommentsFragment()
val bundle = Bundle()
bundle.putInt("mediaId", media.id)
bundle.putString("mediaName", media.mainName())
if (commentId != -1) bundle.putInt("commentId", commentId)
fragment.arguments = bundle
fragment
}
else -> MediaInfoFragment() else -> MediaInfoFragment()
} }
@@ -423,7 +488,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
binding.mediaCover.visibility = binding.mediaCover.visibility =
if (binding.mediaCover.scaleX == 0f) View.GONE else View.VISIBLE if (binding.mediaCover.scaleX == 0f) View.GONE else View.VISIBLE
val duration = (200 * uiSettings.animationSpeed).toLong() val duration = (200 * (PrefManager.getVal(PrefName.AnimationSpeed) as Float)).toLong()
val typedValue = TypedValue() val typedValue = TypedValue()
this@MediaDetailsActivity.theme.resolveAttribute( this@MediaDetailsActivity.theme.resolveAttribute(
com.google.android.material.R.attr.colorSecondary, com.google.android.material.R.attr.colorSecondary,
@@ -442,7 +507,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", screenWidth) ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", screenWidth)
.setDuration(duration).start() .setDuration(duration).start()
binding.mediaBanner.pause() binding.mediaBanner.pause()
if (!uiSettings.immersiveMode) this.window.statusBarColor = color
} }
if (percentage <= percent && isCollapsed) { if (percentage <= percent && isCollapsed) {
isCollapsed = false isCollapsed = false
@@ -454,8 +518,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
.start() .start()
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", 0f) ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", 0f)
.setDuration(duration).start() .setDuration(duration).start()
if (uiSettings.bannerAnimations) binding.mediaBanner.resume() if (PrefManager.getVal(PrefName.BannerAnimations)) binding.mediaBanner.resume()
if (!uiSettings.immersiveMode) this.window.statusBarColor = color
} }
if (percentage == 1 && model.scrolledToTop.value != false) model.scrolledToTop.postValue( if (percentage == 1 && model.scrolledToTop.value != false) model.scrolledToTop.postValue(
false false
@@ -471,6 +534,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
private val c1: Int, private val c1: Int,
private val c2: Int, private val c2: Int,
var clicked: Boolean, var clicked: Boolean,
needsInitialClick: Boolean = false,
callback: suspend (Boolean) -> (Unit) callback: suspend (Boolean) -> (Unit)
) { ) {
private var disabled = false private var disabled = false
@@ -479,8 +543,10 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
init { init {
enabled(true) enabled(true)
scope.launch { if (needsInitialClick) {
clicked() scope.launch {
clicked()
}
} }
image.setOnClickListener { image.setOnClickListener {
if (pressable && !disabled) { if (pressable && !disabled) {
@@ -532,5 +598,8 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
image.alpha = if (disabled) 0.33f else 1f image.alpha = if (disabled) 0.33f else 1f
} }
} }
}
companion object {
var mediaSingleton: Media? = null
}
}

View File

@@ -1,7 +1,5 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.app.Activity
import android.content.SharedPreferences
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
@@ -11,8 +9,7 @@ import androidx.lifecycle.ViewModel
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.loadData import ani.dantotsu.util.Logger
import ani.dantotsu.logger
import ani.dantotsu.media.anime.Episode import ani.dantotsu.media.anime.Episode
import ani.dantotsu.media.anime.SelectorDialogFragment import ani.dantotsu.media.anime.SelectorDialogFragment
import ani.dantotsu.media.manga.MangaChapter import ani.dantotsu.media.manga.MangaChapter
@@ -28,35 +25,32 @@ import ani.dantotsu.parsers.NovelSources
import ani.dantotsu.parsers.ShowResponse import ani.dantotsu.parsers.ShowResponse
import ani.dantotsu.parsers.VideoExtractor import ani.dantotsu.parsers.VideoExtractor
import ani.dantotsu.parsers.WatchSources import ani.dantotsu.parsers.WatchSources
import ani.dantotsu.saveData import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.tryWithSuspend import ani.dantotsu.tryWithSuspend
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MediaDetailsViewModel : ViewModel() { class MediaDetailsViewModel : ViewModel() {
val scrolledToTop = MutableLiveData(true) val scrolledToTop = MutableLiveData(true)
fun saveSelected(id: Int, data: Selected, activity: Activity? = null) { fun saveSelected(id: Int, data: Selected) {
saveData("$id-select", data, activity) PrefManager.setCustomVal("Selected-$id", data)
} }
fun loadSelected(media: Media, isDownload: Boolean = false): Selected { fun loadSelected(media: Media, isDownload: Boolean = false): Selected {
val sharedPreferences = Injekt.get<SharedPreferences>() val data =
val data = loadData<Selected>("${media.id}-select") ?: Selected().let { PrefManager.getNullableCustomVal("Selected-${media.id}", null, Selected::class.java)
it.sourceIndex = if (media.isAdult) 0 else when (media.anime != null) { ?: Selected().let {
true -> sharedPreferences.getInt("settings_def_anime_source_s_r", 0) it.sourceIndex = 0
else -> sharedPreferences.getInt(("settings_def_manga_source_s_r"), 0) it.preferDub = PrefManager.getVal(PrefName.SettingsPreferDub)
} saveSelected(media.id, it)
it.preferDub = loadData("settings_prefer_dub") ?: false it
saveSelected(media.id, it) }
it
}
if (isDownload) { if (isDownload) {
data.sourceIndex = if (media.anime != null) { data.sourceIndex = if (media.anime != null) {
AnimeSources.list.size - 1 AnimeSources.list.size - 1
@@ -229,7 +223,7 @@ class MediaDetailsViewModel : ViewModel() {
} }
fun setEpisode(ep: Episode?, who: String) { fun setEpisode(ep: Episode?, who: String) {
logger("set episode ${ep?.number} - $who", false) Logger.log("set episode ${ep?.number} - $who")
episode.postValue(ep) episode.postValue(ep)
MainScope().launch(Dispatchers.Main) { MainScope().launch(Dispatchers.Main) {
episode.value = null episode.value = null
@@ -242,7 +236,8 @@ class MediaDetailsViewModel : ViewModel() {
i: String, i: String,
manager: FragmentManager, manager: FragmentManager,
launch: Boolean = true, launch: Boolean = true,
prevEp: String? = null prevEp: String? = null,
isDownload: Boolean = false
) { ) {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
if (manager.findFragmentByTag("dialog") == null && !manager.isDestroyed) { if (manager.findFragmentByTag("dialog") == null && !manager.isDestroyed) {
@@ -254,13 +249,17 @@ class MediaDetailsViewModel : ViewModel() {
} }
media.selected = this.loadSelected(media) media.selected = this.loadSelected(media)
val selector = val selector =
SelectorDialogFragment.newInstance(media.selected!!.server, launch, prevEp) SelectorDialogFragment.newInstance(
media.selected!!.server,
launch,
prevEp,
isDownload
)
selector.show(manager, "dialog") selector.show(manager, "dialog")
} }
} }
} }
//Manga //Manga
var mangaReadSources: MangaReadSources? = null var mangaReadSources: MangaReadSources? = null
@@ -271,7 +270,7 @@ class MediaDetailsViewModel : ViewModel() {
mangaChapters mangaChapters
suspend fun loadMangaChapters(media: Media, i: Int, invalidate: Boolean = false) { suspend fun loadMangaChapters(media: Media, i: Int, invalidate: Boolean = false) {
logger("Loading Manga Chapters : $mangaLoaded") Logger.log("Loading Manga Chapters : $mangaLoaded")
if (!mangaLoaded.containsKey(i) || invalidate) tryWithSuspend { if (!mangaLoaded.containsKey(i) || invalidate) tryWithSuspend {
mangaLoaded[i] = mangaLoaded[i] =
mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend
@@ -314,7 +313,8 @@ class MediaDetailsViewModel : ViewModel() {
val novelSources = NovelSources val novelSources = NovelSources
val novelResponses = MutableLiveData<List<ShowResponse>>(null) val novelResponses = MutableLiveData<List<ShowResponse>>(null)
suspend fun searchNovels(query: String, i: Int) { suspend fun searchNovels(query: String, i: Int) {
val source = novelSources[i] val position = if (i >= novelSources.list.size) 0 else i
val source = novelSources[position]
tryWithSuspend(post = true) { tryWithSuspend(post = true) {
if (source != null) { if (source != null) {
novelResponses.postValue(source.search(query)) novelResponses.postValue(source.search(query))

View File

@@ -3,6 +3,7 @@ package ani.dantotsu.media
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.CountDownTimer import android.os.CountDownTimer
@@ -25,6 +26,8 @@ import ani.dantotsu.*
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.GenresViewModel import ani.dantotsu.connections.anilist.GenresViewModel
import ani.dantotsu.databinding.* import ani.dantotsu.databinding.*
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin import io.noties.markwon.SoftBreakAddsNewLinePlugin
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -59,6 +62,7 @@ class MediaInfoFragment : Fragment() {
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val model: MediaDetailsViewModel by activityViewModels() val model: MediaDetailsViewModel by activityViewModels()
val offline: Boolean = PrefManager.getVal(PrefName.OfflineMode)
binding.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE binding.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
binding.mediaInfoContainer.visibility = if (loaded) View.VISIBLE else View.GONE binding.mediaInfoContainer.visibility = if (loaded) View.VISIBLE else View.GONE
binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight } binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight }
@@ -70,6 +74,8 @@ class MediaInfoFragment : Fragment() {
model.getMedia().observe(viewLifecycleOwner) { media -> model.getMedia().observe(viewLifecycleOwner) { media ->
if (media != null && !loaded) { if (media != null && !loaded) {
loaded = true loaded = true
binding.mediaInfoProgressBar.visibility = View.GONE binding.mediaInfoProgressBar.visibility = View.GONE
binding.mediaInfoContainer.visibility = View.VISIBLE binding.mediaInfoContainer.visibility = View.VISIBLE
binding.mediaInfoName.text = "\t\t\t" + (media.name ?: media.nameRomaji) binding.mediaInfoName.text = "\t\t\t" + (media.name ?: media.nameRomaji)
@@ -92,8 +98,31 @@ class MediaInfoFragment : Fragment() {
binding.mediaInfoStart.text = media.startDate?.toString() ?: "??" binding.mediaInfoStart.text = media.startDate?.toString() ?: "??"
binding.mediaInfoEnd.text = media.endDate?.toString() ?: "??" binding.mediaInfoEnd.text = media.endDate?.toString() ?: "??"
if (media.anime != null) { if (media.anime != null) {
binding.mediaInfoDuration.text = val episodeDuration = media.anime.episodeDuration
if (media.anime.episodeDuration != null) media.anime.episodeDuration.toString() else "??"
binding.mediaInfoDuration.text = when {
episodeDuration != null -> {
val hours = episodeDuration / 60
val minutes = episodeDuration % 60
val formattedDuration = buildString {
if (hours > 0) {
append("$hours hour")
if (hours > 1) append("s")
}
if (minutes > 0) {
if (hours > 0) append(", ")
append("$minutes min")
if (minutes > 1) append("s")
}
}
formattedDuration
}
else -> "??"
}
binding.mediaInfoDurationContainer.visibility = View.VISIBLE binding.mediaInfoDurationContainer.visibility = View.VISIBLE
binding.mediaInfoSeasonContainer.visibility = View.VISIBLE binding.mediaInfoSeasonContainer.visibility = View.VISIBLE
binding.mediaInfoSeason.text = binding.mediaInfoSeason.text =
@@ -101,29 +130,33 @@ class MediaInfoFragment : Fragment() {
if (media.anime.mainStudio != null) { if (media.anime.mainStudio != null) {
binding.mediaInfoStudioContainer.visibility = View.VISIBLE binding.mediaInfoStudioContainer.visibility = View.VISIBLE
binding.mediaInfoStudio.text = media.anime.mainStudio!!.name binding.mediaInfoStudio.text = media.anime.mainStudio!!.name
binding.mediaInfoStudioContainer.setOnClickListener { if (!offline) {
ContextCompat.startActivity( binding.mediaInfoStudioContainer.setOnClickListener {
requireActivity(), ContextCompat.startActivity(
Intent(activity, StudioActivity::class.java).putExtra( requireActivity(),
"studio", Intent(activity, StudioActivity::class.java).putExtra(
media.anime.mainStudio!! as Serializable "studio",
), media.anime.mainStudio!! as Serializable
null ),
) null
)
}
} }
} }
if (media.anime.author != null) { if (media.anime.author != null) {
binding.mediaInfoAuthorContainer.visibility = View.VISIBLE binding.mediaInfoAuthorContainer.visibility = View.VISIBLE
binding.mediaInfoAuthor.text = media.anime.author!!.name binding.mediaInfoAuthor.text = media.anime.author!!.name
binding.mediaInfoAuthorContainer.setOnClickListener { if (!offline) {
ContextCompat.startActivity( binding.mediaInfoAuthorContainer.setOnClickListener {
requireActivity(), ContextCompat.startActivity(
Intent(activity, AuthorActivity::class.java).putExtra( requireActivity(),
"author", Intent(activity, AuthorActivity::class.java).putExtra(
media.anime.author!! as Serializable "author",
), media.anime.author!! as Serializable
null ),
) null
)
}
} }
} }
binding.mediaInfoTotalTitle.setText(R.string.total_eps) binding.mediaInfoTotalTitle.setText(R.string.total_eps)
@@ -137,15 +170,17 @@ class MediaInfoFragment : Fragment() {
if (media.manga.author != null) { if (media.manga.author != null) {
binding.mediaInfoAuthorContainer.visibility = View.VISIBLE binding.mediaInfoAuthorContainer.visibility = View.VISIBLE
binding.mediaInfoAuthor.text = media.manga.author!!.name binding.mediaInfoAuthor.text = media.manga.author!!.name
binding.mediaInfoAuthorContainer.setOnClickListener { if (!offline) {
ContextCompat.startActivity( binding.mediaInfoAuthorContainer.setOnClickListener {
requireActivity(), ContextCompat.startActivity(
Intent(activity, AuthorActivity::class.java).putExtra( requireActivity(),
"author", Intent(activity, AuthorActivity::class.java).putExtra(
media.manga.author!! as Serializable "author",
), media.manga.author!! as Serializable
null ),
) null
)
}
} }
} }
} }
@@ -189,7 +224,7 @@ class MediaInfoFragment : Fragment() {
parent.addView(bind.root) parent.addView(bind.root)
} }
if (media.trailer != null) { if (media.trailer != null && !offline) {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
class MyChrome : WebChromeClient() { class MyChrome : WebChromeClient() {
private var mCustomView: View? = null private var mCustomView: View? = null
@@ -243,7 +278,7 @@ class MediaInfoFragment : Fragment() {
parent.addView(bind.root) parent.addView(bind.root)
} }
if (media.anime != null && (media.anime.op.isNotEmpty() || media.anime.ed.isNotEmpty())) { if (media.anime != null && (media.anime.op.isNotEmpty() || media.anime.ed.isNotEmpty()) && !offline) {
val markWon = Markwon.builder(requireContext()) val markWon = Markwon.builder(requireContext())
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build() .usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
@@ -304,7 +339,7 @@ class MediaInfoFragment : Fragment() {
} }
} }
if (media.genres.isNotEmpty()) { if (media.genres.isNotEmpty() && !offline) {
val bind = ActivityGenreBinding.inflate( val bind = ActivityGenreBinding.inflate(
LayoutInflater.from(context), LayoutInflater.from(context),
parent, parent,
@@ -335,7 +370,7 @@ class MediaInfoFragment : Fragment() {
parent.addView(bind.root) parent.addView(bind.root)
} }
if (media.tags.isNotEmpty()) { if (media.tags.isNotEmpty() && !offline) {
val bind = ItemTitleChipgroupBinding.inflate( val bind = ItemTitleChipgroupBinding.inflate(
LayoutInflater.from(context), LayoutInflater.from(context),
parent, parent,
@@ -376,24 +411,7 @@ class MediaInfoFragment : Fragment() {
parent.addView(bind.root) parent.addView(bind.root)
} }
if (!media.characters.isNullOrEmpty()) { if (!media.relations.isNullOrEmpty() && !offline) {
val bind = ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context),
parent,
false
)
bind.itemTitle.setText(R.string.characters)
bind.itemRecycler.adapter =
CharacterAdapter(media.characters!!)
bind.itemRecycler.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
parent.addView(bind.root)
}
if (!media.relations.isNullOrEmpty()) {
if (media.sequel != null || media.prequel != null) { if (media.sequel != null || media.prequel != null) {
val bind = ItemQuelsBinding.inflate( val bind = ItemQuelsBinding.inflate(
LayoutInflater.from(context), LayoutInflater.from(context),
@@ -455,8 +473,39 @@ class MediaInfoFragment : Fragment() {
) )
parent.addView(bindi.root) parent.addView(bindi.root)
} }
if (!media.characters.isNullOrEmpty() && !offline) {
if (!media.recommendations.isNullOrEmpty()) { val bind = ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context),
parent,
false
)
bind.itemTitle.setText(R.string.characters)
bind.itemRecycler.adapter =
CharacterAdapter(media.characters!!)
bind.itemRecycler.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
parent.addView(bind.root)
}
if (!media.staff.isNullOrEmpty() && !offline) {
val bind = ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context),
parent,
false
)
bind.itemTitle.setText(R.string.staff)
bind.itemRecycler.adapter =
AuthorAdapter(media.staff!!)
bind.itemRecycler.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
parent.addView(bind.root)
}
if (!media.recommendations.isNullOrEmpty() && !offline) {
val bind = ItemTitleRecyclerBinding.inflate( val bind = ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context), LayoutInflater.from(context),
parent, parent,

View File

@@ -16,7 +16,7 @@ import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.FuzzyDate import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.mal.MAL import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.databinding.BottomSheetMediaListBinding import ani.dantotsu.databinding.BottomSheetMediaListBinding
import com.google.android.material.switchmaterial.SwitchMaterial import com.google.android.material.materialswitch.MaterialSwitch
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -196,7 +196,7 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
} }
media?.inCustomListsOf?.forEach { media?.inCustomListsOf?.forEach {
SwitchMaterial(requireContext()).apply { MaterialSwitch(requireContext()).apply {
isChecked = it.value isChecked = it.value
text = it.key text = it.key
setOnCheckedChangeListener { _, isChecked -> setOnCheckedChangeListener { _, isChecked ->
@@ -254,20 +254,28 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
} }
binding.mediaListDelete.setOnClickListener { binding.mediaListDelete.setOnClickListener {
val id = media!!.userListId var id = media!!.userListId
if (id != null) { scope.launch {
scope.launch { withContext(Dispatchers.IO) {
withContext(Dispatchers.IO) { if (id != null) {
Anilist.mutation.deleteList(id) Anilist.mutation.deleteList(id!!)
MAL.query.deleteList(media?.anime != null, media?.idMAL) MAL.query.deleteList(media?.anime != null, media?.idMAL)
} else {
val profile = Anilist.query.userMediaDetails(media!!)
profile.userListId?.let { listId ->
id = listId
Anilist.mutation.deleteList(listId)
MAL.query.deleteList(media?.anime != null, media?.idMAL)
}
} }
Refresh.all()
snackString(getString(R.string.deleted_from_list))
dismissAllowingStateLoss()
} }
}
if (id != null) {
Refresh.all()
snackString(getString(R.string.deleted_from_list))
dismissAllowingStateLoss()
} else { } else {
snackString(getString(R.string.no_list_id)) snackString(getString(R.string.no_list_id))
Refresh.all()
} }
} }
} }

View File

@@ -58,6 +58,40 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight } binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
val scope = viewLifecycleOwner.lifecycleScope val scope = viewLifecycleOwner.lifecycleScope
binding.mediaListDelete.setOnClickListener {
var id = media.userListId
viewLifecycleOwner.lifecycleScope.launch {
withContext(Dispatchers.IO) {
if (id != null) {
try {
Anilist.mutation.deleteList(id!!)
MAL.query.deleteList(media.anime != null, media.idMAL)
} catch (e: Exception) {
withContext(Dispatchers.Main) {
snackString("Failed to delete because of... ${e.message}")
}
return@withContext
}
} else {
val profile = Anilist.query.userMediaDetails(media)
profile.userListId?.let { listId ->
id = listId
Anilist.mutation.deleteList(listId)
MAL.query.deleteList(media.anime != null, media.idMAL)
}
}
}
withContext(Dispatchers.Main) {
if (id != null) {
Refresh.all()
snackString(getString(R.string.deleted_from_list))
dismissAllowingStateLoss()
} else {
snackString(getString(R.string.no_list_id))
}
}
}
}
binding.mediaListProgressBar.visibility = View.GONE binding.mediaListProgressBar.visibility = View.GONE
binding.mediaListLayout.visibility = View.VISIBLE binding.mediaListLayout.visibility = View.VISIBLE

View File

@@ -16,7 +16,8 @@ import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistSearch import ani.dantotsu.connections.anilist.AnilistSearch
import ani.dantotsu.connections.anilist.SearchResults import ani.dantotsu.connections.anilist.SearchResults
import ani.dantotsu.databinding.ActivitySearchBinding import ani.dantotsu.databinding.ActivitySearchBinding
import ani.dantotsu.others.LangSet import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -33,13 +34,14 @@ class SearchActivity : AppCompatActivity() {
private lateinit var mediaAdaptor: MediaAdaptor private lateinit var mediaAdaptor: MediaAdaptor
private lateinit var progressAdapter: ProgressAdapter private lateinit var progressAdapter: ProgressAdapter
private lateinit var concatAdapter: ConcatAdapter private lateinit var concatAdapter: ConcatAdapter
private lateinit var headerAdaptor: SearchAdapter
lateinit var result: SearchResults lateinit var result: SearchResults
lateinit var updateChips: (() -> Unit) lateinit var updateChips: (() -> Unit)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
binding = ActivitySearchBinding.inflate(layoutInflater) binding = ActivitySearchBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
@@ -51,7 +53,7 @@ class SearchActivity : AppCompatActivity() {
bottom = navBarHeight + 80f.px bottom = navBarHeight + 80f.px
) )
style = loadData<Int>("searchStyle") ?: 0 style = PrefManager.getVal(PrefName.SearchStyle)
var listOnly: Boolean? = intent.getBooleanExtra("listOnly", false) var listOnly: Boolean? = intent.getBooleanExtra("listOnly", false)
if (!listOnly!!) listOnly = null if (!listOnly!!) listOnly = null
@@ -76,9 +78,9 @@ class SearchActivity : AppCompatActivity() {
progressAdapter = ProgressAdapter(searched = model.searched) progressAdapter = ProgressAdapter(searched = model.searched)
mediaAdaptor = MediaAdaptor(style, model.searchResults.results, this, matchParent = true) mediaAdaptor = MediaAdaptor(style, model.searchResults.results, this, matchParent = true)
val headerAdaptor = SearchAdapter(this) headerAdaptor = SearchAdapter(this, model.searchResults.type)
val gridSize = (screenWidth / 124f).toInt() val gridSize = (screenWidth / 120f).toInt()
val gridLayoutManager = GridLayoutManager(this, gridSize) val gridLayoutManager = GridLayoutManager(this, gridSize)
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int { override fun getSpanSize(position: Int): Int {
@@ -154,12 +156,21 @@ class SearchActivity : AppCompatActivity() {
} }
} }
fun emptyMediaAdapter() {
searchTimer.cancel()
searchTimer.purge()
mediaAdaptor.notifyItemRangeRemoved(0, model.searchResults.results.size)
model.searchResults.results.clear()
progressAdapter.bar?.visibility = View.GONE
}
private var searchTimer = Timer() private var searchTimer = Timer()
private var loading = false private var loading = false
fun search() { fun search() {
headerAdaptor.setHistoryVisibility(false)
val size = model.searchResults.results.size val size = model.searchResults.results.size
model.searchResults.results.clear() model.searchResults.results.clear()
runOnUiThread { binding.searchRecyclerView.post {
mediaAdaptor.notifyItemRangeRemoved(0, size) mediaAdaptor.notifyItemRangeRemoved(0, size)
} }
@@ -188,6 +199,9 @@ class SearchActivity : AppCompatActivity() {
var state: Parcelable? = null var state: Parcelable? = null
override fun onPause() { override fun onPause() {
if (this::headerAdaptor.isInitialized) {
headerAdaptor.addHistory()
}
super.onPause() super.onPause()
state = binding.searchRecyclerView.layoutManager?.onSaveInstanceState() state = binding.searchRecyclerView.layoutManager?.onSaveInstanceState()
} }

View File

@@ -1,31 +1,48 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.drawable.Drawable
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat.startActivity
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.HORIZONTAL import androidx.recyclerview.widget.RecyclerView.HORIZONTAL
import ani.dantotsu.App.Companion.context
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.ItemChipBinding import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.databinding.ItemSearchHeaderBinding import ani.dantotsu.databinding.ItemSearchHeaderBinding
import ani.dantotsu.saveData import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.imagesearch.ImageSearchActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import com.google.android.material.checkbox.MaterialCheckBox.* import com.google.android.material.checkbox.MaterialCheckBox.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class SearchAdapter(private val activity: SearchActivity) : class SearchAdapter(private val activity: SearchActivity, private val type: String) :
RecyclerView.Adapter<SearchAdapter.SearchHeaderViewHolder>() { RecyclerView.Adapter<SearchAdapter.SearchHeaderViewHolder>() {
private val itemViewType = 6969 private val itemViewType = 6969
var search: Runnable? = null var search: Runnable? = null
var requestFocus: Runnable? = null var requestFocus: Runnable? = null
private var textWatcher: TextWatcher? = null private var textWatcher: TextWatcher? = null
private lateinit var searchHistoryAdapter: SearchHistoryAdapter
private lateinit var binding: ItemSearchHeaderBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder {
val binding = val binding =
@@ -35,8 +52,13 @@ class SearchAdapter(private val activity: SearchActivity) :
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: SearchHeaderViewHolder, position: Int) { override fun onBindViewHolder(holder: SearchHeaderViewHolder, position: Int) {
val binding = holder.binding binding = holder.binding
searchHistoryAdapter = SearchHistoryAdapter(type) {
binding.searchBarText.setText(it)
}
binding.searchHistoryList.layoutManager = LinearLayoutManager(binding.root.context)
binding.searchHistoryList.adapter = searchHistoryAdapter
val imm: InputMethodManager = val imm: InputMethodManager =
activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
@@ -54,6 +76,13 @@ class SearchAdapter(private val activity: SearchActivity) :
} }
binding.searchBar.hint = activity.result.type binding.searchBar.hint = activity.result.type
if (PrefManager.getVal(PrefName.Incognito)) {
val startIconDrawableRes = R.drawable.ic_incognito_24
val startIconDrawable: Drawable? =
context?.let { AppCompatResources.getDrawable(it, startIconDrawableRes) }
binding.searchBar.startIconDrawable = startIconDrawable
}
var adult = activity.result.isAdult var adult = activity.result.isAdult
var listOnly = activity.result.onList var listOnly = activity.result.onList
@@ -66,13 +95,16 @@ class SearchAdapter(private val activity: SearchActivity) :
binding.searchChipRecycler.adapter = SearchChipAdapter(activity).also { binding.searchChipRecycler.adapter = SearchChipAdapter(activity).also {
activity.updateChips = { it.update() } activity.updateChips = { it.update() }
} }
binding.searchChipRecycler.layoutManager = binding.searchChipRecycler.layoutManager =
LinearLayoutManager(binding.root.context, HORIZONTAL, false) LinearLayoutManager(binding.root.context, HORIZONTAL, false)
binding.searchFilter.setOnClickListener { binding.searchFilter.setOnClickListener {
SearchFilterBottomDialog.newInstance().show(activity.supportFragmentManager, "dialog") SearchFilterBottomDialog.newInstance().show(activity.supportFragmentManager, "dialog")
} }
binding.searchByImage.setOnClickListener {
activity.startActivity(Intent(activity, ImageSearchActivity::class.java))
}
fun searchTitle() { fun searchTitle() {
activity.result.apply { activity.result.apply {
search = search =
@@ -80,6 +112,9 @@ class SearchAdapter(private val activity: SearchActivity) :
onList = listOnly onList = listOnly
isAdult = adult isAdult = adult
} }
if (binding.searchBarText.text.toString().equals("hentai", true)) {
openLinkInBrowser("https://www.youtube.com/watch?v=GgJrEOo0QoA")
}
activity.search() activity.search()
} }
@@ -89,7 +124,18 @@ class SearchAdapter(private val activity: SearchActivity) :
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
searchTitle() if (s.toString().isBlank()) {
activity.emptyMediaAdapter()
CoroutineScope(Dispatchers.IO).launch {
delay(200)
activity.runOnUiThread {
setHistoryVisibility(true)
}
}
} else {
setHistoryVisibility(false)
searchTitle()
}
} }
} }
binding.searchBarText.addTextChangedListener(textWatcher) binding.searchBarText.addTextChangedListener(textWatcher)
@@ -112,14 +158,14 @@ class SearchAdapter(private val activity: SearchActivity) :
it.alpha = 1f it.alpha = 1f
binding.searchResultList.alpha = 0.33f binding.searchResultList.alpha = 0.33f
activity.style = 0 activity.style = 0
saveData("searchStyle", 0) PrefManager.setVal(PrefName.SearchStyle, 0)
activity.recycler() activity.recycler()
} }
binding.searchResultList.setOnClickListener { binding.searchResultList.setOnClickListener {
it.alpha = 1f it.alpha = 1f
binding.searchResultGrid.alpha = 0.33f binding.searchResultGrid.alpha = 0.33f
activity.style = 1 activity.style = 1
saveData("searchStyle", 1) PrefManager.setVal(PrefName.SearchStyle, 1)
activity.recycler() activity.recycler()
} }
@@ -162,6 +208,43 @@ class SearchAdapter(private val activity: SearchActivity) :
requestFocus = Runnable { binding.searchBarText.requestFocus() } requestFocus = Runnable { binding.searchBarText.requestFocus() }
} }
fun setHistoryVisibility(visible: Boolean) {
if (visible) {
binding.searchResultLayout.startAnimation(fadeOutAnimation())
binding.searchHistoryList.startAnimation(fadeInAnimation())
binding.searchResultLayout.visibility = View.GONE
binding.searchHistoryList.visibility = View.VISIBLE
binding.searchByImage.visibility = View.VISIBLE
} else {
if (binding.searchResultLayout.visibility != View.VISIBLE) {
binding.searchResultLayout.startAnimation(fadeInAnimation())
binding.searchHistoryList.startAnimation(fadeOutAnimation())
}
binding.searchResultLayout.visibility = View.VISIBLE
binding.searchHistoryList.visibility = View.GONE
binding.searchByImage.visibility = View.GONE
}
}
private fun fadeInAnimation(): Animation {
return AlphaAnimation(0f, 1f).apply {
duration = 150
fillAfter = true
}
}
private fun fadeOutAnimation(): Animation {
return AlphaAnimation(1f, 0f).apply {
duration = 150
fillAfter = true
}
}
fun addHistory() {
searchHistoryAdapter.add(binding.searchBarText.text.toString())
}
override fun getItemCount(): Int = 1 override fun getItemCount(): Int = 1

View File

@@ -17,6 +17,7 @@ import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.BottomSheetSearchFilterBinding import ani.dantotsu.databinding.BottomSheetSearchFilterBinding
import ani.dantotsu.databinding.ItemChipBinding import ani.dantotsu.databinding.ItemChipBinding
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import java.util.Calendar
class SearchFilterBottomDialog : BottomSheetDialogFragment() { class SearchFilterBottomDialog : BottomSheetDialogFragment() {
private var _binding: BottomSheetSearchFilterBinding? = null private var _binding: BottomSheetSearchFilterBinding? = null
@@ -103,7 +104,8 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
ArrayAdapter( ArrayAdapter(
binding.root.context, binding.root.context,
R.layout.item_dropdown, R.layout.item_dropdown,
(1970 until 2025).map { it.toString() }.reversed().toTypedArray() (1970 until Calendar.getInstance().get(Calendar.YEAR) + 2).map { it.toString() }
.reversed().toTypedArray()
) )
) )
} }

View File

@@ -0,0 +1,100 @@
package ani.dantotsu.media
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.databinding.ItemSearchHistoryBinding
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefManager.asLiveStringSet
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.settings.saving.SharedPreferenceStringSetLiveData
import java.util.Locale
class SearchHistoryAdapter(private val type: String, private val searchClicked: (String) -> Unit) :
ListAdapter<String, SearchHistoryAdapter.SearchHistoryViewHolder>(
DIFF_CALLBACK_INSTALLED
) {
private var searchHistoryLiveData: SharedPreferenceStringSetLiveData? = null
private var searchHistory: MutableSet<String>? = null
private var historyType: PrefName = when (type.lowercase(Locale.ROOT)) {
"anime" -> PrefName.AnimeSearchHistory
"manga" -> PrefName.MangaSearchHistory
else -> throw IllegalArgumentException("Invalid type")
}
init {
searchHistoryLiveData =
PrefManager.getLiveVal(historyType, mutableSetOf<String>()).asLiveStringSet()
searchHistoryLiveData?.observeForever {
searchHistory = it.toMutableSet()
submitList(searchHistory?.toList())
}
}
fun remove(item: String) {
searchHistory?.remove(item)
PrefManager.setVal(historyType, searchHistory)
submitList(searchHistory?.toList())
}
fun add(item: String) {
if (searchHistory?.contains(item) == true || item.isBlank()) return
if (PrefManager.getVal(PrefName.Incognito)) return
searchHistory?.add(item)
submitList(searchHistory?.toList())
PrefManager.setVal(historyType, searchHistory)
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): SearchHistoryAdapter.SearchHistoryViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_search_history, parent, false)
return SearchHistoryViewHolder(view)
}
override fun onBindViewHolder(
holder: SearchHistoryAdapter.SearchHistoryViewHolder,
position: Int
) {
val item = getItem(position)
holder.binding.searchHistoryTextView.text = item
holder.binding.closeTextView.setOnClickListener {
val currentPosition = holder.bindingAdapterPosition
if (currentPosition >= itemCount || currentPosition < 0) return@setOnClickListener
remove(getItem(currentPosition))
}
holder.binding.searchHistoryTextView.setOnClickListener {
val currentPosition = holder.bindingAdapterPosition
if (currentPosition >= itemCount || currentPosition < 0) return@setOnClickListener
searchClicked(getItem(currentPosition))
}
}
inner class SearchHistoryViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val binding = ItemSearchHistoryBinding.bind(view)
}
companion object {
val DIFF_CALLBACK_INSTALLED = object : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(
oldItem: String,
newItem: String
): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(
oldItem: String,
newItem: String
): Boolean {
return oldItem == newItem
}
}
}
}

View File

@@ -18,7 +18,6 @@ import ani.dantotsu.Refresh
import ani.dantotsu.databinding.ActivityStudioBinding import ani.dantotsu.databinding.ActivityStudioBinding
import ani.dantotsu.initActivity import ani.dantotsu.initActivity
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.others.LangSet
import ani.dantotsu.others.getSerialized import ani.dantotsu.others.getSerialized
import ani.dantotsu.px import ani.dantotsu.px
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
@@ -36,7 +35,7 @@ class StudioActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
binding = ActivityStudioBinding.inflate(layoutInflater) binding = ActivityStudioBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)

View File

@@ -1,19 +1,23 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.content.Context import android.content.Context
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.parsers.SubtitleType import ani.dantotsu.parsers.SubtitleType
import ani.dantotsu.snackString
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.Request import okhttp3.Request
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File
class SubtitleDownloader { class SubtitleDownloader {
companion object { companion object {
//doesn't really download the subtitles -\_(o_o)_/- //doesn't really download the subtitles -\_(o_o)_/-
suspend fun downloadSubtitles(context: Context, url: String): SubtitleType = suspend fun loadSubtitleType(context: Context, url: String): SubtitleType =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
// Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it // Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
val networkHelper = Injekt.get<NetworkHelper>() val networkHelper = Injekt.get<NetworkHelper>()
@@ -29,8 +33,8 @@ class SubtitleDownloader {
val subtitleType = when { val subtitleType = when {
responseBody?.contains("[Script Info]") == true -> SubtitleType.ASS responseBody.contains("[Script Info]") -> SubtitleType.ASS
responseBody?.contains("WEBVTT") == true -> SubtitleType.VTT responseBody.contains("WEBVTT") -> SubtitleType.VTT
else -> SubtitleType.SRT else -> SubtitleType.SRT
} }
@@ -39,5 +43,50 @@ class SubtitleDownloader {
return@withContext SubtitleType.UNKNOWN return@withContext SubtitleType.UNKNOWN
} }
} }
//actually downloads lol
suspend fun downloadSubtitle(
context: Context,
url: String,
downloadedType: DownloadedType
) {
try {
val directory = DownloadsManager.getDirectory(
context,
downloadedType.type,
downloadedType.title,
downloadedType.chapter
)
if (!directory.exists()) { //just in case
directory.mkdirs()
}
val type = loadSubtitleType(context, url)
val subtiteFile = File(directory, "subtitle.${type}")
if (subtiteFile.exists()) {
subtiteFile.delete()
}
subtiteFile.createNewFile()
val client = Injekt.get<NetworkHelper>().client
val request = Request.Builder().url(url).build()
val reponse = client.newCall(request).execute()
if (!reponse.isSuccessful) {
snackString("Failed to download subtitle")
return
}
reponse.body.byteStream().use { input ->
subtiteFile.outputStream().use { output ->
input.copyTo(output)
}
}
} catch (e: Exception) {
snackString("Failed to download subtitle")
e.printStackTrace()
return
}
}
} }
} }

View File

@@ -0,0 +1,136 @@
package ani.dantotsu.media
import android.graphics.Color
import android.view.ViewGroup
import androidx.core.view.updateLayoutParams
import ani.dantotsu.R
import ani.dantotsu.navBarHeight
import nl.joery.animatedbottombar.AnimatedBottomBar
class TripleNavAdapter(
private val nav1: AnimatedBottomBar,
private val nav2: AnimatedBottomBar,
private val nav3: AnimatedBottomBar,
anime: Boolean,
format: String,
private val isScreenVertical: Boolean = false
) {
var selected: Int = 0
var selectionListener: ((Int, Int) -> Unit)? = null
init {
nav1.tabs.clear()
nav2.tabs.clear()
nav3.tabs.clear()
val infoTab = nav1.createTab(R.drawable.ic_round_info_24, R.string.info, R.id.info)
val watchTab = if (anime) {
nav2.createTab(R.drawable.ic_round_movie_filter_24, R.string.watch, R.id.watch)
} else if (format == "NOVEL") {
nav2.createTab(R.drawable.ic_round_book_24, R.string.read, R.id.read)
} else {
nav2.createTab(R.drawable.ic_round_import_contacts_24, R.string.read, R.id.read)
}
val commentTab = nav3.createTab(R.drawable.ic_round_comment_24, R.string.comments, R.id.comment)
nav1.addTab(infoTab)
nav1.visibility = ViewGroup.VISIBLE
if (isScreenVertical) {
nav2.visibility = ViewGroup.GONE
nav3.visibility = ViewGroup.GONE
nav1.addTab(watchTab)
nav1.addTab(commentTab)
nav1.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
nav2.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
nav3.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
} else {
nav1.indicatorColor = Color.TRANSPARENT
nav2.indicatorColor = Color.TRANSPARENT
nav3.indicatorColor = Color.TRANSPARENT
nav2.visibility = ViewGroup.VISIBLE
nav3.visibility = ViewGroup.VISIBLE
nav2.addTab(watchTab)
nav3.addTab(commentTab)
nav2.setOnTabSelectListener(object : AnimatedBottomBar.OnTabSelectListener {
override fun onTabSelected(
lastIndex: Int,
lastTab: AnimatedBottomBar.Tab?,
newIndex: Int,
newTab: AnimatedBottomBar.Tab
) {
selected = 1
deselectOthers(selected)
selectionListener?.invoke(selected, newTab.id)
}
})
nav3.setOnTabSelectListener(object : AnimatedBottomBar.OnTabSelectListener {
override fun onTabSelected(
lastIndex: Int,
lastTab: AnimatedBottomBar.Tab?,
newIndex: Int,
newTab: AnimatedBottomBar.Tab
) {
selected = 2
deselectOthers(selected)
selectionListener?.invoke(selected, newTab.id)
}
})
}
nav1.setOnTabSelectListener(object : AnimatedBottomBar.OnTabSelectListener {
override fun onTabSelected(
lastIndex: Int,
lastTab: AnimatedBottomBar.Tab?,
newIndex: Int,
newTab: AnimatedBottomBar.Tab
) {
if (!isScreenVertical) {
selected = 0
deselectOthers(selected)
} else selected = newIndex
selectionListener?.invoke(selected, newTab.id)
}
})
}
private fun deselectOthers(selected: Int) {
if (selected == 0) {
nav2.clearSelection()
nav3.clearSelection()
}
if (selected == 1) {
nav1.clearSelection()
nav3.clearSelection()
}
if (selected == 2) {
nav1.clearSelection()
nav2.clearSelection()
}
}
fun selectTab(tab: Int) {
selected = tab
if (!isScreenVertical) {
when (tab) {
0 -> nav1.selectTabAt(0)
1 -> nav2.selectTabAt(0)
2 -> nav3.selectTabAt(0)
}
deselectOthers(selected)
} else {
nav1.selectTabAt(selected)
}
}
fun setVisibility(visibility: Int) {
if (isScreenVertical) {
nav1.visibility = visibility
return
}
nav1.visibility = visibility
nav2.visibility = visibility
nav3.visibility = visibility
}
}

View File

@@ -1,12 +1,68 @@
package ani.dantotsu.media.anime package ani.dantotsu.media.anime
import java.util.Locale
import java.util.regex.Matcher import java.util.regex.Matcher
import java.util.regex.Pattern import java.util.regex.Pattern
class AnimeNameAdapter { class AnimeNameAdapter {
companion object { companion object {
const val episodeRegex =
"(episode|episodio|ep|e)[\\s:.\\-]*([\\d]+\\.?[\\d]*)[\\s:.\\-]*\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*"
const val failedEpisodeNumberRegex =
"(?<!part\\s)\\b(\\d+)\\b"
const val seasonRegex = "(season|s)[\\s:.\\-]*(\\d+)[\\s:.\\-]*"
const val subdubRegex = "^(soft)?[\\s-]*(sub|dub|mixed)(bed|s)?\\s*$"
fun setSubDub(text: String, typeToSetTo: SubDubType): String? {
val subdubPattern: Pattern = Pattern.compile(subdubRegex, Pattern.CASE_INSENSITIVE)
val subdubMatcher: Matcher = subdubPattern.matcher(text)
return if (subdubMatcher.find()) {
val soft = subdubMatcher.group(1)
val subdub = subdubMatcher.group(2)
val bed = subdubMatcher.group(3) ?: ""
val toggled = when (typeToSetTo) {
SubDubType.SUB -> "sub"
SubDubType.DUB -> "dub"
SubDubType.NULL -> ""
}
val toggledCasePreserved =
if (subdub?.get(0)?.isUpperCase() == true || soft?.get(0)
?.isUpperCase() == true
) toggled.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(
Locale.ROOT
) else it.toString()
} else toggled
subdubMatcher.replaceFirst(toggledCasePreserved + bed)
} else {
null
}
}
fun getSubDub(text: String): SubDubType {
val subdubPattern: Pattern = Pattern.compile(subdubRegex, Pattern.CASE_INSENSITIVE)
val subdubMatcher: Matcher = subdubPattern.matcher(text)
return if (subdubMatcher.find()) {
val subdub = subdubMatcher.group(2)?.lowercase(Locale.ROOT)
when (subdub) {
"sub" -> SubDubType.SUB
"dub" -> SubDubType.DUB
else -> SubDubType.NULL
}
} else {
SubDubType.NULL
}
}
enum class SubDubType {
SUB, DUB, NULL
}
fun findSeasonNumber(text: String): Int? { fun findSeasonNumber(text: String): Int? {
val seasonRegex = "(season|s)[\\s:.\\-]*(\\d+)"
val seasonPattern: Pattern = Pattern.compile(seasonRegex, Pattern.CASE_INSENSITIVE) val seasonPattern: Pattern = Pattern.compile(seasonRegex, Pattern.CASE_INSENSITIVE)
val seasonMatcher: Matcher = seasonPattern.matcher(text) val seasonMatcher: Matcher = seasonPattern.matcher(text)
@@ -16,5 +72,56 @@ class AnimeNameAdapter {
null null
} }
} }
fun findEpisodeNumber(text: String): Float? {
val episodePattern: Pattern = Pattern.compile(episodeRegex, Pattern.CASE_INSENSITIVE)
val episodeMatcher: Matcher = episodePattern.matcher(text)
return if (episodeMatcher.find()) {
if (episodeMatcher.group(2) != null) {
episodeMatcher.group(2)?.toFloat()
} else {
val failedEpisodeNumberPattern: Pattern =
Pattern.compile(failedEpisodeNumberRegex, Pattern.CASE_INSENSITIVE)
val failedEpisodeNumberMatcher: Matcher =
failedEpisodeNumberPattern.matcher(text)
if (failedEpisodeNumberMatcher.find()) {
failedEpisodeNumberMatcher.group(1)?.toFloat()
} else {
null
}
}
} else {
null
}
}
fun removeEpisodeNumber(text: String): String {
val regexPattern = Regex(episodeRegex, RegexOption.IGNORE_CASE)
val removedNumber = text.replace(regexPattern, "").ifEmpty {
text
}
val letterPattern = Regex("[a-zA-Z]")
return if (letterPattern.containsMatchIn(removedNumber)) {
removedNumber
} else {
text
}
}
fun removeEpisodeNumberCompletely(text: String): String {
val regexPattern = Regex(episodeRegex, RegexOption.IGNORE_CASE)
val removedNumber = text.replace(regexPattern, "")
return if (removedNumber.equals(text, true)) { // if nothing was removed
val failedEpisodeNumberPattern: Regex =
Regex(failedEpisodeNumberRegex, RegexOption.IGNORE_CASE)
failedEpisodeNumberPattern.replace(removedNumber) { mr ->
mr.value.replaceFirst(mr.groupValues[1], "")
}
} else {
removedNumber
}
}
} }
} }

View File

@@ -5,28 +5,38 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import ani.dantotsu.settings.FAQActivity
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.ImageView import android.widget.ImageButton
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.startActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.databinding.DialogLayoutBinding
import ani.dantotsu.databinding.ItemAnimeWatchBinding import ani.dantotsu.databinding.ItemAnimeWatchBinding
import ani.dantotsu.databinding.ItemChipBinding import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.SourceSearchDialogFragment import ani.dantotsu.media.SourceSearchDialogFragment
import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.others.webview.CookieCatcher
import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.DynamicAnimeParser import ani.dantotsu.parsers.DynamicAnimeParser
import ani.dantotsu.parsers.WatchSources import ani.dantotsu.parsers.WatchSources
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId import ani.dantotsu.settings.saving.PrefName
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_SUBSCRIPTION_CHECK
import eu.kanade.tachiyomi.util.system.WebViewUtil
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class AnimeWatchAdapter( class AnimeWatchAdapter(
private val media: Media, private val media: Media,
private val fragment: AnimeWatchFragment, private val fragment: AnimeWatchFragment,
@@ -41,20 +51,29 @@ class AnimeWatchAdapter(
return ViewHolder(bind) return ViewHolder(bind)
} }
private var nestedDialog: AlertDialog? = null
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding = holder.binding val binding = holder.binding
_binding = binding _binding = binding
binding.faqbutton.setOnClickListener {
startActivity(
fragment.requireContext(),
Intent(fragment.requireContext(), FAQActivity::class.java),
null
)
}
//Youtube //Youtube
if (media.anime!!.youtube != null && fragment.uiSettings.showYtButton) { if (media.anime?.youtube != null && PrefManager.getVal(PrefName.ShowYtButton)) {
binding.animeSourceYT.visibility = View.VISIBLE binding.animeSourceYT.visibility = View.VISIBLE
binding.animeSourceYT.setOnClickListener { binding.animeSourceYT.setOnClickListener {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(media.anime.youtube)) val intent = Intent(Intent.ACTION_VIEW, Uri.parse(media.anime.youtube))
fragment.requireContext().startActivity(intent) fragment.requireContext().startActivity(intent)
} }
} }
binding.animeSourceDubbed.isChecked = media.selected!!.preferDub binding.animeSourceDubbed.isChecked = media.selected!!.preferDub
binding.animeSourceDubbedText.text = binding.animeSourceDubbedText.text =
if (media.selected!!.preferDub) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString( if (media.selected!!.preferDub) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(
@@ -78,6 +97,15 @@ class AnimeWatchAdapter(
null null
) )
} }
val offline = if (!isOnline(binding.root.context) || PrefManager.getVal(
PrefName.OfflineMode
)
) View.GONE else View.VISIBLE
binding.animeSourceNameContainer.visibility = offline
binding.animeSourceSettings.visibility = offline
binding.animeSourceSearch.visibility = offline
binding.animeSourceTitle.visibility = offline
//Source Selection //Source Selection
var source = var source =
@@ -90,7 +118,7 @@ class AnimeWatchAdapter(
binding.animeSourceTitle.text = showUserText binding.animeSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } } showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
binding.animeSourceDubbedCont.visibility = binding.animeSourceDubbedCont.visibility =
if (isDubAvailableSeparately) View.VISIBLE else View.GONE if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
} }
} }
@@ -110,7 +138,7 @@ class AnimeWatchAdapter(
binding.animeSourceDubbed.isChecked = selectDub binding.animeSourceDubbed.isChecked = selectDub
changing = false changing = false
binding.animeSourceDubbedCont.visibility = binding.animeSourceDubbedCont.visibility =
if (isDubAvailableSeparately) View.VISIBLE else View.GONE if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
source = i source = i
setLanguageList(0, i) setLanguageList(0, i)
} }
@@ -131,7 +159,7 @@ class AnimeWatchAdapter(
binding.animeSourceDubbed.isChecked = selectDub binding.animeSourceDubbed.isChecked = selectDub
changing = false changing = false
binding.animeSourceDubbedCont.visibility = binding.animeSourceDubbedCont.visibility =
if (isDubAvailableSeparately) View.VISIBLE else View.GONE if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
setLanguageList(i, source) setLanguageList(i, source)
} }
subscribeButton(false) subscribeButton(false)
@@ -147,8 +175,9 @@ class AnimeWatchAdapter(
} }
} }
//Icons
//Subscription //subscribe
subscribe = MediaDetailsActivity.PopImageButton( subscribe = MediaDetailsActivity.PopImageButton(
fragment.lifecycleScope, fragment.lifecycleScope,
binding.animeSourceSubscribe, binding.animeSourceSubscribe,
@@ -156,7 +185,8 @@ class AnimeWatchAdapter(
R.drawable.ic_round_notifications_none_24, R.drawable.ic_round_notifications_none_24,
R.color.bg_opp, R.color.bg_opp,
R.color.violet_400, R.color.violet_400,
fragment.subscribed fragment.subscribed,
true
) { ) {
fragment.onNotificationPressed(it, binding.animeSource.text.toString()) fragment.onNotificationPressed(it, binding.animeSource.text.toString())
} }
@@ -164,47 +194,110 @@ class AnimeWatchAdapter(
subscribeButton(false) subscribeButton(false)
binding.animeSourceSubscribe.setOnLongClickListener { binding.animeSourceSubscribe.setOnLongClickListener {
openSettings(fragment.requireContext(), getChannelId(true, media.id)) openSettings(fragment.requireContext(), CHANNEL_SUBSCRIPTION_CHECK)
} }
//Icons //Nested Button
var reversed = media.selected!!.recyclerReversed binding.animeNestedButton.setOnClickListener {
var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.animeDefaultView val dialogView =
binding.animeSourceTop.rotation = if (reversed) -90f else 90f LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null)
binding.animeSourceTop.setOnClickListener { val dialogBinding = DialogLayoutBinding.bind(dialogView)
reversed = !reversed var refresh = false
binding.animeSourceTop.rotation = if (reversed) -90f else 90f var run = false
fragment.onIconPressed(style, reversed) var reversed = media.selected!!.recyclerReversed
} var style =
var selected = when (style) { media.selected!!.recyclerStyle ?: PrefManager.getVal(PrefName.AnimeDefaultView)
0 -> binding.animeSourceList dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
1 -> binding.animeSourceGrid dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
2 -> binding.animeSourceCompact dialogBinding.animeSourceTop.setOnClickListener {
else -> binding.animeSourceList reversed = !reversed
} dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
selected.alpha = 1f dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
fun selected(it: ImageView) { run = true
selected.alpha = 0.33f }
selected = it //Grids
var selected = when (style) {
0 -> dialogBinding.animeSourceList
1 -> dialogBinding.animeSourceGrid
2 -> dialogBinding.animeSourceCompact
else -> dialogBinding.animeSourceList
}
when (style) {
0 -> dialogBinding.layoutText.text = "List"
1 -> dialogBinding.layoutText.text = "Grid"
2 -> dialogBinding.layoutText.text = "Compact"
else -> dialogBinding.animeSourceList
}
selected.alpha = 1f selected.alpha = 1f
fun selected(it: ImageButton) {
selected.alpha = 0.33f
selected = it
selected.alpha = 1f
}
dialogBinding.animeSourceList.setOnClickListener {
selected(it as ImageButton)
style = 0
dialogBinding.layoutText.text = "List"
run = true
}
dialogBinding.animeSourceGrid.setOnClickListener {
selected(it as ImageButton)
style = 1
dialogBinding.layoutText.text = "Grid"
run = true
}
dialogBinding.animeSourceCompact.setOnClickListener {
selected(it as ImageButton)
style = 2
dialogBinding.layoutText.text = "Compact"
run = true
}
dialogBinding.animeWebviewContainer.setOnClickListener {
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
toast("WebView not installed")
}
//start CookieCatcher activity
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
val sourceAHH = watchSources[source] as? DynamicAnimeParser
val sourceHttp =
sourceAHH?.extension?.sources?.firstOrNull() as? AnimeHttpSource
val url = sourceHttp?.baseUrl
url?.let {
refresh = true
val headersMap = try {
sourceHttp.headers.toMultimap()
.mapValues { it.value.getOrNull(0) ?: "" }
} catch (e: Exception) {
emptyMap()
}
val intent = Intent(fragment.requireContext(), CookieCatcher::class.java)
.putExtra("url", url)
.putExtra("headers", headersMap as HashMap<String, String>)
startActivity(fragment.requireContext(), intent, null)
}
}
}
//hidden
dialogBinding.animeScanlatorContainer.visibility = View.GONE
dialogBinding.animeDownloadContainer.visibility = View.GONE
nestedDialog = AlertDialog.Builder(fragment.requireContext(), R.style.MyPopup)
.setTitle("Options")
.setView(dialogView)
.setPositiveButton("OK") { _, _ ->
if (run) fragment.onIconPressed(style, reversed)
if (refresh) fragment.loadEpisodes(source, true)
}
.setNegativeButton("Cancel") { _, _ ->
if (refresh) fragment.loadEpisodes(source, true)
}
.setOnCancelListener {
if (refresh) fragment.loadEpisodes(source, true)
}
.create()
nestedDialog?.show()
} }
binding.animeSourceList.setOnClickListener {
selected(it as ImageView)
style = 0
fragment.onIconPressed(style, reversed)
}
binding.animeSourceGrid.setOnClickListener {
selected(it as ImageView)
style = 1
fragment.onIconPressed(style, reversed)
}
binding.animeSourceCompact.setOnClickListener {
selected(it as ImageView)
style = 2
fragment.onIconPressed(style, reversed)
}
binding.animeScanlatorTop.visibility = View.GONE
binding.animeDownloadTop.visibility = View.GONE
//Episode Handling //Episode Handling
handleEpisodes() handleEpisodes()
} }
@@ -278,7 +371,9 @@ class AnimeWatchAdapter(
val episodes = media.anime.episodes!!.keys.toTypedArray() val episodes = media.anime.episodes!!.keys.toTypedArray()
val anilistEp = (media.userProgress ?: 0).plus(1) val anilistEp = (media.userProgress ?: 0).plus(1)
val appEp = loadData<String>("${media.id}_current_ep")?.toIntOrNull() ?: 1 val appEp =
PrefManager.getCustomVal<String?>("${media.id}_current_ep", "")?.toIntOrNull()
?: 1
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString() var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
if (episodes.contains(continueEp)) { if (episodes.contains(continueEp)) {
@@ -290,7 +385,10 @@ class AnimeWatchAdapter(
media.id, media.id,
continueEp continueEp
) )
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight > fragment.playerSettings.watchPercentage) { if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight > PrefManager.getVal<Float>(
PrefName.WatchPercentage
)
) {
val e = episodes.indexOf(continueEp) val e = episodes.indexOf(continueEp)
if (e != -1 && e + 1 < episodes.size) { if (e != -1 && e + 1 < episodes.size) {
continueEp = episodes[e + 1] continueEp = episodes[e + 1]
@@ -304,17 +402,23 @@ class AnimeWatchAdapter(
} }
} }
val ep = media.anime.episodes!![continueEp]!! val ep = media.anime.episodes!![continueEp]!!
val cleanedTitle = ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) }
binding.itemEpisodeImage.loadImage( binding.itemEpisodeImage.loadImage(
ep.thumb ?: FileUrl[media.banner ?: media.cover], 0 ep.thumb ?: FileUrl[media.banner ?: media.cover], 0
) )
if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE
binding.animeSourceContinueText.text = binding.animeSourceContinueText.text =
currActivity()!!.getString(R.string.continue_episode) + "${ep.number}${if (ep.filler) " - Filler" else ""}${if (ep.title != null) "\n${ep.title}" else ""}" currActivity()!!.getString(R.string.continue_episode) + "${ep.number}${if (ep.filler) " - Filler" else ""}${"\n$cleanedTitle"}"
binding.animeSourceContinue.setOnClickListener { binding.animeSourceContinue.setOnClickListener {
fragment.onEpisodeClick(continueEp) fragment.onEpisodeClick(continueEp)
} }
if (fragment.continueEp) { if (fragment.continueEp) {
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight < fragment.playerSettings.watchPercentage) { if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight < PrefManager.getVal<Float>(
PrefName.WatchPercentage
)
) {
binding.animeSourceContinue.performClick() binding.animeSourceContinue.performClick()
fragment.continueEp = false fragment.continueEp = false
} }
@@ -322,21 +426,26 @@ class AnimeWatchAdapter(
} else { } else {
binding.animeSourceContinue.visibility = View.GONE binding.animeSourceContinue.visibility = View.GONE
} }
binding.animeSourceProgressBar.visibility = View.GONE binding.animeSourceProgressBar.visibility = View.GONE
if (media.anime.episodes!!.isNotEmpty()) if (media.anime.episodes!!.isNotEmpty()) {
binding.animeSourceNotFound.visibility = View.GONE binding.animeSourceNotFound.visibility = View.GONE
else binding.faqbutton.visibility = View.GONE}
else {
binding.animeSourceNotFound.visibility = View.VISIBLE binding.animeSourceNotFound.visibility = View.VISIBLE
binding.faqbutton.visibility = View.VISIBLE
}
} else { } else {
binding.animeSourceContinue.visibility = View.GONE binding.animeSourceContinue.visibility = View.GONE
binding.animeSourceNotFound.visibility = View.GONE binding.animeSourceNotFound.visibility = View.GONE
binding.faqbutton.visibility = View.GONE
clearChips() clearChips()
binding.animeSourceProgressBar.visibility = View.VISIBLE binding.animeSourceProgressBar.visibility = View.VISIBLE
} }
} }
} }
fun setLanguageList(lang: Int, source: Int) { private fun setLanguageList(lang: Int, source: Int) {
val binding = _binding val binding = _binding
if (watchSources is AnimeSources) { if (watchSources is AnimeSources) {
val parser = watchSources[source] as? DynamicAnimeParser val parser = watchSources[source] as? DynamicAnimeParser
@@ -351,12 +460,16 @@ class AnimeWatchAdapter(
parser.extension.sources.firstOrNull()?.lang ?: "Unknown" parser.extension.sources.firstOrNull()?.lang ?: "Unknown"
) )
} }
binding?.animeSourceLanguage?.setAdapter( val adapter = ArrayAdapter(
ArrayAdapter( fragment.requireContext(),
fragment.requireContext(), R.layout.item_dropdown,
R.layout.item_dropdown, parser.extension.sources.map { LanguageMapper.mapLanguageCodeToName(it.lang) }
parser.extension.sources.map { it.lang })
) )
val items = adapter.count
binding?.animeSourceLanguageContainer?.visibility =
if (items > 1) View.VISIBLE else View.GONE
binding?.animeSourceLanguage?.setAdapter(adapter)
} }
} }
@@ -371,4 +484,4 @@ class AnimeWatchAdapter(
countDown(media, binding.animeSourceContainer) countDown(media, binding.animeSourceContainer)
} }
} }
} }

View File

@@ -2,6 +2,10 @@ package ani.dantotsu.media.anime
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog import android.app.AlertDialog
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
@@ -9,39 +13,47 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.Toast import android.widget.Toast
import androidx.annotation.OptIn
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils import androidx.core.math.MathUtils
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.DownloadService
import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.databinding.FragmentAnimeWatchBinding import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.anime.AnimeDownloaderService
import ani.dantotsu.download.video.ExoplayerDownloadService
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.parsers.AnimeParser import ani.dantotsu.parsers.AnimeParser
import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.HAnimeSources import ani.dantotsu.parsers.HAnimeSources
import ani.dantotsu.settings.PlayerSettings
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
import ani.dantotsu.subcriptions.Notifications import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.subcriptions.Notifications.Group.ANIME_GROUP import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId import ani.dantotsu.notifications.subscription.SubscriptionHelper
import ani.dantotsu.subcriptions.SubscriptionHelper import ani.dantotsu.notifications.subscription.SubscriptionHelper.Companion.saveSubscription
import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.navigationrail.NavigationRailView
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.max import kotlin.math.max
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -61,14 +73,14 @@ class AnimeWatchFragment : Fragment() {
private lateinit var headerAdapter: AnimeWatchAdapter private lateinit var headerAdapter: AnimeWatchAdapter
private lateinit var episodeAdapter: EpisodeAdapter private lateinit var episodeAdapter: EpisodeAdapter
val downloadManager = Injekt.get<DownloadsManager>()
var screenWidth = 0f var screenWidth = 0f
private var progress = View.VISIBLE private var progress = View.VISIBLE
var continueEp: Boolean = false var continueEp: Boolean = false
var loaded = false var loaded = false
lateinit var playerSettings: PlayerSettings
lateinit var uiSettings: UserInterfaceSettings
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@@ -80,18 +92,27 @@ class AnimeWatchFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val intentFilter = IntentFilter().apply {
addAction(ACTION_DOWNLOAD_STARTED)
addAction(ACTION_DOWNLOAD_FINISHED)
addAction(ACTION_DOWNLOAD_FAILED)
addAction(ACTION_DOWNLOAD_PROGRESS)
}
ContextCompat.registerReceiver(
requireContext(),
downloadStatusReceiver,
intentFilter,
ContextCompat.RECEIVER_EXPORTED
)
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight) binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight)
screenWidth = resources.displayMetrics.widthPixels.dp screenWidth = resources.displayMetrics.widthPixels.dp
var maxGridSize = (screenWidth / 100f).roundToInt() var maxGridSize = (screenWidth / 100f).roundToInt()
maxGridSize = max(4, maxGridSize - (maxGridSize % 2)) maxGridSize = max(4, maxGridSize - (maxGridSize % 2))
playerSettings =
loadData("player_settings", toast = false)
?: PlayerSettings().apply { saveData("player_settings", this) }
uiSettings = loadData("ui_settings", toast = false)
?: UserInterfaceSettings().apply { saveData("ui_settings", this) }
val gridLayoutManager = GridLayoutManager(requireContext(), maxGridSize) val gridLayoutManager = GridLayoutManager(requireContext(), maxGridSize)
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
@@ -112,6 +133,23 @@ class AnimeWatchFragment : Fragment() {
binding.animeSourceRecycler.layoutManager = gridLayoutManager binding.animeSourceRecycler.layoutManager = gridLayoutManager
binding.ScrollTop.setOnClickListener {
binding.animeSourceRecycler.scrollToPosition(10)
binding.animeSourceRecycler.smoothScrollToPosition(0)
}
binding.animeSourceRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val position = gridLayoutManager.findFirstVisibleItemPosition()
if (position > 2) {
binding.ScrollTop.translationY = -navBarHeight.toFloat()
binding.ScrollTop.visibility = View.VISIBLE
} else {
binding.ScrollTop.visibility = View.GONE
}
}
})
model.scrolledToTop.observe(viewLifecycleOwner) { model.scrolledToTop.observe(viewLifecycleOwner) {
if (it) binding.animeSourceRecycler.scrollToPosition(0) if (it) binding.animeSourceRecycler.scrollToPosition(0)
} }
@@ -123,7 +161,7 @@ class AnimeWatchFragment : Fragment() {
media.selected = model.loadSelected(media) media.selected = model.loadSelected(media)
subscribed = subscribed =
SubscriptionHelper.getSubscriptions(requireContext()).containsKey(media.id) SubscriptionHelper.getSubscriptions().containsKey(media.id)
style = media.selected!!.recyclerStyle style = media.selected!!.recyclerStyle
reverse = media.selected!!.recyclerReversed reverse = media.selected!!.recyclerReversed
@@ -134,9 +172,17 @@ class AnimeWatchFragment : Fragment() {
if (!loaded) { if (!loaded) {
model.watchSources = if (media.isAdult) HAnimeSources else AnimeSources model.watchSources = if (media.isAdult) HAnimeSources else AnimeSources
val offlineMode =
model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)
headerAdapter = AnimeWatchAdapter(it, this, model.watchSources!!) headerAdapter = AnimeWatchAdapter(it, this, model.watchSources!!)
episodeAdapter = episodeAdapter =
EpisodeAdapter(style ?: uiSettings.animeDefaultView, media, this) EpisodeAdapter(
style ?: PrefManager.getVal(PrefName.AnimeDefaultView),
media,
this,
offlineMode = offlineMode
)
binding.animeSourceRecycler.adapter = binding.animeSourceRecycler.adapter =
ConcatAdapter(headerAdapter, episodeAdapter) ConcatAdapter(headerAdapter, episodeAdapter)
@@ -169,12 +215,15 @@ class AnimeWatchFragment : Fragment() {
if (media.anime?.kitsuEpisodes != null) { if (media.anime?.kitsuEpisodes != null) {
if (media.anime!!.kitsuEpisodes!!.containsKey(i)) { if (media.anime!!.kitsuEpisodes!!.containsKey(i)) {
episode.desc = episode.desc =
episode.desc ?: media.anime!!.kitsuEpisodes!![i]?.desc media.anime!!.kitsuEpisodes!![i]?.desc ?: episode.desc
episode.title = episode.title = if (AnimeNameAdapter.removeEpisodeNumberCompletely(
episode.title ?: media.anime!!.kitsuEpisodes!![i]?.title episode.title ?: ""
episode.thumb = ).isBlank()
episode.thumb ?: media.anime!!.kitsuEpisodes!![i]?.thumb ) media.anime!!.kitsuEpisodes!![i]?.title
?: FileUrl[media.cover] ?: episode.title else episode.title
?: media.anime!!.kitsuEpisodes!![i]?.title ?: episode.title
episode.thumb = media.anime!!.kitsuEpisodes!![i]?.thumb
?: FileUrl[media.cover]
} }
} }
} }
@@ -230,7 +279,7 @@ class AnimeWatchFragment : Fragment() {
model.watchSources?.get(selected.sourceIndex)?.showUserTextListener = null model.watchSources?.get(selected.sourceIndex)?.showUserTextListener = null
selected.sourceIndex = i selected.sourceIndex = i
selected.server = null selected.server = null
model.saveSelected(media.id, selected, requireActivity()) model.saveSelected(media.id, selected)
media.selected = selected media.selected = selected
return model.watchSources?.get(i)!! return model.watchSources?.get(i)!!
} }
@@ -238,7 +287,7 @@ class AnimeWatchFragment : Fragment() {
fun onLangChange(i: Int) { fun onLangChange(i: Int) {
val selected = model.loadSelected(media) val selected = model.loadSelected(media)
selected.langIndex = i selected.langIndex = i
model.saveSelected(media.id, selected, requireActivity()) model.saveSelected(media.id, selected)
media.selected = selected media.selected = selected
} }
@@ -246,7 +295,7 @@ class AnimeWatchFragment : Fragment() {
val selected = model.loadSelected(media) val selected = model.loadSelected(media)
model.watchSources?.get(selected.sourceIndex)?.selectDub = checked model.watchSources?.get(selected.sourceIndex)?.selectDub = checked
selected.preferDub = checked selected.preferDub = checked
model.saveSelected(media.id, selected, requireActivity()) model.saveSelected(media.id, selected)
media.selected = selected media.selected = selected
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
model.forceLoadEpisode( model.forceLoadEpisode(
@@ -265,7 +314,7 @@ class AnimeWatchFragment : Fragment() {
reverse = rev reverse = rev
media.selected!!.recyclerStyle = style media.selected!!.recyclerStyle = style
media.selected!!.recyclerReversed = reverse media.selected!!.recyclerReversed = reverse
model.saveSelected(media.id, media.selected!!, requireActivity()) model.saveSelected(media.id, media.selected!!)
reload() reload()
} }
@@ -273,23 +322,14 @@ class AnimeWatchFragment : Fragment() {
media.selected!!.chip = i media.selected!!.chip = i
start = s start = s
end = e end = e
model.saveSelected(media.id, media.selected!!, requireActivity()) model.saveSelected(media.id, media.selected!!)
reload() reload()
} }
var subscribed = false var subscribed = false
fun onNotificationPressed(subscribed: Boolean, source: String) { fun onNotificationPressed(subscribed: Boolean, source: String) {
this.subscribed = subscribed this.subscribed = subscribed
saveSubscription(requireContext(), media, subscribed) saveSubscription(media, subscribed)
if (!subscribed)
Notifications.deleteChannel(requireContext(), getChannelId(true, media.id))
else
Notifications.createChannel(
requireContext(),
ANIME_GROUP,
getChannelId(true, media.id),
media.userPreferredName
)
snackString( snackString(
if (subscribed) getString(R.string.subscribed_notification, source) if (subscribed) getString(R.string.subscribed_notification, source)
else getString(R.string.unsubscribed_notification) else getString(R.string.unsubscribed_notification)
@@ -305,28 +345,25 @@ class AnimeWatchFragment : Fragment() {
activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
try {
activity.findViewById<CustomBottomNavBar>(R.id.mediaTab).visibility = visibility activity.tabLayout.setVisibility(visibility)
} catch (e: ClassCastException) {
activity.findViewById<NavigationRailView>(R.id.mediaTab).visibility = visibility
}
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility = activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
if (show) View.GONE else View.VISIBLE if (show) View.GONE else View.VISIBLE
} }
} }
var itemSelected = false
val allSettings = pkg.sources.filterIsInstance<ConfigurableAnimeSource>() val allSettings = pkg.sources.filterIsInstance<ConfigurableAnimeSource>()
if (allSettings.isNotEmpty()) { if (allSettings.isNotEmpty()) {
var selectedSetting = allSettings[0] var selectedSetting = allSettings[0]
if (allSettings.size > 1) { if (allSettings.size > 1) {
val names = allSettings.map { it.lang }.toTypedArray() val names =
var selectedIndex = 0 allSettings.map { LanguageMapper.mapLanguageCodeToName(it.lang) }.toTypedArray()
val dialog = AlertDialog.Builder(requireContext()) val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
.setTitle("Select a Source") .setTitle("Select a Source")
.setSingleChoiceItems(names, selectedIndex) { _, which -> .setSingleChoiceItems(names, -1) { dialog, which ->
selectedIndex = which selectedSetting = allSettings[which]
} itemSelected = true
.setPositiveButton("OK") { dialog, _ ->
selectedSetting = allSettings[selectedIndex]
dialog.dismiss() dialog.dismiss()
// Move the fragment transaction here // Move the fragment transaction here
@@ -343,10 +380,10 @@ class AnimeWatchFragment : Fragment() {
.commit() .commit()
} }
} }
.setNegativeButton("Cancel") { dialog, _ -> .setOnDismissListener {
dialog.cancel() if (!itemSelected) {
changeUIVisibility(true) changeUIVisibility(true)
return@setNegativeButton }
} }
.show() .show()
dialog.window?.setDimAmount(0.8f) dialog.window?.setDimAmount(0.8f)
@@ -375,10 +412,91 @@ class AnimeWatchFragment : Fragment() {
fun onEpisodeClick(i: String) { fun onEpisodeClick(i: String) {
model.continueMedia = false model.continueMedia = false
model.saveSelected(media.id, media.selected!!, requireActivity()) model.saveSelected(media.id, media.selected!!)
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager) model.onEpisodeClick(media, i, requireActivity().supportFragmentManager)
} }
fun onAnimeEpisodeDownloadClick(i: String) {
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
}
fun onAnimeEpisodeStopDownloadClick(i: String) {
val cancelIntent = Intent().apply {
action = AnimeDownloaderService.ACTION_CANCEL_DOWNLOAD
putExtra(
AnimeDownloaderService.EXTRA_TASK_NAME,
AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
)
}
requireContext().sendBroadcast(cancelIntent)
// Remove the download from the manager and update the UI
downloadManager.removeDownload(
DownloadedType(
media.mainName(),
i,
DownloadedType.Type.ANIME
)
)
episodeAdapter.purgeDownload(i)
}
@OptIn(UnstableApi::class)
fun onAnimeEpisodeRemoveDownloadClick(i: String) {
downloadManager.removeDownload(
DownloadedType(
media.mainName(),
i,
DownloadedType.Type.ANIME
)
)
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
val id = PrefManager.getAnimeDownloadPreferences().getString(
taskName,
""
) ?: ""
PrefManager.getAnimeDownloadPreferences().edit().remove(taskName).apply()
DownloadService.sendRemoveDownload(
requireContext(),
ExoplayerDownloadService::class.java,
id,
true
)
episodeAdapter.deleteDownload(i)
}
private val downloadStatusReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (!this@AnimeWatchFragment::episodeAdapter.isInitialized) return
when (intent.action) {
ACTION_DOWNLOAD_STARTED -> {
val chapterNumber = intent.getStringExtra(EXTRA_EPISODE_NUMBER)
chapterNumber?.let { episodeAdapter.startDownload(it) }
}
ACTION_DOWNLOAD_FINISHED -> {
val chapterNumber = intent.getStringExtra(EXTRA_EPISODE_NUMBER)
chapterNumber?.let { episodeAdapter.stopDownload(it) }
}
ACTION_DOWNLOAD_FAILED -> {
val chapterNumber = intent.getStringExtra(EXTRA_EPISODE_NUMBER)
chapterNumber?.let {
episodeAdapter.purgeDownload(it)
}
}
ACTION_DOWNLOAD_PROGRESS -> {
val chapterNumber = intent.getStringExtra(EXTRA_EPISODE_NUMBER)
val progress = intent.getIntExtra("progress", 0)
chapterNumber?.let {
episodeAdapter.updateDownloadProgress(it, progress)
}
}
}
}
}
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
private fun reload() { private fun reload() {
val selected = model.loadSelected(media) val selected = model.loadSelected(media)
@@ -389,8 +507,10 @@ class AnimeWatchFragment : Fragment() {
selected.latest = selected.latest =
media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest
model.saveSelected(media.id, selected, requireActivity()) model.saveSelected(media.id, selected)
headerAdapter.handleEpisodes() headerAdapter.handleEpisodes()
val isDownloaded = model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)
episodeAdapter.offlineMode = isDownloaded
episodeAdapter.notifyItemRangeRemoved(0, episodeAdapter.arr.size) episodeAdapter.notifyItemRangeRemoved(0, episodeAdapter.arr.size)
var arr: ArrayList<Episode> = arrayListOf() var arr: ArrayList<Episode> = arrayListOf()
if (media.anime!!.episodes != null) { if (media.anime!!.episodes != null) {
@@ -403,13 +523,22 @@ class AnimeWatchFragment : Fragment() {
arr = (arr.reversed() as? ArrayList<Episode>) ?: arr arr = (arr.reversed() as? ArrayList<Episode>) ?: arr
} }
episodeAdapter.arr = arr episodeAdapter.arr = arr
episodeAdapter.updateType(style ?: uiSettings.animeDefaultView) episodeAdapter.updateType(style ?: PrefManager.getVal(PrefName.AnimeDefaultView))
episodeAdapter.notifyItemRangeInserted(0, arr.size) episodeAdapter.notifyItemRangeInserted(0, arr.size)
for (download in downloadManager.animeDownloadedTypes) {
if (download.title == media.mainName()) {
episodeAdapter.stopDownload(download.chapter)
}
}
} }
override fun onDestroy() { override fun onDestroy() {
model.watchSources?.flushText() model.watchSources?.flushText()
super.onDestroy() super.onDestroy()
try {
requireContext().unregisterReceiver(downloadStatusReceiver)
} catch (_: IllegalArgumentException) {
}
} }
var state: Parcelable? = null var state: Parcelable? = null
@@ -417,6 +546,8 @@ class AnimeWatchFragment : Fragment() {
super.onResume() super.onResume()
binding.mediaInfoProgressBar.visibility = progress binding.mediaInfoProgressBar.visibility = progress
binding.animeSourceRecycler.layoutManager?.onRestoreInstanceState(state) binding.animeSourceRecycler.layoutManager?.onRestoreInstanceState(state)
requireActivity().setNavigationTheme()
} }
override fun onPause() { override fun onPause() {
@@ -424,4 +555,12 @@ class AnimeWatchFragment : Fragment() {
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState() state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState()
} }
} companion object {
const val ACTION_DOWNLOAD_STARTED = "ani.dantotsu.ACTION_DOWNLOAD_STARTED"
const val ACTION_DOWNLOAD_FINISHED = "ani.dantotsu.ACTION_DOWNLOAD_FINISHED"
const val ACTION_DOWNLOAD_FAILED = "ani.dantotsu.ACTION_DOWNLOAD_FAILED"
const val ACTION_DOWNLOAD_PROGRESS = "ani.dantotsu.ACTION_DOWNLOAD_PROGRESS"
const val EXTRA_EPISODE_NUMBER = "extra_episode_number"
}
}

View File

@@ -0,0 +1,43 @@
package ani.dantotsu.media.anime
import android.content.Context
import android.os.Bundle
import androidx.mediarouter.app.MediaRouteActionProvider
import androidx.mediarouter.app.MediaRouteChooserDialog
import androidx.mediarouter.app.MediaRouteChooserDialogFragment
import androidx.mediarouter.app.MediaRouteControllerDialog
import androidx.mediarouter.app.MediaRouteControllerDialogFragment
import androidx.mediarouter.app.MediaRouteDialogFactory
import ani.dantotsu.R
class CustomCastProvider(context: Context) : MediaRouteActionProvider(context) {
init {
dialogFactory = CustomCastThemeFactory()
}
}
class CustomCastThemeFactory : MediaRouteDialogFactory() {
override fun onCreateChooserDialogFragment(): MediaRouteChooserDialogFragment {
return CustomMediaRouterChooserDialogFragment()
}
override fun onCreateControllerDialogFragment(): MediaRouteControllerDialogFragment {
return CustomMediaRouteControllerDialogFragment()
}
}
class CustomMediaRouterChooserDialogFragment : MediaRouteChooserDialogFragment() {
override fun onCreateChooserDialog(
context: Context,
savedInstanceState: Bundle?
): MediaRouteChooserDialog =
MediaRouteChooserDialog(context, R.style.MyPopup)
}
class CustomMediaRouteControllerDialogFragment : MediaRouteControllerDialogFragment() {
override fun onCreateControllerDialog(
context: Context,
savedInstanceState: Bundle?
): MediaRouteControllerDialog =
MediaRouteControllerDialog(context, R.style.MyPopup)
}

View File

@@ -14,7 +14,8 @@ data class Episode(
var selectedExtractor: String? = null, var selectedExtractor: String? = null,
var selectedVideo: Int = 0, var selectedVideo: Int = 0,
var selectedSubtitle: Int? = -1, var selectedSubtitle: Int? = -1,
var extractors: MutableList<VideoExtractor>? = null, var downloadProgress: String? = null,
@Transient var extractors: MutableList<VideoExtractor>? = null,
@Transient var extractorCallback: ((VideoExtractor) -> Unit)? = null, @Transient var extractorCallback: ((VideoExtractor) -> Unit)? = null,
var allStreams: Boolean = false, var allStreams: Boolean = false,
var watched: Long? = null, var watched: Long? = null,

View File

@@ -1,23 +1,36 @@
package ani.dantotsu.media.anime package ani.dantotsu.media.anime
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog
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 android.view.animation.LinearInterpolator
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.annotation.OptIn
import androidx.lifecycle.coroutineScope
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.DownloadIndex
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.connections.updateProgress import ani.dantotsu.connections.updateProgress
import ani.dantotsu.databinding.ItemEpisodeCompactBinding import ani.dantotsu.databinding.ItemEpisodeCompactBinding
import ani.dantotsu.databinding.ItemEpisodeGridBinding import ani.dantotsu.databinding.ItemEpisodeGridBinding
import ani.dantotsu.databinding.ItemEpisodeListBinding import ani.dantotsu.databinding.ItemEpisodeListBinding
import ani.dantotsu.download.anime.AnimeDownloaderService
import ani.dantotsu.download.video.Helper
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.settings.saving.PrefManager
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.GlideUrl
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.ln
import kotlin.math.pow
fun handleProgress(cont: LinearLayout, bar: View, empty: View, mediaId: Int, ep: String) { fun handleProgress(cont: LinearLayout, bar: View, empty: View, mediaId: Int, ep: String) {
val curr = loadData<Long>("${mediaId}_${ep}") val curr = PrefManager.getNullableCustomVal("${mediaId}_${ep}", null, Long::class.java)
val max = loadData<Long>("${mediaId}_${ep}_max") val max = PrefManager.getNullableCustomVal("${mediaId}_${ep}_max", null, Long::class.java)
if (curr != null && max != null) { if (curr != null && max != null) {
cont.visibility = View.VISIBLE cont.visibility = View.VISIBLE
val div = curr.toFloat() / max.toFloat() val div = curr.toFloat() / max.toFloat()
@@ -32,13 +45,24 @@ fun handleProgress(cont: LinearLayout, bar: View, empty: View, mediaId: Int, ep:
} }
} }
@OptIn(UnstableApi::class)
class EpisodeAdapter( class EpisodeAdapter(
private var type: Int, private var type: Int,
private val media: Media, private val media: Media,
private val fragment: AnimeWatchFragment, private val fragment: AnimeWatchFragment,
var arr: List<Episode> = arrayListOf() var arr: List<Episode> = arrayListOf(),
var offlineMode: Boolean
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private lateinit var index: DownloadIndex
init {
if (offlineMode) {
index = Helper.downloadManager(fragment.requireContext()).downloadIndex
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return (when (viewType) { return (when (viewType) {
0 -> EpisodeListViewHolder( 0 -> EpisodeListViewHolder(
@@ -76,24 +100,23 @@ class EpisodeAdapter(
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val ep = arr[position] val ep = arr[position]
val title = val title = if (!ep.title.isNullOrEmpty() && ep.title != "null") {
"${ ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) }
if (!ep.title.isNullOrEmpty() && ep.title != "null") "" else currContext()!!.getString( } else {
R.string.episode_singular ep.number
) } ?: ""
} ${if (!ep.title.isNullOrEmpty() && ep.title != "null") ep.title else ep.number}"
when (holder) { when (holder) {
is EpisodeListViewHolder -> { is EpisodeListViewHolder -> {
val binding = holder.binding val binding = holder.binding
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings) setAnimation(fragment.requireContext(), holder.binding.root)
val thumb = val thumb =
ep.thumb?.let { if (it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null } ep.thumb?.let { if (it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null }
Glide.with(binding.itemEpisodeImage).load(thumb ?: media.cover).override(400, 0) Glide.with(binding.itemEpisodeImage).load(thumb ?: media.cover).override(400, 0)
.into(binding.itemEpisodeImage) .into(binding.itemEpisodeImage)
binding.itemEpisodeNumber.text = ep.number binding.itemEpisodeNumber.text = ep.number
binding.itemEpisodeTitle.text = title binding.itemEpisodeTitle.text = if (ep.number == title) "Episode $title" else title
if (ep.filler) { if (ep.filler) {
binding.itemEpisodeFiller.visibility = View.VISIBLE binding.itemEpisodeFiller.visibility = View.VISIBLE
@@ -105,6 +128,7 @@ class EpisodeAdapter(
binding.itemEpisodeDesc.visibility = binding.itemEpisodeDesc.visibility =
if (ep.desc != null && ep.desc?.trim(' ') != "") View.VISIBLE else View.GONE if (ep.desc != null && ep.desc?.trim(' ') != "") View.VISIBLE else View.GONE
binding.itemEpisodeDesc.text = ep.desc ?: "" binding.itemEpisodeDesc.text = ep.desc ?: ""
holder.bind(ep.number, ep.downloadProgress, ep.desc)
if (media.userProgress != null) { if (media.userProgress != null) {
if ((ep.number.toFloatOrNull() ?: 9999f) <= media.userProgress!!.toFloat()) { if ((ep.number.toFloatOrNull() ?: 9999f) <= media.userProgress!!.toFloat()) {
@@ -134,7 +158,7 @@ class EpisodeAdapter(
is EpisodeGridViewHolder -> { is EpisodeGridViewHolder -> {
val binding = holder.binding val binding = holder.binding
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings) setAnimation(fragment.requireContext(), holder.binding.root)
val thumb = val thumb =
ep.thumb?.let { if (it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null } ep.thumb?.let { if (it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null }
@@ -177,7 +201,7 @@ class EpisodeAdapter(
is EpisodeCompactViewHolder -> { is EpisodeCompactViewHolder -> {
val binding = holder.binding val binding = holder.binding
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings) setAnimation(fragment.requireContext(), holder.binding.root)
binding.itemEpisodeNumber.text = ep.number binding.itemEpisodeNumber.text = ep.number
binding.itemEpisodeFillerView.visibility = binding.itemEpisodeFillerView.visibility =
if (ep.filler) View.VISIBLE else View.GONE if (ep.filler) View.VISIBLE else View.GONE
@@ -205,6 +229,77 @@ class EpisodeAdapter(
override fun getItemCount(): Int = arr.size override fun getItemCount(): Int = arr.size
private val activeDownloads = mutableSetOf<String>()
private val downloadedEpisodes = mutableSetOf<String>()
fun startDownload(episodeNumber: String) {
activeDownloads.add(episodeNumber)
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == episodeNumber }
if (position != -1) {
notifyItemChanged(position)
}
}
@OptIn(UnstableApi::class)
fun stopDownload(episodeNumber: String) {
activeDownloads.remove(episodeNumber)
downloadedEpisodes.add(episodeNumber)
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == episodeNumber }
if (position != -1) {
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(
media.mainName(),
episodeNumber
)
val id = PrefManager.getAnimeDownloadPreferences().getString(
taskName,
""
) ?: ""
val size = try {
val download = index.getDownload(id)
bytesToHuman(download?.bytesDownloaded ?: 0)
} catch (e: Exception) {
null
}
arr[position].downloadProgress = "Downloaded" + if (size != null) ": ($size)" else ""
notifyItemChanged(position)
}
}
fun deleteDownload(episodeNumber: String) {
downloadedEpisodes.remove(episodeNumber)
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == episodeNumber }
if (position != -1) {
arr[position].downloadProgress = null
notifyItemChanged(position)
}
}
fun purgeDownload(episodeNumber: String) {
activeDownloads.remove(episodeNumber)
downloadedEpisodes.remove(episodeNumber)
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == episodeNumber }
if (position != -1) {
arr[position].downloadProgress = "Failed"
notifyItemChanged(position)
}
}
fun updateDownloadProgress(episodeNumber: String, progress: Int) {
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == episodeNumber }
if (position != -1) {
arr[position].downloadProgress = "Downloading: $progress%"
notifyItemChanged(position)
}
}
inner class EpisodeCompactViewHolder(val binding: ItemEpisodeCompactBinding) : inner class EpisodeCompactViewHolder(val binding: ItemEpisodeCompactBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
init { init {
@@ -227,11 +322,36 @@ class EpisodeAdapter(
inner class EpisodeListViewHolder(val binding: ItemEpisodeListBinding) : inner class EpisodeListViewHolder(val binding: ItemEpisodeListBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
private val activeCoroutines = mutableSetOf<String>()
init { init {
itemView.setOnClickListener { itemView.setOnClickListener {
if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0) if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0)
fragment.onEpisodeClick(arr[bindingAdapterPosition].number) fragment.onEpisodeClick(arr[bindingAdapterPosition].number)
} }
binding.itemDownload.setOnClickListener {
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size) {
val episodeNumber = arr[bindingAdapterPosition].number
if (activeDownloads.contains(episodeNumber)) {
fragment.onAnimeEpisodeStopDownloadClick(episodeNumber)
return@setOnClickListener
} else if (downloadedEpisodes.contains(episodeNumber)) {
val builder = AlertDialog.Builder(currContext(), R.style.MyPopup)
builder.setTitle("Delete Episode")
builder.setMessage("Are you sure you want to delete Episode ${episodeNumber}?")
builder.setPositiveButton("Yes") { _, _ ->
fragment.onAnimeEpisodeRemoveDownloadClick(episodeNumber)
}
builder.setNegativeButton("No") { _, _ ->
}
val dialog = builder.show()
dialog.window?.setDimAmount(0.8f)
return@setOnClickListener
} else {
fragment.onAnimeEpisodeDownloadClick(episodeNumber)
}
}
}
binding.itemEpisodeDesc.setOnClickListener { binding.itemEpisodeDesc.setOnClickListener {
if (binding.itemEpisodeDesc.maxLines == 3) if (binding.itemEpisodeDesc.maxLines == 3)
binding.itemEpisodeDesc.maxLines = 100 binding.itemEpisodeDesc.maxLines = 100
@@ -239,11 +359,79 @@ class EpisodeAdapter(
binding.itemEpisodeDesc.maxLines = 3 binding.itemEpisodeDesc.maxLines = 3
} }
} }
fun bind(episodeNumber: String, progress: String?, desc: String?) {
if (progress != null) {
binding.itemEpisodeDesc.visibility = View.GONE
binding.itemDownloadStatus.visibility = View.VISIBLE
binding.itemDownloadStatus.text = progress
} else {
binding.itemDownloadStatus.visibility = View.GONE
binding.itemDownloadStatus.text = ""
}
if (activeDownloads.contains(episodeNumber)) {
// Show spinner
binding.itemDownload.setImageResource(R.drawable.ic_sync)
startOrContinueRotation(episodeNumber) {
binding.itemDownload.rotation = 0f
}
binding.itemEpisodeDesc.visibility = View.GONE
} else if (downloadedEpisodes.contains(episodeNumber)) {
binding.itemEpisodeDesc.visibility = View.GONE
binding.itemDownloadStatus.visibility = View.VISIBLE
// Show checkmark
binding.itemDownload.setImageResource(R.drawable.ic_circle_check)
binding.itemDownload.postDelayed({
binding.itemDownload.setImageResource(R.drawable.ic_round_delete_24)
binding.itemDownload.rotation = 0f
}, 1000)
} else {
binding.itemDownloadStatus.visibility = View.GONE
binding.itemEpisodeDesc.visibility =
if (desc != null && desc.trim(' ') != "") View.VISIBLE else View.GONE
// Show download icon
binding.itemDownload.setImageResource(R.drawable.ic_download_24)
binding.itemDownload.rotation = 0f
}
}
private fun startOrContinueRotation(episodeNumber: String, resetRotation: () -> Unit) {
if (!isRotationCoroutineRunningFor(episodeNumber)) {
val scope = fragment.lifecycle.coroutineScope
scope.launch {
// Add chapter number to active coroutines set
activeCoroutines.add(episodeNumber)
while (activeDownloads.contains(episodeNumber)) {
binding.itemDownload.animate().rotationBy(360f).setDuration(1000)
.setInterpolator(
LinearInterpolator()
).start()
delay(1000)
}
// Remove chapter number from active coroutines set
activeCoroutines.remove(episodeNumber)
resetRotation()
}
}
}
private fun isRotationCoroutineRunningFor(episodeNumber: String): Boolean {
return episodeNumber in activeCoroutines
}
} }
fun updateType(t: Int) { fun updateType(t: Int) {
type = t type = t
} }
private fun bytesToHuman(bytes: Long): String? {
if (bytes < 0) return null
val unit = 1000
if (bytes < unit) return "$bytes B"
val exp = (Math.log(bytes.toDouble()) / ln(unit.toDouble())).toInt()
val pre = ("KMGTPE")[exp - 1]
return String.format("%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre)
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,14 @@
package ani.dantotsu.media.anime package ani.dantotsu.media.anime
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.app.AlertDialog
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.TypedValue import android.util.TypedValue
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@@ -17,20 +18,28 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.databinding.BottomSheetSelectorBinding import ani.dantotsu.databinding.BottomSheetSelectorBinding
import ani.dantotsu.databinding.ItemStreamBinding import ani.dantotsu.databinding.ItemStreamBinding
import ani.dantotsu.databinding.ItemUrlBinding import ani.dantotsu.databinding.ItemUrlBinding
import ani.dantotsu.download.video.Helper
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.others.Download.download import ani.dantotsu.others.Download.download
import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.VideoExtractor import ani.dantotsu.parsers.VideoExtractor
import ani.dantotsu.parsers.VideoType import ani.dantotsu.parsers.VideoType
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.DecimalFormat import java.text.DecimalFormat
class SelectorDialogFragment : BottomSheetDialogFragment() { class SelectorDialogFragment : BottomSheetDialogFragment() {
private var _binding: BottomSheetSelectorBinding? = null private var _binding: BottomSheetSelectorBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
@@ -42,6 +51,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
private var makeDefault = false private var makeDefault = false
private var selected: String? = null private var selected: String? = null
private var launch: Boolean? = null private var launch: Boolean? = null
private var isDownloadMenu: Boolean? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -49,6 +59,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
selected = it.getString("server") selected = it.getString("server")
launch = it.getBoolean("launch", true) launch = it.getBoolean("launch", true)
prevEpisode = it.getString("prev") prevEpisode = it.getString("prev")
isDownloadMenu = it.getBoolean("isDownload")
} }
} }
@@ -76,14 +87,17 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
val ep = media?.anime?.episodes?.get(media?.anime?.selectedEpisode) val ep = media?.anime?.episodes?.get(media?.anime?.selectedEpisode)
episode = ep episode = ep
if (ep != null) { if (ep != null) {
if (isDownloadMenu == true) {
binding.selectorMakeDefault.visibility = View.GONE
}
if (selected != null) { if (selected != null && isDownloadMenu == false) {
binding.selectorListContainer.visibility = View.GONE binding.selectorListContainer.visibility = View.GONE
binding.selectorAutoListContainer.visibility = View.VISIBLE binding.selectorAutoListContainer.visibility = View.VISIBLE
binding.selectorAutoText.text = selected binding.selectorAutoText.text = selected
binding.selectorCancel.setOnClickListener { binding.selectorCancel.setOnClickListener {
media!!.selected!!.server = null media!!.selected!!.server = null
model.saveSelected(media!!.id, media!!.selected!!, requireActivity()) model.saveSelected(media!!.id, media!!.selected!!)
tryWith { tryWith {
dismiss() dismiss()
} }
@@ -95,7 +109,12 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
fun load() { fun load() {
val size = val size =
ep.extractors?.find { it.server.name == selected }?.videos?.size if (model.watchSources!!.isDownloadedSource(media!!.selected!!.sourceIndex)) {
ep.extractors?.firstOrNull()?.videos?.size
} else {
ep.extractors?.find { it.server.name == selected }?.videos?.size
}
if (size != null && size >= media!!.selected!!.video) { if (size != null && size >= media!!.selected!!.video) {
media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedExtractor = media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedExtractor =
selected selected
@@ -127,11 +146,11 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
} }
binding.selectorRecyclerView.adapter = null binding.selectorRecyclerView.adapter = null
binding.selectorProgressBar.visibility = View.VISIBLE binding.selectorProgressBar.visibility = View.VISIBLE
makeDefault = loadData("make_default") ?: true makeDefault = PrefManager.getVal(PrefName.MakeDefault)
binding.selectorMakeDefault.isChecked = makeDefault binding.selectorMakeDefault.isChecked = makeDefault
binding.selectorMakeDefault.setOnClickListener { binding.selectorMakeDefault.setOnClickListener {
makeDefault = binding.selectorMakeDefault.isChecked makeDefault = binding.selectorMakeDefault.isChecked
saveData("make_default", makeDefault) PrefManager.setVal(PrefName.MakeDefault, makeDefault)
} }
binding.selectorRecyclerView.layoutManager = binding.selectorRecyclerView.layoutManager =
LinearLayoutManager( LinearLayoutManager(
@@ -145,6 +164,9 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
ep.extractorCallback = { ep.extractorCallback = {
scope.launch { scope.launch {
adapter.add(it) adapter.add(it)
if (model.watchSources!!.isDownloadedSource(media?.selected!!.sourceIndex)) {
adapter.performClick(0)
}
} }
} }
model.getEpisode().observe(this) { model.getEpisode().observe(this) {
@@ -159,11 +181,26 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
model.loadEpisodeVideos(ep, media!!.selected!!.sourceIndex) model.loadEpisodeVideos(ep, media!!.selected!!.sourceIndex)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
binding.selectorProgressBar.visibility = View.GONE binding.selectorProgressBar.visibility = View.GONE
if (adapter.itemCount == 0) {
snackString(getString(R.string.stream_selection_empty))
tryWith {
dismiss()
}
}
} }
} }
} else { } else {
media!!.anime?.episodes?.set(media!!.anime?.selectedEpisode!!, ep) media!!.anime?.episodes?.set(media!!.anime?.selectedEpisode!!, ep)
adapter.addAll(ep.extractors) adapter.addAll(ep.extractors)
if (ep.extractors?.size == 0) {
snackString(getString(R.string.stream_selection_empty))
tryWith {
dismiss()
}
}
if (model.watchSources!!.isDownloadedSource(media?.selected!!.sourceIndex)) {
adapter.performClick(0)
}
binding.selectorProgressBar.visibility = View.GONE binding.selectorProgressBar.visibility = View.GONE
} }
} }
@@ -179,7 +216,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
prevEpisode = null prevEpisode = null
dismiss() dismiss()
if (launch!!) { if (launch!! || model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)) {
stopAddingToList() stopAddingToList()
val intent = Intent(activity, ExoplayerView::class.java) val intent = Intent(activity, ExoplayerView::class.java)
ExoplayerView.media = media ExoplayerView.media = media
@@ -214,7 +251,8 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) { override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
val extractor = links[position] val extractor = links[position]
holder.binding.streamName.text = extractor.server.name holder.binding.streamName.text = ""//extractor.server.name
holder.binding.streamName.visibility = View.GONE
holder.binding.streamRecyclerView.layoutManager = LinearLayoutManager(requireContext()) holder.binding.streamRecyclerView.layoutManager = LinearLayoutManager(requireContext())
holder.binding.streamRecyclerView.adapter = VideoAdapter(extractor) holder.binding.streamRecyclerView.adapter = VideoAdapter(extractor)
@@ -235,6 +273,18 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
notifyItemRangeInserted(0, extractors.size) notifyItemRangeInserted(0, extractors.size)
} }
fun performClick(position: Int) {
try { //bandaid fix for crash
val extractor = links[position]
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedExtractor =
extractor.server.name
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedVideo = 0
startExoplayer(media!!)
} catch (e: Exception) {
Injekt.get<CrashlyticsInterface>().logException(e)
}
}
private inner class StreamViewHolder(val binding: ItemStreamBinding) : private inner class StreamViewHolder(val binding: ItemStreamBinding) :
RecyclerView.ViewHolder(binding.root) RecyclerView.ViewHolder(binding.root)
} }
@@ -256,22 +306,98 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
override fun onBindViewHolder(holder: UrlViewHolder, position: Int) { override fun onBindViewHolder(holder: UrlViewHolder, position: Int) {
val binding = holder.binding val binding = holder.binding
val video = extractor.videos[position] val video = extractor.videos[position]
binding.urlQuality.text = if (isDownloadMenu == true) {
if (video.quality != null) "${video.quality}p" else "Default Quality" binding.urlDownload.visibility = View.VISIBLE
binding.urlNote.text = video.extraNote ?: "" } else {
binding.urlNote.visibility = if (video.extraNote != null) View.VISIBLE else View.GONE binding.urlDownload.visibility = View.GONE
binding.urlDownload.visibility = View.VISIBLE }
binding.urlDownload.setSafeOnClickListener { binding.urlDownload.setSafeOnClickListener {
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor = media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor =
extractor.server.name extractor.server.name
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedVideo = media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedVideo =
position position
binding.urlDownload.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) if ((PrefManager.getVal(PrefName.DownloadManager) as Int) != 0) {
download( download(
requireActivity(), requireActivity(),
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!, media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!,
media!!.userPreferredName media!!.userPreferredName
) )
} else {
val episode = media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!
val selectedVideo =
if (extractor.videos.size > episode.selectedVideo) extractor.videos[episode.selectedVideo] else null
val subtitles = extractor.subtitles
val subtitleNames = subtitles.map { it.language }
var subtitleToDownload: Subtitle? = null
val activity = currActivity()?:requireActivity()
if (subtitles.isNotEmpty()) {
val alertDialog = AlertDialog.Builder(context, R.style.MyPopup)
.setTitle("Download Subtitle")
.setSingleChoiceItems(
subtitleNames.toTypedArray(),
-1
) { dialog, which ->
subtitleToDownload = subtitles[which]
}
.setPositiveButton("Download") { _, _ ->
dialog?.dismiss()
if (selectedVideo != null) {
Helper.startAnimeDownloadService(
activity,
media!!.mainName(),
episode.number,
selectedVideo,
subtitleToDownload,
media,
episode.thumb?.url ?: media!!.banner ?: media!!.cover
)
broadcastDownloadStarted(episode.number, activity)
} else {
snackString("No Video Selected")
}
}
.setNegativeButton("Skip") { dialog, _ ->
subtitleToDownload = null
if (selectedVideo != null) {
Helper.startAnimeDownloadService(
currActivity()!!,
media!!.mainName(),
episode.number,
selectedVideo,
subtitleToDownload,
media,
episode.thumb?.url ?: media!!.banner ?: media!!.cover
)
broadcastDownloadStarted(episode.number, activity)
} else {
snackString("No Video Selected")
}
dialog.dismiss()
}
.setNeutralButton("Cancel") { dialog, _ ->
subtitleToDownload = null
dialog.dismiss()
}
.show()
alertDialog.window?.setDimAmount(0.8f)
} else {
if (selectedVideo != null) {
Helper.startAnimeDownloadService(
requireActivity(),
media!!.mainName(),
episode.number,
selectedVideo,
subtitleToDownload,
media,
episode.thumb?.url ?: media!!.banner ?: media!!.cover
)
broadcastDownloadStarted(episode.number, activity)
} else {
snackString("No Video Selected")
}
}
}
dismiss() dismiss()
} }
if (video.format == VideoType.CONTAINER) { if (video.format == VideoType.CONTAINER) {
@@ -281,12 +407,17 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
(if (video.extraNote != null) " : " else "") + (if (video.size == 0.0) "Unknown Size" else (DecimalFormat( (if (video.extraNote != null) " : " else "") + (if (video.size == 0.0) "Unknown Size" else (DecimalFormat(
"#.##" "#.##"
).format(video.size ?: 0).toString() + " MB")) ).format(video.size ?: 0).toString() + " MB"))
} else {
binding.urlQuality.text = "Multi Quality"
if ((loadData<Int>("settings_download_manager") ?: 0) == 0) {
binding.urlDownload.visibility = View.GONE
}
} }
binding.urlNote.visibility = View.VISIBLE
binding.urlNote.text = video.format.name
binding.urlQuality.text = extractor.server.name
}
private fun broadcastDownloadStarted(episodeNumber: String, activity: Activity) {
val intent = Intent(AnimeWatchFragment.ACTION_DOWNLOAD_STARTED).apply {
putExtra(AnimeWatchFragment.EXTRA_EPISODE_NUMBER, episodeNumber)
}
activity.sendBroadcast(intent)
} }
override fun getItemCount(): Int = extractor.videos.size override fun getItemCount(): Int = extractor.videos.size
@@ -295,6 +426,10 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
init { init {
itemView.setSafeOnClickListener { itemView.setSafeOnClickListener {
if (isDownloadMenu == true) {
binding.urlDownload.performClick()
return@setSafeOnClickListener
}
tryWith(true) { tryWith(true) {
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedExtractor = media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedExtractor =
extractor.server.name extractor.server.name
@@ -303,7 +438,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
if (makeDefault) { if (makeDefault) {
media!!.selected!!.server = extractor.server.name media!!.selected!!.server = extractor.server.name
media!!.selected!!.video = bindingAdapterPosition media!!.selected!!.video = bindingAdapterPosition
model.saveSelected(media!!.id, media!!.selected!!, requireActivity()) model.saveSelected(media!!.id, media!!.selected!!)
} }
startExoplayer(media!!) startExoplayer(media!!)
} }
@@ -326,13 +461,15 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
fun newInstance( fun newInstance(
server: String? = null, server: String? = null,
la: Boolean = true, la: Boolean = true,
prev: String? = null prev: String? = null,
isDownload: Boolean
): SelectorDialogFragment = ): SelectorDialogFragment =
SelectorDialogFragment().apply { SelectorDialogFragment().apply {
arguments = Bundle().apply { arguments = Bundle().apply {
putString("server", server) putString("server", server)
putBoolean("launch", la) putBoolean("launch", la)
putString("prev", prev) putString("prev", prev)
putBoolean("isDownload", isDownload)
} }
} }
} }

View File

@@ -6,17 +6,18 @@ 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.annotation.OptIn
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.BottomSheetDialogFragment import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.databinding.BottomSheetSubtitlesBinding import ani.dantotsu.databinding.BottomSheetSubtitlesBinding
import ani.dantotsu.databinding.ItemSubtitleTextBinding import ani.dantotsu.databinding.ItemSubtitleTextBinding
import ani.dantotsu.loadData
import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.parsers.Subtitle import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.saveData import ani.dantotsu.settings.saving.PrefManager
class SubtitleDialogFragment : BottomSheetDialogFragment() { class SubtitleDialogFragment : BottomSheetDialogFragment() {
private var _binding: BottomSheetSubtitlesBinding? = null private var _binding: BottomSheetSubtitlesBinding? = null
@@ -60,13 +61,14 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
) )
) )
@OptIn(UnstableApi::class)
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) { override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
val binding = holder.binding val binding = holder.binding
if (position == 0) { if (position == 0) {
binding.subtitleTitle.setText(R.string.none) binding.subtitleTitle.setText(R.string.none)
model.getMedia().observe(viewLifecycleOwner) { media -> model.getMedia().observe(viewLifecycleOwner) { media ->
val mediaID: Int = media.id val mediaID: Int = media.id
val selSubs: String? = loadData("subLang_${mediaID}", activity) val selSubs = PrefManager.getNullableCustomVal("subLang_${mediaID}", null, String::class.java)
if (episode.selectedSubtitle != null && selSubs != "None") { if (episode.selectedSubtitle != null && selSubs != "None") {
binding.root.setCardBackgroundColor(TRANSPARENT) binding.root.setCardBackgroundColor(TRANSPARENT)
} }
@@ -76,7 +78,7 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
model.setEpisode(episode, "Subtitle") model.setEpisode(episode, "Subtitle")
model.getMedia().observe(viewLifecycleOwner) { media -> model.getMedia().observe(viewLifecycleOwner) { media ->
val mediaID: Int = media.id val mediaID: Int = media.id
saveData("subLang_${mediaID}", "None", activity) PrefManager.setCustomVal("subLang_${mediaID}", "None")
} }
dismiss() dismiss()
} }
@@ -105,7 +107,8 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
} }
model.getMedia().observe(viewLifecycleOwner) { media -> model.getMedia().observe(viewLifecycleOwner) { media ->
val mediaID: Int = media.id val mediaID: Int = media.id
val selSubs: String? = loadData("subLang_${mediaID}", activity) val selSubs: String? =
PrefManager.getNullableCustomVal("subLang_${mediaID}", null, String::class.java)
if (episode.selectedSubtitle != position - 1 && selSubs != subtitles[position - 1].language) { if (episode.selectedSubtitle != position - 1 && selSubs != subtitles[position - 1].language) {
binding.root.setCardBackgroundColor(TRANSPARENT) binding.root.setCardBackgroundColor(TRANSPARENT)
} }
@@ -116,7 +119,10 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
model.setEpisode(episode, "Subtitle") model.setEpisode(episode, "Subtitle")
model.getMedia().observe(viewLifecycleOwner) { media -> model.getMedia().observe(viewLifecycleOwner) { media ->
val mediaID: Int = media.id val mediaID: Int = media.id
saveData("subLang_${mediaID}", subtitles[position - 1].language, activity) PrefManager.setCustomVal(
"subLang_${mediaID}",
subtitles[position - 1].language
)
} }
dismiss() dismiss()
} }

View File

@@ -0,0 +1,394 @@
package ani.dantotsu.media.comments
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Color
import android.view.View
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.R
import ani.dantotsu.connections.comments.Comment
import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.copyToClipboard
import ani.dantotsu.databinding.ItemCommentsBinding
import ani.dantotsu.loadImage
import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.setAnimation
import ani.dantotsu.snackString
import ani.dantotsu.util.ColorEditor.Companion.adjustColorForContrast
import ani.dantotsu.util.ColorEditor.Companion.getContrastRatio
import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.Section
import com.xwray.groupie.viewbinding.BindableItem
import io.noties.markwon.Markwon
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Date
import java.util.TimeZone
import kotlin.math.abs
import kotlin.math.sqrt
class CommentItem(val comment: Comment,
private val markwon: Markwon,
val parentSection: Section,
private val commentsFragment: CommentsFragment,
private val backgroundColor: Int,
val commentDepth: Int
) : BindableItem<ItemCommentsBinding>() {
lateinit var binding: ItemCommentsBinding
val adapter = GroupieAdapter()
private var subCommentIds: MutableList<Int> = mutableListOf()
val repliesSection = Section()
private var isEditing = false
var isReplying = false
private var repliesVisible = false
var MAX_DEPTH = 3
init {
adapter.add(repliesSection)
}
@SuppressLint("SetTextI18n")
override fun bind(viewBinding: ItemCommentsBinding, position: Int) {
binding = viewBinding
setAnimation(binding.root.context, binding.root)
viewBinding.commentRepliesList.layoutManager = LinearLayoutManager(commentsFragment.activity)
viewBinding.commentRepliesList.adapter = adapter
val isUserComment = CommentsAPI.userId == comment.userId
val levelColor = getAvatarColor(comment.totalVotes, backgroundColor)
markwon.setMarkdown(viewBinding.commentText, comment.content)
viewBinding.commentDelete.visibility = if (isUserComment || CommentsAPI.isAdmin || CommentsAPI.isMod) View.VISIBLE else View.GONE
viewBinding.commentBanUser.visibility = if ((CommentsAPI.isAdmin || CommentsAPI.isMod) && !isUserComment) View.VISIBLE else View.GONE
viewBinding.commentReport.visibility = if (!isUserComment) View.VISIBLE else View.GONE
viewBinding.commentEdit.visibility = if (isUserComment) View.VISIBLE else View.GONE
if (comment.tag == null) {
viewBinding.commentUserTagLayout.visibility = View.GONE
} else {
viewBinding.commentUserTagLayout.visibility = View.VISIBLE
viewBinding.commentUserTag.text = comment.tag.toString()
}
replying(isReplying) //sets default text
editing(isEditing)
if ((comment.replyCount ?: 0) > 0) {
viewBinding.commentTotalReplies.visibility = View.VISIBLE
viewBinding.commentRepliesDivider.visibility = View.VISIBLE
viewBinding.commentTotalReplies.text = if(repliesVisible) "Hide Replies" else
"View ${comment.replyCount} repl${if (comment.replyCount == 1) "y" else "ies"}"
} else {
viewBinding.commentTotalReplies.visibility = View.GONE
viewBinding.commentRepliesDivider.visibility = View.GONE
}
viewBinding.commentReply.visibility = View.VISIBLE
viewBinding.commentTotalReplies.setOnClickListener {
if (repliesVisible) {
repliesSection.clear()
removeSubCommentIds()
viewBinding.commentTotalReplies.text = "View ${comment.replyCount} repl${if (comment.replyCount == 1) "y" else "ies"}"
repliesVisible = false
} else {
viewBinding.commentTotalReplies.text = "Hide Replies"
repliesSection.clear()
commentsFragment.viewReplyCallback(this)
repliesVisible = true
}
}
viewBinding.commentUserName.setOnClickListener {
ContextCompat.startActivity(
commentsFragment.activity, Intent(commentsFragment.activity, ProfileActivity::class.java)
.putExtra("userId", comment.userId.toInt())
.putExtra("userLVL","[${levelColor.second}]"), null
)
}
viewBinding.commentUserAvatar.setOnClickListener {
ContextCompat.startActivity(
commentsFragment.activity, Intent(commentsFragment.activity, ProfileActivity::class.java)
.putExtra("userId", comment.userId.toInt())
.putExtra("userLVL","[${levelColor.second}]"), null
)
}
viewBinding.commentText.setOnLongClickListener {
copyToClipboard(comment.content)
true
}
viewBinding.commentEdit.setOnClickListener {
editing(!isEditing)
commentsFragment.editCallback(this)
}
viewBinding.commentReply.setOnClickListener {
replying(!isReplying)
commentsFragment.replyTo(this, comment.username)
commentsFragment.replyCallback(this)
}
viewBinding.modBadge.visibility = if (comment.isMod == true) View.VISIBLE else View.GONE
viewBinding.adminBadge.visibility = if (comment.isAdmin == true) View.VISIBLE else View.GONE
viewBinding.commentDelete.setOnClickListener {
dialogBuilder("Delete Comment", "Are you sure you want to delete this comment?") {
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
scope.launch {
val success = CommentsAPI.deleteComment(comment.commentId)
if (success) {
snackString("Comment Deleted")
parentSection.remove(this@CommentItem)
}
}
}
}
viewBinding.commentBanUser.setOnClickListener {
dialogBuilder("Ban User", "Are you sure you want to ban this user?") {
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
scope.launch {
val success = CommentsAPI.banUser(comment.userId)
if (success) {
snackString("User Banned")
}
}
}
}
viewBinding.commentReport.setOnClickListener {
dialogBuilder("Report Comment", "Only report comments that violate the rules. Are you sure you want to report this comment?") {
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
scope.launch {
val success = CommentsAPI.reportComment(comment.commentId, comment.username, commentsFragment.mediaName, comment.userId)
if (success) {
snackString("Comment Reported")
}
}
}
}
//fill the icon if the user has liked the comment
setVoteButtons(viewBinding)
viewBinding.commentUpVote.setOnClickListener {
val voteType = if (comment.userVoteType == 1) 0 else 1
val previousVoteType = comment.userVoteType
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
scope.launch {
val success = CommentsAPI.vote(comment.commentId, voteType)
if (success) {
comment.userVoteType = voteType
if (previousVoteType == -1) {
comment.downvotes -= 1
}
comment.upvotes += if (voteType == 1) 1 else -1
notifyChanged()
}
}
}
viewBinding.commentDownVote.setOnClickListener {
val voteType = if (comment.userVoteType == -1) 0 else -1
val previousVoteType = comment.userVoteType
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
scope.launch {
val success = CommentsAPI.vote(comment.commentId, voteType)
if (success) {
comment.userVoteType = voteType
if (previousVoteType == 1) {
comment.upvotes -= 1
}
comment.downvotes += if (voteType == -1) 1 else -1
notifyChanged()
}
}
}
viewBinding.commentTotalVotes.text = (comment.upvotes - comment.downvotes).toString()
viewBinding.commentUserAvatar.setOnLongClickListener {
ImageViewDialog.newInstance(
commentsFragment.activity,
"${comment.username}'s [Cover]",
comment.profilePictureUrl
)
}
comment.profilePictureUrl?.let { viewBinding.commentUserAvatar.loadImage(it) }
viewBinding.commentUserName.text = comment.username
viewBinding.commentUserLevel.text = "[${levelColor.second}]"
viewBinding.commentUserLevel.setTextColor(levelColor.first)
viewBinding.commentUserTime.text = formatTimestamp(comment.timestamp)
}
override fun getLayout(): Int {
return R.layout.item_comments
}
fun containsGif(): Boolean {
return comment.content.contains(".gif")
}
override fun initializeViewBinding(view: View): ItemCommentsBinding {
return ItemCommentsBinding.bind(view)
}
fun replying(isReplying: Boolean) {
binding.commentReply.text = if (isReplying) commentsFragment.activity.getString(R.string.cancel) else "Reply"
this.isReplying = isReplying
}
fun editing(isEditing: Boolean) {
binding.commentEdit.text = if (isEditing) commentsFragment.activity.getString(R.string.cancel) else commentsFragment.activity.getString(R.string.edit)
this.isEditing = isEditing
}
fun registerSubComment(id: Int) {
subCommentIds.add(id)
}
private fun removeSubCommentIds(){
subCommentIds.forEach { id ->
val parentComments = parentSection.groups as? List<CommentItem> ?: emptyList()
val commentToRemove = parentComments.find { it.comment.commentId == id }
commentToRemove?.let {
it.removeSubCommentIds()
parentSection.remove(it)
}
}
subCommentIds.clear()
}
private fun setVoteButtons(viewBinding: ItemCommentsBinding) {
when (comment.userVoteType) {
1 -> {
viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_active_24)
viewBinding.commentUpVote.alpha = 1f
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
}
-1 -> {
viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_active_24)
viewBinding.commentDownVote.alpha = 1f
}
else -> {
viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
}
}
}
private fun formatTimestamp(timestamp: String): String {
return try {
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
val parsedDate = dateFormat.parse(timestamp)
val currentDate = Date()
val diff = currentDate.time - (parsedDate?.time ?: 0)
val days = diff / (24 * 60 * 60 * 1000)
val hours = diff / (60 * 60 * 1000) % 24
val minutes = diff / (60 * 1000) % 60
return when {
days > 0 -> "${days}d"
hours > 0 -> "${hours}h"
minutes > 0 -> "${minutes}m"
else -> "now"
}
} catch (e: Exception) {
"now"
}
}
companion object {
fun timestampToMillis(timestamp: String): Long {
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
val parsedDate = dateFormat.parse(timestamp)
return parsedDate?.time ?: 0
}
}
private fun getAvatarColor(voteCount: Int, backgroundColor: Int): Pair<Int, Int> {
val level = if (voteCount < 0) 0 else sqrt(abs(voteCount.toDouble()) / 0.8).toInt()
val colorString = if (level > usernameColors.size - 1) usernameColors[usernameColors.size - 1] else usernameColors[level]
var color = Color.parseColor(colorString)
val ratio = getContrastRatio(color, backgroundColor)
if (ratio < 4.5) {
color = adjustColorForContrast(color, backgroundColor)
}
return Pair(color, level)
}
/**
* Builds the dialog for yes/no confirmation
* no doesn't do anything, yes calls the callback
* @param title the title of the dialog
* @param message the message of the dialog
* @param callback the callback to call when the user clicks yes
*/
private fun dialogBuilder(title: String, message: String, callback: () -> Unit) {
val alertDialog = android.app.AlertDialog.Builder(commentsFragment.activity, R.style.MyPopup)
.setTitle(title)
.setMessage(message)
.setPositiveButton("Yes") { dialog, _ ->
callback()
dialog.dismiss()
}
.setNegativeButton("No") { dialog, _ ->
dialog.dismiss()
}
val dialog = alertDialog.show()
dialog?.window?.setDimAmount(0.8f)
}
private val usernameColors: Array<String> = arrayOf(
"#9932cc",
"#a020f0",
"#8b008b",
"#7b68ee",
"#da70d6",
"#dda0dd",
"#ffe4b5",
"#f0e68c",
"#ffb6c1",
"#fa8072",
"#b03060",
"#ff1493",
"#ff00ff",
"#ff69b4",
"#dc143c",
"#8b0000",
"#ff0000",
"#a0522d",
"#f4a460",
"#b8860b",
"#ffa500",
"#d2691e",
"#ff6347",
"#808000",
"#ffd700",
"#ffff54",
"#8fbc8f",
"#3cb371",
"#008000",
"#00fa9a",
"#98fb98",
"#00ff00",
"#adff2f",
"#32cd32",
"#556b2f",
"#9acd32",
"#7fffd4",
"#2f4f4f",
"#5f9ea0",
"#87ceeb",
"#00bfff",
"#00ffff",
"#1e90ff",
"#4682b4",
"#0000ff",
"#0000cd",
"#00008b",
"#191970",
"#ffffff",
)
}

View File

@@ -0,0 +1,707 @@
package ani.dantotsu.media.comments
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context.INPUT_METHOD_SERVICE
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import androidx.appcompat.widget.PopupMenu
import androidx.core.animation.doOnEnd
import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.R
import ani.dantotsu.buildMarkwon
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.comments.Comment
import ani.dantotsu.connections.comments.CommentResponse
import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.databinding.FragmentCommentsBinding
import ani.dantotsu.loadImage
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.toast
import ani.dantotsu.util.Logger
import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.Section
import io.noties.markwon.editor.MarkwonEditor
import io.noties.markwon.editor.MarkwonEditorTextWatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.TimeZone
@SuppressLint("ClickableViewAccessibility")
class CommentsFragment : Fragment() {
lateinit var binding: FragmentCommentsBinding
lateinit var activity: MediaDetailsActivity
private var interactionState = InteractionState.NONE
private var commentWithInteraction: CommentItem? = null
private val section = Section()
private val adapter = GroupieAdapter()
private var tag: Int? = null
private var filterTag: Int? = null
private var mediaId: Int = -1
var mediaName: String = ""
private var backgroundColor: Int = 0
var pagesLoaded = 1
var totalPages = 1
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentCommentsBinding.inflate(inflater, container, false)
binding.commentsLayout.isNestedScrollingEnabled = true
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
activity = requireActivity() as MediaDetailsActivity
//get the media id from the intent
val mediaId = arguments?.getInt("mediaId") ?: -1
mediaName = arguments?.getString("mediaName") ?: "unknown"
if (mediaId == -1) {
snackString("Invalid Media ID")
return
}
this.mediaId = mediaId
backgroundColor = (binding.root.background as? ColorDrawable)?.color ?: 0
val markwon = buildMarkwon(activity, fragment = this@CommentsFragment)
activity.binding.commentUserAvatar.loadImage(Anilist.avatar)
val markwonEditor = MarkwonEditor.create(markwon)
activity.binding.commentInput.addTextChangedListener(
MarkwonEditorTextWatcher.withProcess(
markwonEditor
)
)
binding.commentsRefresh.setOnRefreshListener {
lifecycleScope.launch {
loadAndDisplayComments()
binding.commentsRefresh.isRefreshing = false
}
activity.binding.commentReplyToContainer.visibility = View.GONE
}
binding.commentsList.adapter = adapter
binding.commentsList.layoutManager = LinearLayoutManager(activity)
if (CommentsAPI.authToken != null) {
lifecycleScope.launch {
val commentId = arguments?.getInt("commentId")
if (commentId != null && commentId > 0) {
loadSingleComment(commentId)
} else {
loadAndDisplayComments()
}
}
} else {
toast("Not logged in")
activity.binding.commentMessageContainer.visibility = View.GONE
}
binding.commentSort.setOnClickListener { sortView ->
fun sortComments(sortOrder: String) {
val groups = section.groups
when (sortOrder) {
"newest" -> groups.sortByDescending { CommentItem.timestampToMillis((it as CommentItem).comment.timestamp) }
"oldest" -> groups.sortBy { CommentItem.timestampToMillis((it as CommentItem).comment.timestamp) }
"highest_rated" -> groups.sortByDescending { (it as CommentItem).comment.upvotes - it.comment.downvotes }
"lowest_rated" -> groups.sortBy { (it as CommentItem).comment.upvotes - it.comment.downvotes }
}
section.update(groups)
}
val popup = PopupMenu(activity, sortView)
popup.setOnMenuItemClickListener { item ->
val sortOrder = when (item.itemId) {
R.id.comment_sort_newest -> "newest"
R.id.comment_sort_oldest -> "oldest"
R.id.comment_sort_highest_rated -> "highest_rated"
R.id.comment_sort_lowest_rated -> "lowest_rated"
else -> return@setOnMenuItemClickListener false
}
PrefManager.setVal(PrefName.CommentSortOrder, sortOrder)
if (totalPages > pagesLoaded) {
lifecycleScope.launch {
loadAndDisplayComments()
activity.binding.commentReplyToContainer.visibility = View.GONE
}
} else {
sortComments(sortOrder)
}
binding.commentsList.scrollToPosition(0)
true
}
popup.inflate(R.menu.comments_sort_menu)
popup.show()
}
binding.commentFilter.setOnClickListener {
val alertDialog = AlertDialog.Builder(activity, R.style.MyPopup)
.setTitle("Enter a chapter/episode number tag")
.setView(R.layout.dialog_edittext)
.setPositiveButton("OK") { dialog, _ ->
val editText =
(dialog as AlertDialog).findViewById<EditText>(R.id.dialogEditText)
val text = editText?.text.toString()
filterTag = text.toIntOrNull()
lifecycleScope.launch {
loadAndDisplayComments()
}
dialog.dismiss()
}
.setNeutralButton("Clear") { dialog, _ ->
filterTag = null
lifecycleScope.launch {
loadAndDisplayComments()
}
dialog.dismiss()
}
.setNegativeButton("Cancel") { dialog, _ ->
filterTag = null
dialog.dismiss()
}
val dialog = alertDialog.show()
dialog?.window?.setDimAmount(0.8f)
}
var isFetching = false
binding.commentsList.setOnTouchListener(
object : View.OnTouchListener {
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
if (event?.action == MotionEvent.ACTION_UP) {
if (!binding.commentsList.canScrollVertically(1) && !isFetching &&
(binding.commentsList.layoutManager as LinearLayoutManager).findLastVisibleItemPosition() == (binding.commentsList.adapter!!.itemCount - 1)
) {
if (pagesLoaded < totalPages && totalPages > 1) {
binding.commentBottomRefresh.visibility = View.VISIBLE
loadMoreComments()
lifecycleScope.launch {
kotlinx.coroutines.delay(1000)
withContext(Dispatchers.Main) {
binding.commentBottomRefresh.visibility = View.GONE
}
}
} else {
//snackString("No more comments") fix spam?
Logger.log("No more comments")
}
}
}
return false
}
private fun loadMoreComments() {
isFetching = true
lifecycleScope.launch {
val comments = fetchComments()
comments?.comments?.forEach { comment ->
updateUIWithComment(comment)
}
totalPages = comments?.totalPages ?: 1
pagesLoaded++
isFetching = false
}
}
private suspend fun fetchComments(): CommentResponse? {
return withContext(Dispatchers.IO) {
CommentsAPI.getCommentsForId(
mediaId,
pagesLoaded + 1,
filterTag,
PrefManager.getVal(PrefName.CommentSortOrder, "newest")
)
}
}
//adds additional comments to the section
private suspend fun updateUIWithComment(comment: Comment) {
withContext(Dispatchers.Main) {
section.add(
CommentItem(
comment,
buildMarkwon(activity, fragment = this@CommentsFragment),
section,
this@CommentsFragment,
backgroundColor,
0
)
)
}
}
})
activity.binding.commentInput.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
override fun afterTextChanged(s: android.text.Editable?) {
if ((activity.binding.commentInput.text.length) > 300) {
activity.binding.commentInput.text.delete(
300,
activity.binding.commentInput.text.length
)
snackString("Comment cannot be longer than 300 characters")
}
}
})
activity.binding.commentInput.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
val targetWidth = activity.binding.commentInputLayout.width -
activity.binding.commentLabel.width -
activity.binding.commentSend.width -
activity.binding.commentUserAvatar.width - 12 + 16
val anim = ValueAnimator.ofInt(activity.binding.commentInput.width, targetWidth)
anim.addUpdateListener { valueAnimator ->
val layoutParams = activity.binding.commentInput.layoutParams
layoutParams.width = valueAnimator.animatedValue as Int
activity.binding.commentInput.layoutParams = layoutParams
}
anim.duration = 300
anim.start()
anim.doOnEnd {
activity.binding.commentLabel.visibility = View.VISIBLE
activity.binding.commentSend.visibility = View.VISIBLE
activity.binding.commentLabel.animate().translationX(0f).setDuration(300)
.start()
activity.binding.commentSend.animate().translationX(0f).setDuration(300).start()
}
}
activity.binding.commentLabel.setOnClickListener {
//alert dialog to enter a number, with a cancel and ok button
val alertDialog = android.app.AlertDialog.Builder(activity, R.style.MyPopup)
.setTitle("Enter a chapter/episode number tag")
.setView(R.layout.dialog_edittext)
.setPositiveButton("OK") { dialog, _ ->
val editText =
(dialog as AlertDialog).findViewById<EditText>(R.id.dialogEditText)
val text = editText?.text.toString()
tag = text.toIntOrNull()
if (tag == null) {
activity.binding.commentLabel.background = ResourcesCompat.getDrawable(
resources,
R.drawable.ic_label_off_24,
null
)
} else {
activity.binding.commentLabel.background = ResourcesCompat.getDrawable(
resources,
R.drawable.ic_label_24,
null
)
}
dialog.dismiss()
}
.setNeutralButton("Clear") { dialog, _ ->
tag = null
activity.binding.commentLabel.background = ResourcesCompat.getDrawable(
resources,
R.drawable.ic_label_off_24,
null
)
dialog.dismiss()
}
.setNegativeButton("Cancel") { dialog, _ ->
tag = null
activity.binding.commentLabel.background = ResourcesCompat.getDrawable(
resources,
R.drawable.ic_label_off_24,
null
)
dialog.dismiss()
}
val dialog = alertDialog.show()
dialog?.window?.setDimAmount(0.8f)
}
}
activity.binding.commentSend.setOnClickListener {
if (CommentsAPI.isBanned) {
snackString("You are banned from commenting :(")
return@setOnClickListener
}
if (PrefManager.getVal(PrefName.FirstComment)) {
showCommentRulesDialog()
} else {
processComment()
}
}
}
@SuppressLint("NotifyDataSetChanged")
override fun onStart() {
super.onStart()
}
@SuppressLint("NotifyDataSetChanged")
override fun onResume() {
super.onResume()
tag = null
section.groups.forEach {
if (it is CommentItem && it.containsGif()) {
it.notifyChanged()
}
}
}
enum class InteractionState {
NONE, EDIT, REPLY
}
/**
* Loads and displays the comments
* Called when the activity is created
* Or when the user refreshes the comments
*/
private suspend fun loadAndDisplayComments() {
binding.commentsProgressBar.visibility = View.VISIBLE
binding.commentsList.visibility = View.GONE
adapter.clear()
section.clear()
val comments = withContext(Dispatchers.IO) {
CommentsAPI.getCommentsForId(
mediaId,
tag = filterTag,
sort = PrefManager.getVal(PrefName.CommentSortOrder, "newest")
)
}
val sortedComments = sortComments(comments?.comments)
sortedComments.forEach {
withContext(Dispatchers.Main) {
section.add(
CommentItem(
it,
buildMarkwon(activity, fragment = this@CommentsFragment),
section,
this@CommentsFragment,
backgroundColor,
0
)
)
}
}
totalPages = comments?.totalPages ?: 1
binding.commentsProgressBar.visibility = View.GONE
binding.commentsList.visibility = View.VISIBLE
adapter.add(section)
}
private suspend fun loadSingleComment(commentId: Int) {
binding.commentsProgressBar.visibility = View.VISIBLE
binding.commentsList.visibility = View.GONE
adapter.clear()
section.clear()
val comment = withContext(Dispatchers.IO) {
CommentsAPI.getSingleComment(commentId)
}
if (comment != null) {
withContext(Dispatchers.Main) {
section.add(
CommentItem(
comment,
buildMarkwon(activity, fragment = this@CommentsFragment),
section,
this@CommentsFragment,
backgroundColor,
0
)
)
}
}
binding.commentsProgressBar.visibility = View.GONE
binding.commentsList.visibility = View.VISIBLE
adapter.add(section)
}
private fun sortComments(comments: List<Comment>?): List<Comment> {
if (comments == null) return emptyList()
return when (PrefManager.getVal(PrefName.CommentSortOrder, "newest")) {
"newest" -> comments.sortedByDescending { CommentItem.timestampToMillis(it.timestamp) }
"oldest" -> comments.sortedBy { CommentItem.timestampToMillis(it.timestamp) }
"highest_rated" -> comments.sortedByDescending { it.upvotes - it.downvotes }
"lowest_rated" -> comments.sortedBy { it.upvotes - it.downvotes }
else -> comments
}
}
/**
* Resets the old state of the comment input
* @return the old state
*/
private fun resetOldState(): InteractionState {
val oldState = interactionState
interactionState = InteractionState.NONE
return when (oldState) {
InteractionState.EDIT -> {
activity.binding.commentReplyToContainer.visibility = View.GONE
activity.binding.commentInput.setText("")
val imm = activity.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(activity.binding.commentInput.windowToken, 0)
commentWithInteraction?.editing(false)
InteractionState.EDIT
}
InteractionState.REPLY -> {
activity.binding.commentReplyToContainer.visibility = View.GONE
activity.binding.commentInput.setText("")
val imm = activity.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(activity.binding.commentInput.windowToken, 0)
commentWithInteraction?.replying(false)
InteractionState.REPLY
}
else -> {
InteractionState.NONE
}
}
}
/**
* Callback from the comment item to edit the comment
* Called every time the edit button is clicked
* @param comment the comment to edit
*/
fun editCallback(comment: CommentItem) {
if (resetOldState() == InteractionState.EDIT) return
commentWithInteraction = comment
activity.binding.commentInput.setText(comment.comment.content)
activity.binding.commentInput.requestFocus()
activity.binding.commentInput.setSelection(activity.binding.commentInput.text.length)
val imm = activity.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(activity.binding.commentInput, InputMethodManager.SHOW_IMPLICIT)
interactionState = InteractionState.EDIT
}
/**
* Callback from the comment item to reply to the comment
* Called every time the reply button is clicked
* @param comment the comment to reply to
*/
fun replyCallback(comment: CommentItem) {
if (resetOldState() == InteractionState.REPLY) return
commentWithInteraction = comment
activity.binding.commentReplyToContainer.visibility = View.VISIBLE
activity.binding.commentInput.requestFocus()
activity.binding.commentInput.setSelection(activity.binding.commentInput.text.length)
val imm = activity.getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(activity.binding.commentInput, InputMethodManager.SHOW_IMPLICIT)
interactionState = InteractionState.REPLY
}
@SuppressLint("SetTextI18n")
fun replyTo(comment: CommentItem, username: String) {
if (comment.isReplying) {
activity.binding.commentReplyToContainer.visibility = View.VISIBLE
activity.binding.commentReplyTo.text = "Replying to $username"
activity.binding.commentReplyToCancel.setOnClickListener {
comment.replying(false)
replyCallback(comment)
activity.binding.commentReplyToContainer.visibility = View.GONE
}
} else {
activity.binding.commentReplyToContainer.visibility = View.GONE
}
}
/**
* Callback from the comment item to view the replies to the comment
* @param comment the comment to view the replies of
*/
fun viewReplyCallback(comment: CommentItem) {
lifecycleScope.launch {
val replies = withContext(Dispatchers.IO) {
CommentsAPI.getRepliesFromId(comment.comment.commentId)
}
replies?.comments?.forEach {
val depth =
if (comment.commentDepth + 1 > comment.MAX_DEPTH) comment.commentDepth else comment.commentDepth + 1
val section =
if (comment.commentDepth + 1 > comment.MAX_DEPTH) comment.parentSection else comment.repliesSection
if (depth >= comment.MAX_DEPTH) comment.registerSubComment(it.commentId)
val newCommentItem = CommentItem(
it,
buildMarkwon(activity, fragment = this@CommentsFragment),
section,
this@CommentsFragment,
backgroundColor,
depth
)
section.add(newCommentItem)
}
}
}
/**
* Shows the comment rules dialog
* Called when the user tries to comment for the first time
*/
private fun showCommentRulesDialog() {
val alertDialog = android.app.AlertDialog.Builder(activity, R.style.MyPopup)
.setTitle("Commenting Rules")
.setMessage(
"I WILL BAN YOU WITHOUT HESITATION\n" +
"1. No racism\n" +
"2. No hate speech\n" +
"3. No spam\n" +
"4. No NSFW content\n" +
"6. ENGLISH ONLY\n" +
"7. No self promotion\n" +
"8. No impersonation\n" +
"9. No harassment\n" +
"10. No illegal content\n" +
"11. Anything you know you shouldn't comment\n"
)
.setPositiveButton("I Understand") { dialog, _ ->
dialog.dismiss()
PrefManager.setVal(PrefName.FirstComment, false)
processComment()
}
.setNegativeButton("Cancel") { dialog, _ ->
dialog.dismiss()
}
val dialog = alertDialog.show()
dialog?.window?.setDimAmount(0.8f)
}
private fun processComment() {
val commentText = activity.binding.commentInput.text.toString()
if (commentText.isEmpty()) {
snackString("Comment cannot be empty")
return
}
activity.binding.commentInput.text.clear()
lifecycleScope.launch {
if (interactionState == InteractionState.EDIT) {
handleEditComment(commentText)
} else {
handleNewComment(commentText)
tag = null
activity.binding.commentLabel.background = ResourcesCompat.getDrawable(
resources,
R.drawable.ic_label_off_24,
null
)
}
resetOldState()
}
}
private suspend fun handleEditComment(commentText: String) {
val success = withContext(Dispatchers.IO) {
CommentsAPI.editComment(
commentWithInteraction?.comment?.commentId ?: return@withContext false, commentText
)
}
if (success) {
updateCommentInSection(commentText)
}
}
private fun updateCommentInSection(commentText: String) {
val groups = section.groups
groups.forEach { item ->
if (item is CommentItem && item.comment.commentId == commentWithInteraction?.comment?.commentId) {
updateCommentItem(item, commentText)
snackString("Comment edited")
}
}
}
private fun updateCommentItem(item: CommentItem, commentText: String) {
item.comment.content = commentText
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault())
dateFormat.timeZone = TimeZone.getTimeZone("UTC")
item.comment.timestamp = dateFormat.format(System.currentTimeMillis())
item.notifyChanged()
}
/**
* Handles the new user-added comment
* @param commentText the text of the comment
*/
private suspend fun handleNewComment(commentText: String) {
val success = withContext(Dispatchers.IO) {
CommentsAPI.comment(
mediaId,
if (interactionState == InteractionState.REPLY) commentWithInteraction?.comment?.commentId else null,
commentText,
tag
)
}
success?.let {
if (interactionState == InteractionState.REPLY) {
if (commentWithInteraction == null) return@let
val section =
if (commentWithInteraction!!.commentDepth + 1 > commentWithInteraction!!.MAX_DEPTH) commentWithInteraction?.parentSection else commentWithInteraction?.repliesSection
val depth =
if (commentWithInteraction!!.commentDepth + 1 > commentWithInteraction!!.MAX_DEPTH) commentWithInteraction!!.commentDepth else commentWithInteraction!!.commentDepth + 1
if (depth >= commentWithInteraction!!.MAX_DEPTH) commentWithInteraction!!.registerSubComment(
it.commentId
)
section?.add(
if (commentWithInteraction!!.commentDepth + 1 > commentWithInteraction!!.MAX_DEPTH) 0 else section.itemCount,
CommentItem(
it,
buildMarkwon(activity, fragment = this@CommentsFragment),
section,
this@CommentsFragment,
backgroundColor,
depth
)
)
} else {
section.add(
0,
CommentItem(
it,
buildMarkwon(activity, fragment = this@CommentsFragment),
section,
this@CommentsFragment,
backgroundColor,
0
)
)
}
}
}
}

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