Compare commits

...

205 Commits

Author SHA1 Message Date
rebelonion
870cb751a4 fix: duplicate download 2024-04-21 08:06:23 -05:00
rebelonion
4ffe9d7505 fix: novel loading 2024-04-21 07:36:23 -05:00
rebelonion
513b937e59 fix: some sorting problems 2024-04-21 06:41:51 -05:00
rebelonion
6113a10556 fix: update spinner 2024-04-21 06:21:10 -05:00
rebelonion
233f4bfb48 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-04-21 06:06:01 -05:00
aayush262
3fd01d582a fix: forgot to remove todo strings 2024-04-21 16:22:02 +05:30
aayush262
00758af458 feat: desc for every setting 2024-04-21 16:02:21 +05:30
rebelonion
4477e3a0e1 fix: view clickable after hidden
https://stackoverflow.com/questions/4728908/android-view-with-view-gone-still-receives-ontouch-and-onclick
2024-04-21 04:50:55 -05:00
rebelonion
e475cc5c01 fix: novel extension installing 2024-04-21 04:31:24 -05:00
rebelonion
3622d91886 fix: allow deprecated media to be played 2024-04-21 02:58:17 -05:00
rebelonion
3c46c21a25 feat: downloading extensions 2024-04-19 11:24:03 -05:00
Sadwhy
44178b2de2 Why use decapitated actions? (#373) 2024-04-19 10:52:12 -05:00
rebel onion
13f5d0978d Update Crowdin configuration file 2024-04-19 10:22:05 -05:00
rebelonion
70a50ece43 chore: cleanup pt2 2024-04-19 06:13:14 -05:00
rebelonion
24147e746a chore: code cleanup 2024-04-19 06:03:40 -05:00
rebelonion
386e02a564 fix: exoplayer initialization 2024-04-19 05:40:43 -05:00
rebelonion
865b96a219 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-04-19 05:25:01 -05:00
rebel onion
dd38bb156b Update Crowdin configuration file 2024-04-19 05:07:19 -05:00
rebelonion
72c07b7d7a fix: app updater single apk 2024-04-19 04:33:36 -05:00
rebelonion
3f19cadffc fix: workflow universal 2024-04-19 04:16:54 -05:00
rebel onion
670d16bd8e Addons (#368)
* feat: (wip) torrent

credit to kuukiyomi

* fix: extensions -> addons

* fix: unified loader

* feat: (wip) modularity

* fix: addon ui

* feat: addon install/uninstall

---------

Co-authored-by: aayush262 <aayushthakur262006@gmail.com>
2024-04-19 04:08:20 -05:00
aayush262
3d1040b280 [skip ci] feat: theme crash fix 2024-04-18 01:55:15 +05:30
aayush262
cd3bb20afd [skip ci] feat: new settings UI 2024-04-17 14:35:53 +05:30
aayush262
91d1d2cf1d feat: WIP new settings UI 2024-04-17 01:51:56 +05:30
Sadwhy
f8a6fad513 fix(settings): Center icons (again) 😔 (#361) 2024-04-16 00:28:41 -05:00
aayush262
9d3d394c7d feat: hide a12 theme settings for unsupported devices 2024-04-15 22:10:38 +05:30
aayush262
820a09b28f feat: settings to recycler view 2024-04-15 22:04:15 +05:30
aayush262
108285021e fix: custom theme dialog not working 2024-04-15 21:54:01 +05:30
aayush262
1d005585c8 feat: sort forks by stars 2024-04-15 21:52:45 +05:30
rebelonion
714591dd2e fix: infinite loop 2024-04-15 01:51:33 -05:00
rebelonion
6e399b32e1 feat: embedded tracks
modified #338
2024-04-15 01:02:28 -05:00
rebelonion
4b413b78fe fix: alpha update message 2024-04-14 23:30:52 -05:00
rebelonion
126bc6134e chore: update extension api 2024-04-14 23:30:37 -05:00
rebelonion
bf33f5d9c8 fix: activity opening twice 2024-04-14 22:39:30 -05:00
rebelonion
a8ff4fdc26 feat: nomedia file 2024-04-14 22:35:11 -05:00
rebelonion
7ca44480a9 fix: offline mode failing 2024-04-14 22:24:58 -05:00
Sadwhy
ea29449413 fix(settings): Centre icons 2024-04-14 23:54:12 +05:30
Sadwhy
9ec448e503 Feet(Settings): Revamped UI (#352)
Feet(Settings): Revamped UI
2024-04-13 01:22:10 +05:30
aayush262
70be4e92fb fix: Disclaimer dialog crash 2024-04-12 13:40:28 +05:30
Sadwhy
c0e3243ee6 Feet(Settings): UI changes (#351)
* Account

* Theme

* Extension, common, notification and anime

* manga and about

* fix(Settings): icon colours
2024-04-12 12:45:53 +05:30
aayush262
b961701189 fix: settings scrolling 2024-04-11 21:37:25 +05:30
aayush262
3619355cb4 fix: idr 2024-04-11 17:26:20 +05:30
aayush262
674a512630 feat: split all settings 2024-04-11 17:25:41 +05:30
aayush262
5e5277404e feat: toggle for icon in rpc 2024-04-10 14:05:24 +05:30
aayush262
c242d9dd99 fix: no more 13 arifs 2024-04-08 22:52:13 +05:30
aayush262
a5a94e5003 fix: no 13 arifs 2024-04-08 21:08:49 +05:30
aayush262
9b6dc1318d fix: idr 2024-04-08 17:45:18 +05:30
TwistedUmbrellaX
87535a9239 fix: got lost in the cherry-picking (#337) 2024-04-07 21:48:26 -05:00
tutel
6be589618c Added Skip Recap Feature (#336)
* Added Skip Recap Feature

* Reverted gradle.properties to default
2024-04-07 21:30:49 -05:00
TwistedUmbrellaX
a51e025c03 fix: address possible format issues (#331)
* fix: address possible format issued

* fix: improve results and logging

* fix: not everything is a title

Fixed the book title style of capitalization

Toast and Snackbar messages appear for less than 3 seconds. Why are they paragraphs?

* Fix: the other half of the file

Probably missed a few, but this fixes the rest of the obvious ones (including a double negative)
2024-04-07 21:28:34 -05:00
TwistedUmbrellaX
29e115ce41 feat: repo editor in extension window (#332)
* fix: error checking in repo editor

* feat: edit repos from extension page
2024-04-07 21:27:27 -05:00
TwistedUmbrellaX
f96d2ffaa5 feat: add per-widget configuration (#333)
* feat: add per-widget configuration

* fix: no need to overengineer it

* feat: add cache to bitmap download

dfgdfg

* fix: elvis has left the operation
2024-04-07 21:21:24 -05:00
TwistedUmbrellaX
47d05e737d fix: exo / subtitle improvements (#335) 2024-04-07 21:19:50 -05:00
aayush262
3666758e6e feat(socials): ratings and progress 2024-04-07 21:31:15 +05:30
aayush262
e49f0dbf32 feat: socials in media 2024-04-07 00:51:50 +05:30
aayush262
abe3f883ae fix: telegram changelogs (again) 2024-04-06 16:47:31 +05:30
aayush262
e5cb7c7fdf fix: Voice artists not showing media 2024-04-06 16:23:58 +05:30
rebelonion
79337b5e7f Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-04-06 00:58:16 -05:00
rebelonion
ae5907e6b3 fix: correct updater for alpha 2024-04-06 00:57:33 -05:00
aayush262
04fb31eff9 fix: changelogs for telegram 2024-04-06 11:12:00 +05:30
rebelonion
9f7e01a1fb fix: only show count on releasing/hiatus manga 2024-04-05 21:59:44 -05:00
TwistedUmbrellaX
9ace8e5235 fix: it was only an int for convenience (#330)
Probably would have saved a lot of elaborate attempts to fix the issue by simply going the other way.
2024-04-05 21:53:25 -05:00
rebelonion
771cdcc163 fix: MangaExtensionRepos where AnimeExtensionRepos should be 2024-04-05 21:43:32 -05:00
rebelonion
58d5b5bc41 fix: fix the fix for the fix for MangaUpdates
null is better than a crash
2024-04-05 21:21:33 -05:00
TwistedUmbrellaX
04538c52f2 fix: fix the fix for MangaUpdates (#327)
Further filtering prevents the bad records from cancelling the whole operation
2024-04-05 20:04:03 -05:00
rebelonion
dd994dcfab fix: request storage permission for novels 2024-04-05 20:02:36 -05:00
rebelonion
594b71dc16 fix: cache upcoming widget data 2024-04-05 19:50:59 -05:00
rebelonion
cf7ccaebd1 feat: AppUpdater can handle splits 2024-04-05 19:21:00 -05:00
rebelonion
8bde831794 fix: default SearchSources to false
many people thought this was a bug. defaulting off max users more aware of what's going on
2024-04-05 18:54:56 -05:00
rebelonion
2f30bdb6a8 fix: case for empty headers 2024-04-05 18:34:25 -05:00
rebelonion
4d28ae2e3e fix: handle last manga chapter check being null 2024-04-05 18:00:24 -05:00
rebelonion
5fcbfeb3db fix: download name comparison 2024-04-05 17:44:59 -05:00
TwistedUmbrellaX
f6c7b09d9b fix: remove duplicate extension code (#322)
* fix: remove duplicate extension code

* fix: allow Material You icons to load

* fix: remove unused preference item

* fix: load mono on square setups
2024-04-05 16:50:40 -05:00
TwistedUmbrellaX
72c69e7c79 ExoPlayer improvements (#325)
* fix: add declarations for BT headsets

* fix: stop overriding user settings

* fix: offload cache to external storage
2024-04-05 16:49:15 -05:00
aayush262
13a65c2bfa Merge remote-tracking branch 'origin/dev' into dev 2024-04-06 01:21:12 +05:30
aayush262
d2f118a86c feat: more media in recommendations 2024-04-06 01:20:29 +05:30
Sadwhy
ce11c71e95 Fixed artifact upload (#326) 2024-04-05 13:43:22 -05:00
rebel onion
e4574d6c03 feat: send all apks to telegram 2024-04-05 11:40:11 -05:00
Sadwhy
d8c311fbd7 Include all Splits for discord (#324) 2024-04-05 11:36:27 -05:00
aayush262
d6e6c6f8fb Merge remote-tracking branch 'origin/dev' into dev 2024-04-05 13:20:57 +05:30
aayush262
63c3058f5b feat: voiceActor's characters info 2024-04-05 13:20:24 +05:30
aayush262
0d5815d3c9 fix: workflow 2024-04-05 01:22:33 +05:30
aayush262
dec4996760 feat: voiceActors (not info for now) 2024-04-05 01:09:56 +05:30
aayush262
e0df092a70 fix: some tweaks in settings 2024-04-04 22:59:58 +05:30
rebelonion
da56aecd5e fix: return jsdeliver 2024-04-04 05:23:11 -05:00
rebelonion
7688ffa39f Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-04-04 04:49:08 -05:00
rebelonion
d08e89bb63 Merge branch 'pr/299' into dev 2024-04-04 04:48:51 -05:00
TwistedUmbrellaX
5979479619 Two intents enter, one intent leaves (#317)
* feat: add a profile exit button

* fix: prevent leaving landscape behind

This prevents landscape being left out of changes and potentially causing a crash by adding a shared item for the identical portion of the views.
2024-04-04 04:46:23 -05:00
TwistedUmbrellaX
e1b968bfe0 feat: add a time since chapter item (#316)
* feat: add a time since chapter item

* fix: this is the song that never ends
2024-04-04 04:45:03 -05:00
rebelonion
36c476bc36 fix: remove armeabi 2024-04-04 04:40:46 -05:00
rebel onion
6bfadfa962 Update beta.yml 2024-04-04 04:11:52 -05:00
rebel onion
720b40afa7 feat: custom downloader and downloader location (#313)
* feat: custom downloader (novel broken)

* fix: send headers to ffmpeg

ffmpeg can be a real bitch to work with

* fix: offline page for new download system

* feat: novel to new system | load freezing

* chore: clean manifest

* fix: notification incrementing

* feat: changing the downloads dir
2024-04-04 04:03:45 -05:00
TwistedUmbrellaX
75e90541c9 fix: make bottom to top work properly (#320)
* fix: make bottom to top work properly

Fixes navigating to the wrong chapter, despite the UI being correct. Makes bottom to top its own setting that functions as expected, not just top to bottom with a RTL slider bar.

* fix: allow inversion to pick sides
2024-04-04 03:26:21 -05:00
aayush262
47b1940ace feat: Some comment design tweaks 2024-04-03 22:21:27 +05:30
TwistedUmbrellaX
012b1cd79d fix: make settings great again
auto and time stamps are intertwined already and the dividers should serve a purpose
2024-04-03 10:49:50 -04:00
TwistedUmbrellaX
ff3131d988 feat: manual repository entries
Closes Dantotsu#298
2024-04-03 10:49:50 -04:00
TwistedUmbrellaX
ba1725224a fix: automate getting contributions (#314)
* fix: automate getting contributions

It shouldn't need to be a conscious decision. It would be nice if the site_admin  flag worked for the repo owner, but it's a known value

* fix: also populate the forks page

This hardcodes this repo, since downstream builds should still display the upstream forks
2024-04-02 18:08:44 -05:00
Sadwhy
55bc2add85 Updated icons (#311)
* Branch

* Updated Icons (#13)

* update

* update beta.yml

* nicer icons

* update view_list_24.xml

* revert changes

* Missed one
2024-04-02 18:04:18 -05:00
ibo
9e96fd1e20 feat(accounts): redirect on avatar click (#310)
* feat(discord): custom buttons

* feat(discord): added haptics

* fine...

* fix(strings): my genius is frightening

* feat: add option to only show the first button

* feat: discord rpc menu

* feat(link): add button preview back

* feat(accounts): redirect on avatar click

---------

Co-authored-by: aayush262 <aayushthakur262006@gmail.com>
2024-04-02 18:04:08 -05:00
aayush262
79d20b0b63 feat: ibo happy now? 2024-04-02 19:11:20 +05:30
aayush262
b2a44cfe09 Merge remote-tracking branch 'origin/dev' into dev 2024-04-02 17:02:56 +05:30
ibo
146805af49 feat(filter): revamping search for anime and manga (#272)
* feat: revamping search filter part1

* fix: sortBy dropdown now also calls search instead of only updating image

* feat: added longclick listener to reset and apply + cleaned up code

* feat: status filter fully functional

* chore: upgrade AGP to 8.3.1

* fix: splitted status list and cleaned up

* fix(search): underscore

* feat: attempt to add backend for countryOfOrigin filter

* fix: countryOfOrigin query and gradle

* feat: source filter fully functional

* fix(source): underscore

* feat: swap source with status

* fix: add searchSource to reset fun

* fix: clear underline after reopening bottom sheet

* chore: remove unnecessary declaration

* feat: add global to countryOfOrigin dropdown

* feat: floating cancel and apply button

* fix: added searchStatus and searchYear back to manga filter

* feat: desperate attempt for manga year filter

* feat(sortBy): added new releases item

* fix: year filter

---------

Co-authored-by: aayush262 <aayushthakur262006@gmail.com>
2024-04-01 22:13:41 -05:00
ibo
aabbe9198a feat(discord): custom buttons (#295)
* feat(discord): custom buttons

* feat(discord): added haptics

* fine...

* fix(strings): my genius is frightening

* feat: add option to only show the first button

* feat: discord rpc menu

* feat(link): add button preview back

---------

Co-authored-by: aayush262 <aayushthakur262006@gmail.com>
2024-04-01 22:09:52 -05:00
aayush262
a815bac15d feat: 18+ media on infinite scroll too 2024-04-01 19:31:32 +05:30
Sadwhy
86427a4c3c Add CommitHash to Version Name :prayge: (#307) 2024-04-01 00:15:24 -05:00
aayush262
0d8a82568a feat: Download subs 2024-03-31 18:23:29 +05:30
aayush262
95b2939532 fix: hide recent if its empty 2024-03-31 16:44:27 +05:30
aayush262
76e11e5a3e fix: removed unused banners 2024-03-31 16:43:55 +05:30
aayush262
2d5d02fd67 fix: adult only in recent too 2024-03-31 16:18:38 +05:30
aayush262
f30e6b7809 fix: banner animation 2024-03-31 12:23:17 +05:30
aayush262
04f2034dd1 fix: duplicate media 2024-03-31 09:08:54 +05:30
aayush262
99b3bbaaad feat: adult only media option 2024-03-30 15:44:29 +05:30
aayush262
c0bccc027f feat: combined queries 2024-03-30 15:43:37 +05:30
TwistedUmbrellaX
51beac2d03 Revert (some of) "Just some quality of life garbage (#304)" (#306)
This reverts (some of) commit c29147a681.
2024-03-29 21:53:49 -05:00
rebelonion
63a5150cea fix: home screen number spacing 2024-03-29 18:10:41 -05:00
rebelonion
e34a20bce6 fix: comment scrolling freezing 2024-03-29 18:04:19 -05:00
rebelonion
ca482ea9d4 fix: navbar breaking on return to comments fragment 2024-03-29 17:53:50 -05:00
rebelonion
e31d2ada4f fix: logout of comments when log out of anilist 2024-03-29 17:18:20 -05:00
TwistedUmbrellaX
c29147a681 Just some quality of life garbage (#304)
* fix: statistics widget min sizes

* fix: offset for split TextView values

Due to format and color changes, the text is split between two separate items and this space avoids multiple insertions in code

* feat: extension launch from notice

* fix: wait for the UI to post stuff to it
2024-03-29 17:11:37 -05:00
aayush262
92be9bf626 fix: removed onlist for now 2024-03-30 03:11:05 +05:30
aayush262
a02b8b7b0a fix: text in manga side not disappearing 2024-03-30 03:04:27 +05:30
aayush262
1c1d14fff1 fix: "popular manga" text missing 2024-03-30 02:54:44 +05:30
aayush262
eff0a34c54 Merge remote-tracking branch 'origin/dev' into dev 2024-03-30 02:40:04 +05:30
aayush262
2dc3035a7c feat: more options in anime and manga side 2024-03-30 02:39:47 +05:30
TwistedUmbrellaX
78f6ec27b3 feat: add watch title search button (#303) 2024-03-28 17:46:13 -05:00
TwistedUmbrellaX
6b868fa824 fix: not meant to be quoted (#300)
* fix not meant to be quoted

* fix: thought he was slick

hiding in plain sight

* fix: it's not THAT important

* fix: flexible day / night borders
2024-03-28 17:39:04 -05:00
rebelonion
7951c2cf37 fix: some widget sting newlines 2024-03-27 19:02:42 -05:00
rebelonion
ea678ef55e feat: visual representation of selected widget colors 2024-03-27 18:41:48 -05:00
rebelonion
fbbbf41595 feat: custom theming for stats widget 2024-03-27 18:23:13 -05:00
TwistedUmbrellaX
f83d1d8d84 Profile Stats Widget (#292)
* feat: create a statistics widget

* feat: mirror app color option

* fix: the minimum size cut off

* feat: make the stat widget decent

* fix: prevent bleeding edges

* fix: PREVENT BLEEDING EDGES!

* fix: we didn't really need an overlay
2024-03-27 17:45:26 -05:00
TwistedUmbrellaX
7bcc01b94e Merging stuff. Cleaning up code. The usual (#297)
* chore: merge core extension view

* fix: clean up a sloppy fix

* chore: merge name adapters

* fix: offset the indentation of example
2024-03-27 17:45:01 -05:00
aayush262
ff72f9dbdf fix: activity crash 2024-03-27 14:22:04 +05:30
aayush262
b1210570d1 Merge remote-tracking branch 'origin/dev' into dev 2024-03-27 13:52:13 +05:30
aayush262
ef97b5679e feat(widget): use app color 2024-03-27 13:52:00 +05:30
TwistedUmbrellaX
6dfe0269bf Manga reader quirks (#294)
* fix: resolve showing next on previous

* fix: make your last words succinct
2024-03-26 16:10:27 -05:00
TwistedUmbrellaX
77c57846ed fix: add padding to last item in recycler (#293)
* fix: add padding to last item in recycler

Stop guessing numbers to compensate for a view we can measure. by adding a method to measure them.

* fix: avoid scrolling artifacts in nested
2024-03-26 16:10:02 -05:00
aayush262
19b5b11b07 fix(profile): something 2024-03-26 16:06:53 +05:30
aayush262
27d4ce3c5b fix(profile): info card padding 2024-03-26 15:46:38 +05:30
aayush262
859aa01ec2 fix(profile): info is hidden 2024-03-26 15:11:48 +05:30
aayush262
6d102f7be3 fix(media): comment bar padding 2024-03-26 14:48:18 +05:30
rebelonion
5ae1ead2c9 fix: default bitmap width/height 2024-03-26 00:07:48 -05:00
rebelonion
b1982013dc fix: auto curve edges on resize 2024-03-25 23:36:51 -05:00
rebelonion
954fdde1c4 feat: rounded corners compat 2024-03-25 23:07:45 -05:00
rebelonion
f177e2cf7c chore: clean package location 2024-03-25 22:48:26 -05:00
rebelonion
845ebb4868 feat: widget transparency 2024-03-25 22:18:49 -05:00
rebelonion
b43171bb31 fix: remove unnecessary v26 file 2024-03-25 22:12:23 -05:00
rebelonion
be07fad8f1 fix: layout tweaks for upcoming widget 2024-03-25 22:08:51 -05:00
rebelonion
3375496ef2 fix: auto scale title font size 2024-03-25 21:26:25 -05:00
rebelonion
df23b2f62f feat: currently airing widget 2024-03-25 21:20:17 -05:00
TwistedUmbrellaX
95cddbd409 feat: suppress ime for search in progress (#287) 2024-03-25 15:25:58 -05:00
TwistedUmbrellaX
d46f1b25eb feat: option to disable trending scroll (#288) 2024-03-25 15:24:55 -05:00
TwistedUmbrellaX
378abe73c9 feat: add haptics to long click event (#290)
Please, someone. Anyone. Tell me it's OK to let go....
2024-03-25 15:23:44 -05:00
rebelonion
b5eda797b5 fix: context being lost in settings 2024-03-24 18:47:53 -05:00
TwistedUmbrellaX
f704e322af fix: data loading glitches (#284)
* fix: the obnoxious loading glitch

* chore: some quick build warnings
2024-03-24 16:09:26 -05:00
rebelonion
dc21d28b83 Merge branch 'pr/282' into dev 2024-03-23 22:04:55 -05:00
rebelonion
eb17862177 fix: remove unnecessary InefficientWeight 2024-03-23 22:03:09 -05:00
TwistedUmbrellaX
fc023f307a fix: weights reflect other views (#285) 2024-03-23 22:01:44 -05:00
rebelonion
ad1905c8fe fix: adapter continuous loading on media page 2024-03-23 21:50:24 -05:00
rebelonion
85d54e8f5e Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-23 21:12:57 -05:00
rebelonion
ba09f7533c fix: anime page searching manga 2024-03-23 21:12:55 -05:00
TwistedUmbrellaX
fa6e3a34b5 fix: prefer caching the final version (#283)
While caching the original and the final seems like an ideal way to reduce overhead, you cache an original copy of the image and the modified copy of the image to only ever load the modified copy. The size is set, meaning you are not reusing the original image. There is no reason to cache it.
2024-03-23 20:07:02 -05:00
TwistedUmbrellaX
85ef4b3c12 Add transparency options to subtitle (#281)
* feat: add state llistener to Xpandable

* feat: improve app restart process

* feat: support subtitle transparency
2024-03-23 19:12:22 -05:00
rebel onion
89e18b0e2f Merge pull request #280 from RepoDevil/semi-auto
Automatically search through sources
2024-03-23 19:06:56 -05:00
rebelonion
1b50ffcf11 fix: clean up some warnings 2024-03-23 18:05:43 -05:00
TwistedUmbrellaX
b3f83816c5 feat: support exporting magnets 2024-03-23 18:45:23 -04:00
TwistedUmbrellaX
75b78886ae fix: clarify deceptive descriptions 2024-03-23 18:30:54 -04:00
TwistedUmbrellaX
26d97da066 feat: automatically check sources 2024-03-23 18:30:31 -04:00
rebelonion
ab7bc15573 fix: missing string/imports 2024-03-23 17:07:02 -05:00
rebel onion
d43d643bbd Merge pull request #271 from RepoDevil/cleanup
The motherload
2024-03-23 16:56:18 -05:00
TwistedUmbrellaX
3ca5efc177 chore: update androidx.mediarouter
No additional code changes required
2024-03-23 09:39:36 -04:00
TwistedUmbrellaX
04c858e6fd chore: kotlinOptions is deprecated 2024-03-23 09:32:27 -04:00
TwistedUmbrellaX
25046e4c11 chore: add notes for context view
This will allow the details to be seen when highlighting these items in Android Studio
2024-03-23 08:56:15 -04:00
rebelonion
5134776e2f fix: remove unnecessary setExpedited 2024-03-22 23:08:20 -05:00
rebelonion
cc29ebd75b fix: subscription default importance 2024-03-22 22:57:26 -05:00
TwistedUmbrellaX
2233f1ce44 fix: restore a workaround?
The layout this originally used no longer exists and the new layout is a different type, but maybe this will still work.
2024-03-22 23:47:40 -04:00
rebelonion
a189802061 fix: notification check on app launch 2024-03-22 22:34:21 -05:00
rebelonion
dca6ffdbbe Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-22 21:48:01 -05:00
rebelonion
859946a751 chore: version bump 2024-03-22 21:47:58 -05:00
TwistedUmbrellaX
08bf1a2336 chore: eliminate overlap layouts 2024-03-22 19:12:58 -04:00
TwistedUmbrellaX
27743e3427 fix: padding / margin optimization 2024-03-22 14:35:07 -04:00
TwistedUmbrellaX
52b0cc4129 fix: hardcoded text in profile page 2024-03-22 14:26:12 -04:00
TwistedUmbrellaX
22abc2e21d fix: expanded views while editing
This sets the Editor state of the expanded windows. This is NOT reflected at runtime.
2024-03-22 14:18:16 -04:00
TwistedUmbrellaX
fc8425b12a fix: padding / margin optimization 2024-03-22 14:18:16 -04:00
TwistedUmbrellaX
60fc1fa74b fix: tone down the logging a bit 2024-03-22 14:18:16 -04:00
TwistedUmbrellaX
190e3ce7bb fix: someone liked the paste hotkey 2024-03-22 14:18:16 -04:00
TwistedUmbrellaX
012024ab77 fix: disable auto with time stamps 2024-03-22 14:18:16 -04:00
TwistedUmbrellaX
529bdd74c8 chore: flag ites for no translation 2024-03-22 14:18:15 -04:00
TwistedUmbrellaX
6e349b84c0 chore: extract strings from settings 2024-03-22 14:18:15 -04:00
TwistedUmbrellaX
ab9b92035e fix: merge bindings by category 2024-03-22 14:18:15 -04:00
TwistedUmbrellaX
37ec165319 chore: lint performance optimization
This includes shadowed variables, unnecessary parameters, layouts with string literals, items that cause performance bottlenecks, and the merge of extension types into only the necessary separate classes.
2024-03-22 14:18:15 -04:00
TwistedUmbrellaX
958aa634b1 feat: commit to the prank... 2024-03-22 14:18:14 -04:00
TwistedUmbrellaX
125a95285d chore: addressing SetTextI18n 2024-03-22 14:18:14 -04:00
TwistedUmbrellaX
bbaae2e776 fix: settings has over 80 views
One really long layout is bad for performance, but this design also requires being aware of where an item is being placed.
2024-03-22 14:18:14 -04:00
TwistedUmbrellaX
f9090f59b7 fix: support for round vertical 2024-03-22 14:18:14 -04:00
TwistedUmbrellaX
1d740d33a0 fix: putting out 100 little fires
... before they become an inferno
2024-03-22 14:18:14 -04:00
TwistedUmbrellaX
633ec19c90 fix: don't load selected until intent 2024-03-22 14:18:13 -04:00
TwistedUmbrellaX
9b2015f4cf fix: simplify boolean view logic
This is a pretty basic conversion from `if (true) View.VISIBLE else View.GONE` to `isVisible` which is exactly that, but easier to track.
2024-03-22 14:18:13 -04:00
TwistedUmbrellaX
e65e7a79a5 feat: vertical navigation for profile 2024-03-22 14:18:13 -04:00
TwistedUmbrellaX
0996639cac fix: vertical AnimatedBottomBar 2024-03-22 14:18:13 -04:00
TwistedUmbrellaX
e5f58f20c7 fix: undo all of the margin hacks
Using 72dp as the height appears to have been a bit of a hack to appear beyond the navigation bar. In cases where the bar is not present, such as landscape, this left a gap between the bottom of the screen and bar. On API 23, the result was the opposite. All of this can be addressed by simply relying on the actual measurements and not compensating for compensation.
2024-03-22 14:18:13 -04:00
aayush262
d1e03b8237 feat(media): fav and popularity count 2024-03-22 23:44:51 +05:30
aayush262
917ffe644f feat: something 2024-03-22 20:38:34 +05:30
aayush262
02efc01a10 feat(profile): round chips 2024-03-22 12:50:04 +05:30
aayush262
3016792f95 fix(activity): blur banner 2024-03-22 11:24:36 +05:30
Sadwhy
e1b50c86f3 feet(watch): Fixed one inconsistent switch (#273)
* feet(watch): Fixed one inconsistent switch
2024-03-21 11:41:16 +05:30
422 changed files with 18551 additions and 11795 deletions

View File

@@ -48,9 +48,11 @@ jobs:
echo "COMMIT_LOG=${COMMIT_LOGS}" >> $GITHUB_ENV echo "COMMIT_LOG=${COMMIT_LOGS}" >> $GITHUB_ENV
# Debugging: Print the variable to check its content # Debugging: Print the variable to check its content
echo "$COMMIT_LOGS" echo "$COMMIT_LOGS"
echo "$COMMIT_LOGS" > commit_log.txt
shell: /usr/bin/bash -e {0} shell: /usr/bin/bash -e {0}
env: env:
CI: true CI: true
continue-on-error: true
- name: Save Current SHA for Next Run - name: Save Current SHA for Next Run
run: echo ${{ github.sha }} > last_sha.txt run: echo ${{ github.sha }} > last_sha.txt
@@ -75,7 +77,7 @@ jobs:
- name: List files in the directory - name: List files in the directory
run: ls -l run: ls -l
- name: Make gradlew executable - name: Make gradlew executable
run: chmod +x ./gradlew run: chmod +x ./gradlew
@@ -83,9 +85,11 @@ jobs:
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 }} 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@v4.3.1 uses: actions/upload-artifact@v4
with: with:
name: Dantotsu name: Dantotsu
retention-days: 5
compression-level: 9
path: "app/build/outputs/apk/google/alpha/app-google-alpha.apk" path: "app/build/outputs/apk/google/alpha/app-google-alpha.apk"
- name: Upload APK to Discord and Telegram - name: Upload APK to Discord and Telegram
@@ -99,7 +103,7 @@ jobs:
if [ ${#commit_messages} -gt $max_length ]; then if [ ${#commit_messages} -gt $max_length ]; then
commit_messages="${commit_messages:0:$max_length}... (truncated)" commit_messages="${commit_messages:0:$max_length}... (truncated)"
fi fi
contentbody=$( jq -nc --arg msg "Alpha-Build: <@714249925248024617> **$VERSION**:" --arg commits "$commit_messages" '{"content": ($msg + "\n" + $commits)}' ) contentbody=$( jq -nc --arg msg "Alpha-Build: <@&1225347048321191996> **$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 }} curl -F "payload_json=${contentbody}" -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }}
#Telegram #Telegram
@@ -113,18 +117,13 @@ jobs:
VERSION: ${{ env.VERSION }} VERSION: ${{ env.VERSION }}
- name: Upload Current SHA as Artifact - name: Upload Current SHA as Artifact
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v4
with: with:
name: last-sha name: last-sha
path: last_sha.txt path: last_sha.txt
- name: Upload Commit log as Artifact
- name: Delete Old Pre-Releases uses: actions/upload-artifact@v4
id: delete-pre-releases
uses: sgpublic/delete-release-action@master
with: with:
pre-release-drop: true name: commit-log
pre-release-keep-count: 3 path: commit_log.txt
pre-release-drop-tag: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

3
.gitignore vendored
View File

@@ -31,3 +31,6 @@ output.json
#other #other
scripts/ scripts/
#crowdin
crowdin.yml

View File

@@ -6,6 +6,10 @@ plugins {
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
@@ -17,6 +21,7 @@ android {
versionName "3.0.0" versionName "3.0.0"
versionCode 300000000 versionCode 300000000
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
} }
flavorDimensions += "store" flavorDimensions += "store"
@@ -38,7 +43,7 @@ android {
buildTypes { buildTypes {
alpha { alpha {
applicationIdSuffix ".beta" // keep as beta by popular request applicationIdSuffix ".beta" // keep as beta by popular request
versionNameSuffix "-alpha01" versionNameSuffix "-alpha01-" + gitCommitHash
manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher_alpha" manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher_alpha"
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_alpha_round" manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_alpha_round"
debuggable System.getenv("CI") == null debuggable System.getenv("CI") == null
@@ -46,7 +51,7 @@ android {
} }
debug { debug {
applicationIdSuffix ".beta" applicationIdSuffix ".beta"
versionNameSuffix "-beta01" versionNameSuffix "-beta02"
manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher_beta" manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher_beta"
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_beta_round" manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_beta_round"
debuggable false debuggable false
@@ -75,11 +80,11 @@ android {
dependencies { dependencies {
// FireBase // FireBase
googleImplementation platform('com.google.firebase:firebase-bom:32.7.4') googleImplementation platform('com.google.firebase:firebase-bom:32.8.1')
googleImplementation 'com.google.firebase:firebase-analytics-ktx:21.5.1' googleImplementation 'com.google.firebase:firebase-analytics-ktx:21.6.2'
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.2' googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.4'
// Core // Core
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.browser:browser:1.8.0' implementation 'androidx.browser:browser:1.8.0'
implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.core:core-ktx:1.12.0'
@@ -95,8 +100,9 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3'
implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.webkit:webkit:1.10.0' implementation 'androidx.webkit:webkit:1.10.0'
implementation "com.anggrayudi:storage:1.5.5"
// Glide // Glide
ext.glide_version = '4.16.0' ext.glide_version = '4.16.0'
api "com.github.bumptech.glide:glide:$glide_version" api "com.github.bumptech.glide:glide:$glide_version"
implementation "com.github.bumptech.glide:glide:$glide_version" implementation "com.github.bumptech.glide:glide:$glide_version"
@@ -104,49 +110,48 @@ 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'
// Exoplayer // Exoplayer
ext.exo_version = '1.3.0' ext.exo_version = '1.3.1'
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 // Media3 Casting
implementation "androidx.media3:media3-cast:$exo_version" implementation "androidx.media3:media3-cast:$exo_version"
implementation "androidx.mediarouter:mediarouter:1.6.0" implementation "androidx.mediarouter:mediarouter:1.7.0"
// UI // UI
implementation 'com.google.android.material:material:1.11.0' implementation 'com.google.android.material:material:1.11.0'
//implementation 'nl.joery.animatedbottombar:library:1.1.0' implementation 'com.github.RepoDevil:AnimatedBottomBar:7fcb9af'
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.eltos:simpledialogfragments:v3.7' implementation 'com.github.eltos:simpledialogfragments:v3.7'
implementation 'com.github.AAChartModel:AAChartCore-Kotlin:93972bc' implementation 'com.github.AAChartModel:AAChartCore-Kotlin:7.2.1'
// Markwon // Markwon
ext.markwon_version = '4.6.2' ext.markwon_version = '4.6.2'
implementation "io.noties.markwon:core:$markwon_version" implementation "io.noties.markwon:core:$markwon_version"
implementation "io.noties.markwon:editor:$markwon_version" implementation "io.noties.markwon:editor:$markwon_version"
implementation "io.noties.markwon:ext-strikethrough:$markwon_version" implementation "io.noties.markwon:ext-strikethrough:$markwon_version"
implementation "io.noties.markwon:ext-tables:$markwon_version" implementation "io.noties.markwon:ext-tables:$markwon_version"
implementation "io.noties.markwon:ext-tasklist:$markwon_version" implementation "io.noties.markwon:ext-tasklist:$markwon_version"
implementation "io.noties.markwon:html:$markwon_version" implementation "io.noties.markwon:html:$markwon_version"
implementation "io.noties.markwon:image-glide:$markwon_version" implementation "io.noties.markwon:image-glide:$markwon_version"
// Groupie // Groupie
ext.groupie_version = '2.10.1' ext.groupie_version = '2.10.1'
implementation "com.github.lisawray.groupie:groupie:$groupie_version" implementation "com.github.lisawray.groupie:groupie:$groupie_version"
implementation "com.github.lisawray.groupie:groupie-viewbinding:$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'
// Aniyomi // Aniyomi
implementation 'io.reactivex:rxjava:1.3.8' implementation 'io.reactivex:rxjava:1.3.8'
implementation 'io.reactivex:rxandroid:1.2.1' implementation 'io.reactivex:rxandroid:1.2.1'
implementation 'ru.beryukhov:flowreactivenetwork:1.0.4' implementation 'ru.beryukhov:flowreactivenetwork:1.0.4'

View File

@@ -5,12 +5,12 @@ import com.google.firebase.FirebaseApp
import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.firebase.crashlytics.ktx.crashlytics import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase import com.google.firebase.ktx.Firebase
import com.google.firebase.ktx.app
class FirebaseCrashlytics : CrashlyticsInterface { class FirebaseCrashlytics : CrashlyticsInterface {
override fun initialize(context: Context) { override fun initialize(context: Context) {
FirebaseApp.initializeApp(context) FirebaseApp.initializeApp(context)
} }
override fun logException(e: Throwable) { override fun logException(e: Throwable) {
FirebaseCrashlytics.getInstance().recordException(e) FirebaseCrashlytics.getInstance().recordException(e)
} }

View File

@@ -85,13 +85,18 @@ object AppUpdater {
setPositiveButton(currContext()!!.getString(R.string.lets_go)) { setPositiveButton(currContext()!!.getString(R.string.lets_go)) {
MainScope().launch(Dispatchers.IO) { MainScope().launch(Dispatchers.IO) {
try { try {
client.get("https://api.github.com/repos/$repo/releases/tags/v$version") val apks =
.parsed<GithubResponse>().assets?.find { client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
it.browserDownloadURL.endsWith("apk") .parsed<GithubResponse>().assets?.filter {
}?.browserDownloadURL.apply { it.browserDownloadURL.endsWith(
if (this != null) activity.downloadUpdate(version, this) ".apk"
else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version") )
} }
val apkToDownload = apks?.first()
apkToDownload?.browserDownloadURL.apply {
if (this != null) activity.downloadUpdate(version, this)
else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
}
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
} }
@@ -111,24 +116,25 @@ object AppUpdater {
} }
private fun compareVersion(version: String): Boolean { private fun compareVersion(version: String): Boolean {
return when (BuildConfig.BUILD_TYPE) {
"debug" -> BuildConfig.VERSION_NAME != version
"alpha" -> false
else -> {
fun toDouble(list: List<String>): Double {
return list.mapIndexed { i: Int, s: String ->
when (i) {
0 -> s.toDouble() * 100
1 -> s.toDouble() * 10
2 -> s.toDouble()
else -> s.toDoubleOrNull() ?: 0.0
}
}.sum()
}
if (BuildConfig.DEBUG) { val new = toDouble(version.split("."))
return BuildConfig.VERSION_NAME != version val curr = toDouble(BuildConfig.VERSION_NAME.split("."))
} else { new > curr
fun toDouble(list: List<String>): Double {
return list.mapIndexed { i: Int, s: String ->
when (i) {
0 -> s.toDouble() * 100
1 -> s.toDouble() * 10
2 -> s.toDouble()
else -> s.toDoubleOrNull() ?: 0.0
}
}.sum()
} }
val new = toDouble(version.split("."))
val curr = toDouble(BuildConfig.VERSION_NAME.split("."))
return new > curr
} }
} }

View File

@@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-sdk tools:overrideLibrary="go.server.gojni" />
<uses-feature <uses-feature
android:name="android.software.leanback" android:name="android.software.leanback"
android:required="false" /> android:required="false" />
@@ -19,7 +21,7 @@
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" /> <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="29" />
<uses-permission <uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE" android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" /> <!-- For background jobs --> android:maxSdkVersion="32" /> <!-- For background jobs -->
@@ -38,6 +40,17 @@
android:name="android.permission.READ_APP_SPECIFIC_LOCALES" android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
tools:ignore="ProtectedPermissions" /> tools:ignore="ProtectedPermissions" />
<!-- ExoPlayer: Bluetooth Headsets -->
<uses-feature
android:name="android.hardware.bluetooth"
android:required="false" />
<uses-permission
android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- ExoPlayer: Bluetooth Headsets -->
<queries> <queries>
<package android:name="idm.internet.download.manager.plus" /> <package android:name="idm.internet.download.manager.plus" />
<package android:name="idm.internet.download.manager" /> <package android:name="idm.internet.download.manager" />
@@ -49,6 +62,7 @@
android:name=".App" android:name=".App"
android:allowBackup="true" android:allowBackup="true"
android:banner="@mipmap/ic_banner_foreground" android:banner="@mipmap/ic_banner_foreground"
android:enableOnBackInvokedCallback="true"
android:icon="${icon_placeholder}" android:icon="${icon_placeholder}"
android:label="@string/app_name" android:label="@string/app_name"
android:largeHeap="true" android:largeHeap="true"
@@ -57,9 +71,30 @@
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"
tools:targetApi="tiramisu">
<receiver <receiver
android:name=".widgets.CurrentlyAiringWidget" android:name=".widgets.upcoming.UpcomingWidget"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/upcoming_widget_info" />
</receiver>
<activity
android:name=".widgets.upcoming.UpcomingWidgetConfigure"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<receiver
android:name=".widgets.statistics.ProfileStatsWidget"
android:exported="false"> android:exported="false">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@@ -67,11 +102,10 @@
<meta-data <meta-data
android:name="android.appwidget.provider" android:name="android.appwidget.provider"
android:resource="@xml/currently_airing_widget_info" /> android:resource="@xml/statistics_widget_info" />
</receiver> </receiver>
<receiver android:name=".notifications.IncognitoNotificationClickReceiver" /> <receiver android:name=".notifications.IncognitoNotificationClickReceiver" />
<activity <activity
android:name=".media.novel.novelreader.NovelReaderActivity" android:name=".media.novel.novelreader.NovelReaderActivity"
android:configChanges="orientation|screenSize" android:configChanges="orientation|screenSize"
@@ -103,27 +137,61 @@
android:name=".settings.SettingsActivity" android:name=".settings.SettingsActivity"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
<activity <activity
android:name=".settings.ExtensionsActivity" android:name=".settings.SettingsAboutActivity"
android:windowSoftInputMode="adjustResize|stateHidden"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsAccountActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsAnimeActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsCommonActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsExtensionsActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsAddonActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsMangaActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsNotificationActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsThemeActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.ExtensionsActivity"
android:parentActivityName=".MainActivity"
android:windowSoftInputMode="adjustResize|stateHidden" />
<activity
android:name=".widgets.statistics.ProfileStatsConfigure"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<activity <activity
android:name=".profile.ProfileActivity" android:name=".profile.ProfileActivity"
android:windowSoftInputMode="adjustResize|stateHidden" android:parentActivityName=".MainActivity"
android:parentActivityName=".MainActivity" /> android:windowSoftInputMode="adjustResize|stateHidden" />
<activity <activity
android:name=".profile.FollowActivity" android:name=".profile.FollowActivity"
android:windowSoftInputMode="adjustResize|stateHidden" android:parentActivityName=".MainActivity"
android:parentActivityName=".MainActivity" /> android:windowSoftInputMode="adjustResize|stateHidden" />
<activity <activity
android:name=".profile.activity.FeedActivity" android:name=".profile.activity.FeedActivity"
android:configChanges="orientation|screenSize|screenLayout"
android:label="Inbox Activity" android:label="Inbox Activity"
android:parentActivityName=".MainActivity" > android:parentActivityName=".MainActivity" />
</activity>
<activity <activity
android:name=".profile.activity.NotificationActivity" android:name=".profile.activity.NotificationActivity"
android:label="Inbox Activity" android:label="Inbox Activity"
android:parentActivityName=".MainActivity" > android:parentActivityName=".MainActivity" />
</activity>
<activity <activity
android:name=".others.imagesearch.ImageSearchActivity" android:name=".others.imagesearch.ImageSearchActivity"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
@@ -136,8 +204,9 @@
android:name=".media.CalendarActivity" android:name=".media.CalendarActivity"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
<activity android:name=".media.user.ListActivity" /> <activity android:name=".media.user.ListActivity" />
<activity android:name=".profile.SingleStatActivity" <activity
android:parentActivityName=".profile.ProfileActivity"/> android:name=".profile.SingleStatActivity"
android:parentActivityName=".profile.ProfileActivity" />
<activity <activity
android:name=".media.manga.mangareader.MangaReaderActivity" android:name=".media.manga.mangareader.MangaReaderActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
@@ -149,7 +218,7 @@
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"/> 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
@@ -231,7 +300,6 @@
<data android:host="discord.dantotsu.com" /> <data android:host="discord.dantotsu.com" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".connections.anilist.UrlMedia" android:name=".connections.anilist.UrlMedia"
android:configChanges="orientation|screenSize|layoutDirection" android:configChanges="orientation|screenSize|layoutDirection"
@@ -290,7 +358,9 @@
</intent-filter> </intent-filter>
<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:scheme="content" /> <data android:scheme="content" />
<data android:mimeType="*/*" /> <data android:mimeType="*/*" />
<data android:pathPattern=".*\\.ani" /> <data android:pathPattern=".*\\.ani" />
@@ -299,30 +369,27 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity" android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallActivity"
android:exported="false"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallActivity"
android:exported="false" android:exported="false"
android:theme="@android:style/Theme.Translucent.NoTitleBar" /> android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<receiver android:name=".notifications.AlarmPermissionStateReceiver" <receiver
android:name=".notifications.AlarmPermissionStateReceiver"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.app.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED" /> <action android:name="android.app.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver
<receiver android:name=".notifications.BootCompletedReceiver" 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" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name=".notifications.anilist.AnilistNotificationReceiver"/> <receiver android:name=".notifications.anilist.AnilistNotificationReceiver" />
<receiver android:name=".notifications.comment.CommentNotificationReceiver"/> <receiver android:name=".notifications.comment.CommentNotificationReceiver" />
<receiver android:name=".notifications.subscription.SubscriptionNotificationReceiver"/> <receiver android:name=".notifications.subscription.SubscriptionNotificationReceiver" />
<meta-data <meta-data
android:name="preloaded_fonts" android:name="preloaded_fonts"
@@ -340,25 +407,11 @@
</provider> </provider>
<service <service
android:name=".widgets.CurrentlyAiringRemoteViewsService" android:name=".widgets.upcoming.UpcomingRemoteViewsService"
android:exported="true" android:exported="true"
android:permission="android.permission.BIND_REMOTEVIEWS" /> android:permission="android.permission.BIND_REMOTEVIEWS" />
<service <service
android:name=".download.video.ExoplayerDownloadService" android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallService"
android:exported="false"
android:foregroundServiceType="dataSync">
<intent-filter>
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
<service
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
android:exported="false" android:exported="false"
android:foregroundServiceType="dataSync" /> android:foregroundServiceType="dataSync" />
<service <service
@@ -381,6 +434,11 @@
android:name="androidx.media3.exoplayer.scheduler.PlatformScheduler$PlatformSchedulerService" android:name="androidx.media3.exoplayer.scheduler.PlatformScheduler$PlatformSchedulerService"
android:exported="true" android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" /> android:permission="android.permission.BIND_JOB_SERVICE" />
<service
android:name=".addons.torrent.ServerService"
android:exported="false"
android:foregroundServiceType="dataSync"
android:stopWithTask="true" />
<meta-data <meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME" android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"

View File

@@ -6,6 +6,8 @@ import android.content.Context
import android.os.Bundle import android.os.Bundle
import androidx.multidex.MultiDex import androidx.multidex.MultiDex
import androidx.multidex.MultiDexApplication import androidx.multidex.MultiDexApplication
import ani.dantotsu.addons.download.DownloadAddonManager
import ani.dantotsu.addons.torrent.TorrentAddonManager
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.comments.CommentsAPI
@@ -41,6 +43,9 @@ class App : MultiDexApplication() {
private lateinit var animeExtensionManager: AnimeExtensionManager private lateinit var animeExtensionManager: AnimeExtensionManager
private lateinit var mangaExtensionManager: MangaExtensionManager private lateinit var mangaExtensionManager: MangaExtensionManager
private lateinit var novelExtensionManager: NovelExtensionManager private lateinit var novelExtensionManager: NovelExtensionManager
private lateinit var torrentAddonManager: TorrentAddonManager
private lateinit var downloadAddonManager: DownloadAddonManager
override fun attachBaseContext(base: Context?) { override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base) super.attachBaseContext(base)
MultiDex.install(this) MultiDex.install(this)
@@ -86,7 +91,7 @@ class App : MultiDexApplication() {
Thread.setDefaultUncaughtExceptionHandler(FinalExceptionHandler()) Thread.setDefaultUncaughtExceptionHandler(FinalExceptionHandler())
Logger.log("App: Logging started") Logger.log("App: Logging started")
initializeNetwork(baseContext) initializeNetwork()
setupNotificationChannels() setupNotificationChannels()
if (!LogcatLogger.isInstalled) { if (!LogcatLogger.isInstalled) {
@@ -96,6 +101,8 @@ class App : MultiDexApplication() {
animeExtensionManager = Injekt.get() animeExtensionManager = Injekt.get()
mangaExtensionManager = Injekt.get() mangaExtensionManager = Injekt.get()
novelExtensionManager = Injekt.get() novelExtensionManager = Injekt.get()
torrentAddonManager = Injekt.get()
downloadAddonManager = Injekt.get()
val animeScope = CoroutineScope(Dispatchers.Default) val animeScope = CoroutineScope(Dispatchers.Default)
animeScope.launch { animeScope.launch {
@@ -115,13 +122,20 @@ class App : MultiDexApplication() {
Logger.log("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}") Logger.log("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
NovelSources.init(novelExtensionManager.installedExtensionsFlow) NovelSources.init(novelExtensionManager.installedExtensionsFlow)
} }
val addonScope = CoroutineScope(Dispatchers.Default)
addonScope.launch {
torrentAddonManager.init()
downloadAddonManager.init()
}
val commentsScope = CoroutineScope(Dispatchers.Default) val commentsScope = CoroutineScope(Dispatchers.Default)
commentsScope.launch { commentsScope.launch {
CommentsAPI.fetchAuthToken() CommentsAPI.fetchAuthToken()
} }
val useAlarmManager = PrefManager.getVal<Boolean>(PrefName.UseAlarmManager) val useAlarmManager = PrefManager.getVal<Boolean>(PrefName.UseAlarmManager)
TaskScheduler.create(this, useAlarmManager).scheduleAllTasks(this) val scheduler = TaskScheduler.create(this, useAlarmManager)
scheduler.scheduleAllTasks(this)
scheduler.scheduleSingleWork(this)
} }
private fun setupNotificationChannels() { private fun setupNotificationChannels() {
@@ -152,6 +166,10 @@ class App : MultiDexApplication() {
companion object { companion object {
private var instance: App? = null private var instance: App? = null
/** Reference to the application context.
*
* USE WITH EXTREME CAUTION!**/
var context: Context? = null var context: Context? = null
fun currentContext(): Context? { fun currentContext(): Context? {
return instance?.mFTActivityLifecycleCallbacks?.currentActivity ?: context return instance?.mFTActivityLifecycleCallbacks?.currentActivity ?: context

View File

@@ -10,6 +10,7 @@ import android.app.PendingIntent
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
@@ -66,12 +67,9 @@ import android.widget.FrameLayout
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast 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.NotificationCompat
import androidx.core.app.NotificationManagerCompat 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
@@ -90,6 +88,7 @@ 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.bakaupdates.MangaUpdates
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface 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
@@ -100,6 +99,7 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.settings.saving.internal.PreferenceKeystore import ani.dantotsu.settings.saving.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt
import ani.dantotsu.util.CountUpTimer
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder import com.bumptech.glide.RequestBuilder
@@ -132,9 +132,12 @@ import io.noties.markwon.html.TagHandlerNoOp
import io.noties.markwon.image.AsyncDrawable import io.noties.markwon.image.AsyncDrawable
import io.noties.markwon.image.glide.GlideImagesPlugin import io.noties.markwon.image.glide.GlideImagesPlugin
import jp.wasabeef.glide.transformations.BlurTransformation import jp.wasabeef.glide.transformations.BlurTransformation
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import nl.joery.animatedbottombar.AnimatedBottomBar import nl.joery.animatedbottombar.AnimatedBottomBar
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@@ -182,6 +185,11 @@ fun currActivity(): Activity? {
var loadMedia: Int? = null var loadMedia: Int? = null
var loadIsMAL = false var loadIsMAL = false
val Int.toPx
get() = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), getSystem().displayMetrics
).toInt()
fun initActivity(a: Activity) { fun initActivity(a: Activity) {
val window = a.window val window = a.window
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
@@ -201,13 +209,16 @@ fun initActivity(a: Activity) {
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.navigationBars()).bottom navBarHeight = this.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) navBarHeight += 48.toPx
} }
} }
WindowInsetsControllerCompat( WindowInsetsControllerCompat(
window, window,
window.decorView window.decorView
).hide(WindowInsetsCompat.Type.statusBars()) ).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) {
statusBarHeight = min(boundingRects[0].width(), boundingRects[0].height()) statusBarHeight = min(boundingRects[0].width(), boundingRects[0].height())
@@ -222,6 +233,7 @@ fun initActivity(a: Activity) {
statusBarHeight = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).top statusBarHeight = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).top
navBarHeight = navBarHeight =
windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) navBarHeight += 48.toPx
} }
} }
if (a !is MainActivity) a.setNavigationTheme() if (a !is MainActivity) a.setNavigationTheme()
@@ -262,6 +274,56 @@ fun Activity.setNavigationTheme() {
} }
} }
/**
* Sets clipToPadding false and sets the combined height of navigation bars as bottom padding.
*
* When nesting multiple scrolling views, only call this method on the inner most scrolling view.
*/
fun ViewGroup.setBaseline(navBar: AnimatedBottomBar) {
navBar.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
clipToPadding = false
setPadding(paddingLeft, paddingTop, paddingRight, navBarHeight + navBar.measuredHeight)
}
/**
* Sets clipToPadding false and sets the combined height of navigation bars as bottom padding.
*
* When nesting multiple scrolling views, only call this method on the inner most scrolling view.
*/
fun ViewGroup.setBaseline(navBar: AnimatedBottomBar, overlayView: View) {
navBar.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
overlayView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
clipToPadding = false
setPadding(
paddingLeft,
paddingTop,
paddingRight,
navBarHeight + navBar.measuredHeight + overlayView.measuredHeight
)
}
fun Activity.reloadActivity() {
Refresh.all()
finish()
startActivity(Intent(this, this::class.java))
initActivity(this)
}
fun Activity.restartApp() {
val mainIntent = Intent.makeRestartActivityTask(
packageManager.getLaunchIntentForPackage(this.packageName)!!.component
)
val component =
ComponentName(this@restartApp.packageName, this@restartApp::class.qualifiedName!!)
try {
startActivity(Intent().setComponent(component))
} catch (e: Exception) {
startActivity(mainIntent)
}
finishAndRemoveTask()
PrefManager.setCustomVal("reload", true)
}
open class BottomSheetDialogFragment : BottomSheetDialogFragment() { open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
@@ -359,7 +421,7 @@ class DatePickerFragment(activity: Activity, var date: FuzzyDate = FuzzyDate().g
dialog.setButton( dialog.setButton(
DialogInterface.BUTTON_NEUTRAL, DialogInterface.BUTTON_NEUTRAL,
activity.getString(R.string.remove) activity.getString(R.string.remove)
) { dialog, which -> ) { _, which ->
if (which == DialogInterface.BUTTON_NEUTRAL) { if (which == DialogInterface.BUTTON_NEUTRAL) {
date = FuzzyDate() date = FuzzyDate()
} }
@@ -394,7 +456,6 @@ class InputFilterMinMax(
return "" return ""
} }
@SuppressLint("SetTextI18n")
private fun isInRange(a: Double, b: Double, c: Double): Boolean { private fun isInRange(a: Double, b: Double, c: Double): Boolean {
val statusStrings = currContext()!!.resources.getStringArray(R.array.status_manga)[2] val statusStrings = currContext()!!.resources.getStringArray(R.array.status_manga)[2]
@@ -407,7 +468,7 @@ class InputFilterMinMax(
} }
class ZoomOutPageTransformer() : class ZoomOutPageTransformer :
ViewPager2.PageTransformer { ViewPager2.PageTransformer {
override fun transformPage(view: View, position: Float) { override fun transformPage(view: View, position: Float) {
if (position == 0.0f && PrefManager.getVal(PrefName.LayoutAnimations)) { if (position == 0.0f && PrefManager.getVal(PrefName.LayoutAnimations)) {
@@ -563,13 +624,35 @@ fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
file?.url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { file?.url ?: "" } 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 } if (file.url.startsWith("content://")) {
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size) Glide.with(this.context).load(Uri.parse(file.url)).transition(withCrossFade())
.into(this) .override(size).into(this)
} else {
val glideUrl = GlideUrl(file.url) { file.headers }
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
.into(this)
}
} }
} }
} }
fun ImageView.loadImage(file: FileUrl?, width: Int = 0, height: Int = 0) {
file?.url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { file?.url ?: "" }
if (file?.url?.isNotEmpty() == true) {
tryWith {
if (file.url.startsWith("content://")) {
Glide.with(this.context).load(Uri.parse(file.url)).transition(withCrossFade())
.override(width, height).into(this)
} else {
val glideUrl = GlideUrl(file.url) { file.headers }
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(width, height)
.into(this)
}
}
}
}
fun ImageView.loadLocalImage(file: File?, size: Int = 0) { fun ImageView.loadLocalImage(file: File?, size: Int = 0) {
if (file?.exists() == true) { if (file?.exists() == true) {
tryWith { tryWith {
@@ -712,6 +795,23 @@ fun openLinkInBrowser(link: String?) {
} }
} }
fun openLinkInYouTube(link: String?) {
link?.let {
try {
val videoIntent = Intent(Intent.ACTION_VIEW).apply {
addCategory(Intent.CATEGORY_BROWSABLE)
data = Uri.parse(link)
setPackage("com.google.android.youtube")
}
currContext()!!.startActivity(videoIntent)
} catch (e: ActivityNotFoundException) {
openLinkInBrowser(link)
} catch (e: Exception) {
Logger.log(e)
}
}
}
fun saveImageToDownloads(title: String, bitmap: Bitmap, context: Activity) { fun saveImageToDownloads(title: String, bitmap: Bitmap, context: Activity) {
FileProvider.getUriForFile( FileProvider.getUriForFile(
context, context,
@@ -803,31 +903,6 @@ fun savePrefs(
} }
} }
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(
@@ -897,9 +972,10 @@ fun copyToClipboard(string: String, toast: Boolean = true) {
} }
} }
@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 * 28.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 =
@@ -931,6 +1007,50 @@ fun countDown(media: Media, view: ViewGroup) {
} }
} }
fun sinceWhen(media: Media, view: ViewGroup) {
if (media.status != "RELEASING" && media.status != "HIATUS") return
CoroutineScope(Dispatchers.IO).launch {
MangaUpdates().search(media.mangaName(), media.startDate)?.let {
val latestChapter = MangaUpdates.getLatestChapter(view.context, it)
val timeSince = (System.currentTimeMillis() -
(it.metadata.series.lastUpdated!!.timestamp * 1000)) / 1000
withContext(Dispatchers.Main) {
val v =
ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
view.addView(v.root, 0)
v.mediaCountdownText.text =
currActivity()?.getString(R.string.chapter_release_timeout, latestChapter)
object : CountUpTimer(86400000) {
override fun onTick(second: Int) {
val a = second + timeSince
v.mediaCountdown.text = currActivity()?.getString(
R.string.time_format,
a / 86400,
a % 86400 / 3600,
a % 86400 % 3600 / 60,
a % 86400 % 3600 % 60
)
}
override fun onFinish() {
// The legend will never die.
}
}.start()
}
}
}
}
fun displayTimer(media: Media, view: ViewGroup) {
when {
media.anime != null -> countDown(media, view)
media.format == "MANGA" || media.format == "ONE_SHOT" -> sinceWhen(media, view)
else -> {} // No timer yet
}
}
fun MutableMap<String, Genre>.checkId(id: Int): Boolean { fun MutableMap<String, Genre>.checkId(id: Int): Boolean {
this.forEach { this.forEach {
if (it.value.id == id) { if (it.value.id == id) {
@@ -1000,6 +1120,10 @@ class EmptyAdapter(private val count: Int) : RecyclerView.Adapter<RecyclerView.V
inner class EmptyViewHolder(view: View) : RecyclerView.ViewHolder(view) inner class EmptyViewHolder(view: View) : RecyclerView.ViewHolder(view)
} }
fun getAppString(res: Int): String {
return currContext()?.getString(res) ?: ""
}
fun toast(string: String?) { fun toast(string: String?) {
if (string != null) { if (string != null) {
Logger.log(string) Logger.log(string)
@@ -1010,6 +1134,10 @@ fun toast(string: String?) {
} }
} }
fun toast(res: Int) {
toast(getAppString(res))
}
fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null): Snackbar? { fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null): Snackbar? {
try { //I have no idea why this sometimes crashes for some people... try { //I have no idea why this sometimes crashes for some people...
if (s != null) { if (s != null) {
@@ -1050,6 +1178,10 @@ fun snackString(s: String?, activity: Activity? = null, clipboard: String? = nul
return null return null
} }
fun snackString(r: Int, activity: Activity? = null, clipboard: String? = null): Snackbar? {
return snackString(getAppString(r), activity, clipboard)
}
open class NoPaddingArrayAdapter<T>(context: Context, layoutId: Int, items: List<T>) : open class NoPaddingArrayAdapter<T>(context: Context, layoutId: Int, items: List<T>) :
ArrayAdapter<T>(context, layoutId, items) { ArrayAdapter<T>(context, layoutId, items) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
@@ -1235,8 +1367,12 @@ fun blurImage(imageView: ImageView, banner: String?) {
if (!(context as Activity).isDestroyed) { if (!(context as Activity).isDestroyed) {
val url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { banner } val url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { banner }
Glide.with(context as Context) Glide.with(context as Context)
.load(GlideUrl(url)) .load(
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400) if (banner.startsWith("http")) GlideUrl(url) else if (banner.startsWith("content://")) Uri.parse(
url
) else File(url)
)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE).override(400)
.apply(RequestOptions.bitmapTransform(BlurTransformation(radius, sampling))) .apply(RequestOptions.bitmapTransform(BlurTransformation(radius, sampling)))
.into(imageView) .into(imageView)
} }
@@ -1321,4 +1457,4 @@ fun buildMarkwon(
})) }))
.build() .build()
return markwon return markwon
} }

View File

@@ -5,7 +5,6 @@ import android.annotation.SuppressLint
import android.app.AlertDialog import android.app.AlertDialog
import android.content.Intent import android.content.Intent
import android.content.res.Configuration 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
@@ -14,13 +13,11 @@ 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.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.annotation.OptIn
@@ -36,14 +33,15 @@ 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.common.util.UnstableApi
import androidx.media3.exoplayer.offline.Download
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import androidx.work.OneTimeWorkRequest import androidx.work.OneTimeWorkRequest
import ani.dantotsu.addons.torrent.ServerService
import ani.dantotsu.addons.torrent.TorrentAddonManager
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.DialogUserAgentBinding
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
@@ -56,6 +54,7 @@ import ani.dantotsu.others.CustomBottomDialog
import ani.dantotsu.profile.ProfileActivity import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.activity.FeedActivity import ani.dantotsu.profile.activity.FeedActivity
import ani.dantotsu.profile.activity.NotificationActivity import ani.dantotsu.profile.activity.NotificationActivity
import ani.dantotsu.settings.ExtensionsActivity
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefManager.asLiveBool import ani.dantotsu.settings.saving.PrefManager.asLiveBool
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
@@ -70,11 +69,13 @@ import com.google.android.material.textfield.TextInputEditText
import eu.kanade.domain.source.service.SourcePreferences 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.DelicateCoroutinesApi
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.withContext import kotlinx.coroutines.withContext
import nl.joery.animatedbottombar.AnimatedBottomBar import nl.joery.animatedbottombar.AnimatedBottomBar
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.Serializable import java.io.Serializable
@@ -87,6 +88,7 @@ class MainActivity : AppCompatActivity() {
private var load = false private var load = false
@kotlin.OptIn(DelicateCoroutinesApi::class)
@SuppressLint("InternalInsetResource", "DiscouragedApi") @SuppressLint("InternalInsetResource", "DiscouragedApi")
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -161,16 +163,16 @@ class MainActivity : AppCompatActivity() {
} }
} }
val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar) val bottomNavBar = 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 = bottomNavBar.background as GradientDrawable
val currentColor = backgroundDrawable.color?.defaultColor ?: 0 val currentColor = backgroundDrawable.color?.defaultColor ?: 0
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xF9000000.toInt() val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xF9000000.toInt()
backgroundDrawable.setColor(semiTransparentColor) backgroundDrawable.setColor(semiTransparentColor)
_bottomBar.background = backgroundDrawable bottomNavBar.background = backgroundDrawable
} }
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray) bottomNavBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
val offset = try { val offset = try {
val statusBarHeightId = resources.getIdentifier("status_bar_height", "dimen", "android") val statusBarHeightId = resources.getIdentifier("status_bar_height", "dimen", "android")
@@ -230,17 +232,6 @@ class MainActivity : AppCompatActivity() {
} }
} }
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
lifecycleScope.launch { lifecycleScope.launch {
@@ -284,6 +275,16 @@ class MainActivity : AppCompatActivity() {
binding.root.doOnAttach { binding.root.doOnAttach {
initActivity(this) initActivity(this)
val preferences: SourcePreferences = Injekt.get()
if (preferences.animeExtensionUpdatesCount()
.get() > 0 || preferences.mangaExtensionUpdatesCount().get() > 0
) {
snackString(R.string.extension_updates_available)
?.setDuration(Snackbar.LENGTH_LONG)
?.setAction(R.string.review) {
startActivity(Intent(this, ExtensionsActivity::class.java))
}
}
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent) window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent)
selectedOption = if (fragment != null) { selectedOption = if (fragment != null) {
when (fragment) { when (fragment) {
@@ -300,6 +301,7 @@ class MainActivity : AppCompatActivity() {
} }
} }
var launched = false
intent.extras?.let { extras -> intent.extras?.let { extras ->
val fragmentToLoad = extras.getString("FRAGMENT_TO_LOAD") val fragmentToLoad = extras.getString("FRAGMENT_TO_LOAD")
val mediaId = extras.getInt("mediaId", -1) val mediaId = extras.getInt("mediaId", -1)
@@ -312,6 +314,7 @@ class MainActivity : AppCompatActivity() {
putExtra("mediaId", mediaId) putExtra("mediaId", mediaId)
putExtra("commentId", commentId) putExtra("commentId", commentId)
} }
launched = true
startActivity(detailIntent) startActivity(detailIntent)
} else if (fragmentToLoad == "FEED" && activityId != -1) { } else if (fragmentToLoad == "FEED" && activityId != -1) {
val feedIntent = Intent(this, FeedActivity::class.java).apply { val feedIntent = Intent(this, FeedActivity::class.java).apply {
@@ -319,6 +322,7 @@ class MainActivity : AppCompatActivity() {
putExtra("activityId", activityId) putExtra("activityId", activityId)
} }
launched = true
startActivity(feedIntent) startActivity(feedIntent)
} else if (fragmentToLoad == "NOTIFICATIONS" && activityId != -1) { } else if (fragmentToLoad == "NOTIFICATIONS" && activityId != -1) {
Logger.log("MainActivity, onCreate: $activityId") Logger.log("MainActivity, onCreate: $activityId")
@@ -326,6 +330,7 @@ class MainActivity : AppCompatActivity() {
putExtra("FRAGMENT_TO_LOAD", "NOTIFICATIONS") putExtra("FRAGMENT_TO_LOAD", "NOTIFICATIONS")
putExtra("activityId", activityId) putExtra("activityId", activityId)
} }
launched = true
startActivity(notificationIntent) startActivity(notificationIntent)
} }
} }
@@ -339,7 +344,7 @@ class MainActivity : AppCompatActivity() {
startActivity(Intent(this, NoInternet::class.java)) startActivity(Intent(this, NoInternet::class.java))
} else { } else {
val model: AnilistHomeViewModel by viewModels() val model: AnilistHomeViewModel by viewModels()
model.genres.observe(this) { it -> model.genres.observe(this) {
if (it != null) { if (it != null) {
if (it) { if (it) {
val navbar = binding.includedNavbar.navbar val navbar = binding.includedNavbar.navbar
@@ -364,7 +369,7 @@ class MainActivity : AppCompatActivity() {
mainViewPager.setCurrentItem(newIndex, false) mainViewPager.setCurrentItem(newIndex, false)
} }
}) })
if (mainViewPager.getCurrentItem() != selectedOption) { if (mainViewPager.currentItem != selectedOption) {
navbar.selectTabAt(selectedOption) navbar.selectTabAt(selectedOption)
mainViewPager.post { mainViewPager.post {
mainViewPager.setCurrentItem( mainViewPager.setCurrentItem(
@@ -379,7 +384,7 @@ class MainActivity : AppCompatActivity() {
} }
} }
//Load Data //Load Data
if (!load) { if (!load && !launched) {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
model.loadMain(this@MainActivity) model.loadMain(this@MainActivity)
val id = intent.extras?.getInt("mediaId", 0) val id = intent.extras?.getInt("mediaId", 0)
@@ -450,16 +455,26 @@ class MainActivity : AppCompatActivity() {
} }
} }
} }
lifecycleScope.launch(Dispatchers.IO) { //simple cleanup
val index = Helper.downloadManager(this@MainActivity).downloadIndex val torrentManager = Injekt.get<TorrentAddonManager>()
val downloadCursor = index.getDownloads() fun startTorrent() {
while (downloadCursor.moveToNext()) { if (torrentManager.isAvailable() && PrefManager.getVal(PrefName.TorrentEnabled)) {
val download = downloadCursor.download launchIO {
if (download.state == Download.STATE_FAILED) { if (!ServerService.isRunning()) {
Helper.downloadManager(this@MainActivity).removeDownload(download.request.id) ServerService.start()
}
} }
} }
} }
if (torrentManager.isInitialized.value == false) {
torrentManager.isInitialized.observe(this) {
if (it) {
startTorrent()
}
}
} else {
startTorrent()
}
} }
override fun onRestart() { override fun onRestart() {
@@ -467,34 +482,26 @@ class MainActivity : AppCompatActivity() {
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent) 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) { override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig)
val params : ViewGroup.MarginLayoutParams = val margin = if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) 8 else 32
val params: ViewGroup.MarginLayoutParams =
binding.includedNavbar.navbar.layoutParams as ViewGroup.MarginLayoutParams binding.includedNavbar.navbar.layoutParams as ViewGroup.MarginLayoutParams
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) params.updateMargins(bottom = margin.toPx)
params.updateMargins(bottom = 8.toPx)
else
params.updateMargins(bottom = 32.toPx)
} }
private fun passwordAlertDialog(callback: (CharArray?) -> Unit) { private fun passwordAlertDialog(callback: (CharArray?) -> Unit) {
val password = CharArray(16).apply { fill('0') } val password = CharArray(16).apply { fill('0') }
// Inflate the dialog layout // Inflate the dialog layout
val dialogView = val dialogView = DialogUserAgentBinding.inflate(layoutInflater)
LayoutInflater.from(this).inflate(R.layout.dialog_user_agent, null) dialogView.userAgentTextBox.hint = "Password"
dialogView.findViewById<TextInputEditText>(R.id.userAgentTextBox)?.hint = "Password" dialogView.subtitle.visibility = View.VISIBLE
val subtitleTextView = dialogView.findViewById<TextView>(R.id.subtitle) dialogView.subtitle.text = getString(R.string.enter_password_to_decrypt_file)
subtitleTextView?.visibility = View.VISIBLE
subtitleTextView?.text = "Enter your password to decrypt the file"
val dialog = AlertDialog.Builder(this, R.style.MyPopup) val dialog = AlertDialog.Builder(this, R.style.MyPopup)
.setTitle("Enter Password") .setTitle("Enter Password")
.setView(dialogView) .setView(dialogView.root)
.setPositiveButton("OK", null) .setPositiveButton("OK", null)
.setNegativeButton("Cancel") { dialog, _ -> .setNegativeButton("Cancel") { dialog, _ ->
password.fill('0') password.fill('0')

View File

@@ -1,6 +1,5 @@
package ani.dantotsu package ani.dantotsu
import android.content.Context
import android.os.Build 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
@@ -10,6 +9,7 @@ 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
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkHelper.Companion.defaultUserAgentProvider
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -35,13 +35,13 @@ lateinit var defaultHeaders: Map<String, String>
lateinit var okHttpClient: OkHttpClient lateinit var okHttpClient: OkHttpClient
lateinit var client: Requests lateinit var client: Requests
fun initializeNetwork(context: Context) { fun initializeNetwork() {
val networkHelper = Injekt.get<NetworkHelper>() val networkHelper = Injekt.get<NetworkHelper>()
defaultHeaders = mapOf( defaultHeaders = mapOf(
"User-Agent" to "User-Agent" to
Injekt.get<NetworkHelper>().defaultUserAgentProvider() defaultUserAgentProvider()
.format(Build.VERSION.RELEASE, Build.MODEL) .format(Build.VERSION.RELEASE, Build.MODEL)
) )

View File

@@ -0,0 +1,15 @@
package ani.dantotsu.addons
abstract class Addon {
abstract val name: String
abstract val pkgName: String
abstract val versionName: String
abstract val versionCode: Long
abstract class Installed(
override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Long,
) : Addon()
}

View File

@@ -0,0 +1,128 @@
package ani.dantotsu.addons
import android.app.Activity
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import ani.dantotsu.Mapper
import ani.dantotsu.R
import ani.dantotsu.client
import ani.dantotsu.logError
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.AppUpdater
import ani.dantotsu.settings.InstallerSteps
import ani.dantotsu.toast
import ani.dantotsu.util.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.decodeFromJsonElement
import rx.android.schedulers.AndroidSchedulers
class AddonDownloader {
companion object {
private suspend fun check(repo: String): Pair<String, String> {
return try {
val res = client.get("https://api.github.com/repos/$repo/releases")
.parsed<JsonArray>().map {
Mapper.json.decodeFromJsonElement<AppUpdater.GithubResponse>(it)
}
val r = res.maxByOrNull {
it.timeStamp()
} ?: throw Exception("No Pre Release Found")
val v = r.tagName.substringAfter("v", "")
val md = r.body ?: ""
val version = v.ifEmpty { throw Exception("Weird Version : ${r.tagName}") }
Logger.log("Git Version : $version")
Pair(md, version)
} catch (e: Exception) {
Logger.log("Error checking for update")
Logger.log(e)
Pair("", "")
}
}
suspend fun hasUpdate(repo: String, currentVersion: String): Boolean {
val (_, version) = check(repo)
return compareVersion(version, currentVersion)
}
suspend fun update(
activity: Activity,
manager: AddonManager<*>,
repo: String,
currentVersion: String
) {
val (_, version) = check(repo)
if (!compareVersion(version, currentVersion)) {
toast(activity.getString(R.string.no_update_found))
return
}
MainScope().launch(Dispatchers.IO) {
try {
val apks =
client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
.parsed<AppUpdater.GithubResponse>().assets?.filter {
it.browserDownloadURL.endsWith(
".apk"
)
}
val apkToDownload =
apks?.find { it.browserDownloadURL.contains(getCurrentABI()) }
?: apks?.find { it.browserDownloadURL.contains("universal") }
?: apks?.first()
apkToDownload?.browserDownloadURL.apply {
if (this != null) {
val notificationManager =
activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val installerSteps = InstallerSteps(notificationManager, activity)
manager.install(this)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ installStep -> installerSteps.onInstallStep(installStep) {} },
{ error -> installerSteps.onError(error) {} },
{ installerSteps.onComplete {} }
)
} else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
}
} catch (e: Exception) {
logError(e)
}
}
}
/**
* Returns the ABI that the app is most likely running on.
* @return The primary ABI for the device.
*/
private fun getCurrentABI(): String {
return if (Build.SUPPORTED_ABIS.isNotEmpty()) {
Build.SUPPORTED_ABIS[0]
} else "Unknown"
}
private fun compareVersion(newVersion: String, oldVersion: String): Boolean {
fun toDouble(list: List<String>): Double {
return try {
list.mapIndexed { i: Int, s: String ->
when (i) {
0 -> s.toDouble() * 100
1 -> s.toDouble() * 10
2 -> s.toDouble()
else -> s.toDoubleOrNull() ?: 0.0
}
}.sum()
} catch (e: NumberFormatException) {
0.0
}
}
val new = toDouble(newVersion.split("."))
val curr = toDouble(oldVersion.split("."))
return new > curr
}
}
}

View File

@@ -0,0 +1,131 @@
package ani.dantotsu.addons
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.core.content.ContextCompat
import ani.dantotsu.addons.download.DownloadAddonManager
import ani.dantotsu.addons.torrent.TorrentAddonManager
import ani.dantotsu.media.AddonType
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver.Companion.filter
import eu.kanade.tachiyomi.extension.util.ExtensionInstallReceiver.Companion.getPackageNameFromIntent
import kotlinx.coroutines.DelicateCoroutinesApi
import tachiyomi.core.util.lang.launchNow
internal class AddonInstallReceiver : BroadcastReceiver() {
private var listener: AddonListener? = null
private var type: AddonType? = null
/**
* Registers this broadcast receiver
*/
fun register(context: Context) {
ContextCompat.registerReceiver(context, this, filter, ContextCompat.RECEIVER_EXPORTED)
}
fun setListener(listener: AddonListener, type: AddonType): AddonInstallReceiver {
this.listener = listener
this.type = type
return this
}
/**
* Called when one of the events of the [filter] is received. When the package is an extension,
* it's loaded in background and it notifies the [listener] when finished.
*/
@OptIn(DelicateCoroutinesApi::class)
override fun onReceive(context: Context, intent: Intent?) {
if (intent == null) return
when (intent.action) {
Intent.ACTION_PACKAGE_ADDED -> {
if (ExtensionInstallReceiver.isReplacing(intent)) return
launchNow {
when (type) {
AddonType.DOWNLOAD -> {
getPackageNameFromIntent(intent)?.let { packageName ->
if (packageName != DownloadAddonManager.DOWNLOAD_PACKAGE) return@launchNow
listener?.onAddonInstalled(
AddonLoader.loadFromPkgName(
context,
packageName,
AddonType.DOWNLOAD
)
)
}
}
AddonType.TORRENT -> {
getPackageNameFromIntent(intent)?.let { packageName ->
if (packageName != TorrentAddonManager.TORRENT_PACKAGE) return@launchNow
listener?.onAddonInstalled(
AddonLoader.loadFromPkgName(
context,
packageName,
AddonType.TORRENT
)
)
}
}
else -> {}
}
}
}
Intent.ACTION_PACKAGE_REPLACED -> {
launchNow {
when (type) {
AddonType.DOWNLOAD -> {
getPackageNameFromIntent(intent)?.let { packageName ->
if (packageName != DownloadAddonManager.DOWNLOAD_PACKAGE) return@launchNow
listener?.onAddonUpdated(
AddonLoader.loadFromPkgName(
context,
packageName,
AddonType.DOWNLOAD
)
)
}
}
AddonType.TORRENT -> {
getPackageNameFromIntent(intent)?.let { packageName ->
if (packageName != TorrentAddonManager.TORRENT_PACKAGE) return@launchNow
listener?.onAddonUpdated(
AddonLoader.loadFromPkgName(
context,
packageName,
AddonType.TORRENT
)
)
}
}
else -> {}
}
}
}
Intent.ACTION_PACKAGE_REMOVED -> {
if (ExtensionInstallReceiver.isReplacing(intent)) return
getPackageNameFromIntent(intent)?.let { packageName ->
when (type) {
AddonType.DOWNLOAD -> {
if (packageName != DownloadAddonManager.DOWNLOAD_PACKAGE) return
listener?.onAddonUninstalled(packageName)
}
AddonType.TORRENT -> {
if (packageName != TorrentAddonManager.TORRENT_PACKAGE) return
listener?.onAddonUninstalled(packageName)
}
else -> {}
}
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
package ani.dantotsu.addons
interface AddonListener {
fun onAddonInstalled(result: LoadResult?)
fun onAddonUpdated(result: LoadResult?)
fun onAddonUninstalled(pkgName: String)
enum class ListenerAction {
INSTALL, UPDATE, UNINSTALL
}
}

View File

@@ -0,0 +1,143 @@
package ani.dantotsu.addons
import android.content.Context
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.pm.PackageInfoCompat
import ani.dantotsu.addons.download.DownloadAddon
import ani.dantotsu.addons.download.DownloadAddonApi
import ani.dantotsu.addons.download.DownloadAddonManager
import ani.dantotsu.addons.download.DownloadLoadResult
import ani.dantotsu.addons.torrent.TorrentAddon
import ani.dantotsu.addons.torrent.TorrentAddonApi
import ani.dantotsu.addons.torrent.TorrentAddonManager
import ani.dantotsu.addons.torrent.TorrentLoadResult
import ani.dantotsu.media.AddonType
import ani.dantotsu.util.Logger
import dalvik.system.PathClassLoader
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import eu.kanade.tachiyomi.util.system.getApplicationIcon
class AddonLoader {
companion object {
fun loadExtension(
context: Context,
packageName: String,
className: String,
type: AddonType
): LoadResult? {
val pkgManager = context.packageManager
val installedPkgs = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
pkgManager.getInstalledPackages(PackageManager.PackageInfoFlags.of(ExtensionLoader.PACKAGE_FLAGS.toLong()))
} else {
pkgManager.getInstalledPackages(ExtensionLoader.PACKAGE_FLAGS)
}
val extPkgs = installedPkgs.filter {
isPackageAnExtension(
packageName,
it
)
}
if (extPkgs.isEmpty()) return null
if (extPkgs.size > 1) throw IllegalStateException("Multiple extensions with the same package name found")
val pkgName = extPkgs.first().packageName
val pkgInfo = extPkgs.first()
val appInfo = try {
pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
} catch (error: PackageManager.NameNotFoundException) {
// Unlikely, but the package may have been uninstalled at this point
Logger.log(error)
throw error
}
val extName =
pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Dantotsu: ")
val versionName = pkgInfo.versionName
val versionCode = PackageInfoCompat.getLongVersionCode(pkgInfo)
if (versionName.isNullOrEmpty()) {
Logger.log("Missing versionName for extension $extName")
throw IllegalStateException("Missing versionName for extension $extName")
}
val classLoader =
PathClassLoader(appInfo.sourceDir, appInfo.nativeLibraryDir, context.classLoader)
val loadedClass = try {
Class.forName(className, false, classLoader)
} catch (e: ClassNotFoundException) {
Logger.log("Extension load error: $extName ($className)")
Logger.log(e)
throw e
} catch (e: NoClassDefFoundError) {
Logger.log("Extension load error: $extName ($className)")
Logger.log(e)
throw e
} catch (e: Exception) {
Logger.log("Extension load error: $extName ($className)")
Logger.log(e)
throw e
}
val instance = loadedClass.getDeclaredConstructor().newInstance()
return when (type) {
AddonType.TORRENT -> {
val extension = instance as? TorrentAddonApi
?: throw IllegalStateException("Extension is not a TorrentAddonApi")
TorrentLoadResult.Success(
TorrentAddon.Installed(
name = extName,
pkgName = pkgName,
versionName = versionName,
versionCode = versionCode,
extension = extension,
icon = context.getApplicationIcon(pkgName),
)
)
}
AddonType.DOWNLOAD -> {
val extension = instance as? DownloadAddonApi
?: throw IllegalStateException("Extension is not a DownloadAddonApi")
DownloadLoadResult.Success(
DownloadAddon.Installed(
name = extName,
pkgName = pkgName,
versionName = versionName,
versionCode = versionCode,
extension = extension,
icon = context.getApplicationIcon(pkgName),
)
)
}
}
}
fun loadFromPkgName(context: Context, packageName: String, type: AddonType): LoadResult? {
return when (type) {
AddonType.TORRENT -> loadExtension(
context,
packageName,
TorrentAddonManager.TORRENT_CLASS,
type
)
AddonType.DOWNLOAD -> loadExtension(
context,
packageName,
DownloadAddonManager.DOWNLOAD_CLASS,
type
)
}
}
private fun isPackageAnExtension(type: String, pkgInfo: PackageInfo): Boolean {
return pkgInfo.packageName.equals(type)
}
}
}

View File

@@ -0,0 +1,46 @@
package ani.dantotsu.addons
import android.content.Context
import ani.dantotsu.media.AddonType
import eu.kanade.tachiyomi.extension.InstallStep
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
import rx.Observable
abstract class AddonManager<T : Addon.Installed>(
private val context: Context
) {
abstract var extension: T?
abstract var name: String
abstract var type: AddonType
protected val installer by lazy { ExtensionInstaller(context) }
var hasUpdate: Boolean = false
protected set
protected var onListenerAction: ((AddonListener.ListenerAction) -> Unit)? = null
abstract suspend fun init()
abstract fun isAvailable(): Boolean
abstract fun getVersion(): String?
abstract fun getPackageName(): String?
abstract fun hadError(context: Context): String?
abstract fun updateInstallStep(id: Long, step: InstallStep)
abstract fun setInstalling(id: Long)
fun uninstall() {
getPackageName()?.let {
installer.uninstallApk(it)
}
}
fun addListenerAction(action: (AddonListener.ListenerAction) -> Unit) {
onListenerAction = action
}
fun removeListenerAction() {
onListenerAction = null
}
fun install(url: String): Observable<InstallStep> {
return installer.downloadAndInstall(url, getPackageName() ?: "", name, type)
}
}

View File

@@ -0,0 +1,8 @@
package ani.dantotsu.addons
abstract class LoadResult {
abstract class Success : LoadResult()
}

View File

@@ -0,0 +1,18 @@
package ani.dantotsu.addons.download
import android.graphics.drawable.Drawable
import ani.dantotsu.addons.Addon
sealed class DownloadAddon : Addon() {
data class Installed(
override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Long,
val extension: DownloadAddonApi,
val icon: Drawable?,
val hasUpdate: Boolean = false,
) : Addon.Installed(name, pkgName, versionName, versionCode)
}

View File

@@ -0,0 +1,21 @@
package ani.dantotsu.addons.download
import android.content.Context
import android.net.Uri
interface DownloadAddonApi {
fun cancelDownload(sessionId: Long)
fun setDownloadPath(context: Context, uri: Uri): String
suspend fun executeFFProbe(request: String, logCallback: (String) -> Unit)
suspend fun executeFFMpeg(request: String, statCallback: (Double) -> Unit): Long
fun getState(sessionId: Long): String
fun getStackTrace(sessionId: Long): String?
fun hadError(sessionId: Long): Boolean
}

View File

@@ -0,0 +1,134 @@
package ani.dantotsu.addons.download
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import ani.dantotsu.R
import ani.dantotsu.addons.AddonDownloader
import ani.dantotsu.addons.AddonInstallReceiver
import ani.dantotsu.addons.AddonListener
import ani.dantotsu.addons.AddonLoader
import ani.dantotsu.addons.AddonManager
import ani.dantotsu.addons.LoadResult
import ani.dantotsu.media.AddonType
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.extension.InstallStep
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class DownloadAddonManager(
private val context: Context
) : AddonManager<DownloadAddon.Installed>(context) {
override var extension: DownloadAddon.Installed? = null
override var name: String = "Download Addon"
override var type = AddonType.DOWNLOAD
private val _isInitialized = MutableLiveData<Boolean>().apply { value = false }
val isInitialized: LiveData<Boolean> = _isInitialized
private var error: String? = null
override suspend fun init() {
extension = null
error = null
hasUpdate = false
withContext(Dispatchers.Main) {
_isInitialized.value = false
}
AddonInstallReceiver()
.setListener(InstallationListener(), type)
.register(context)
try {
val result = AddonLoader.loadExtension(
context,
DOWNLOAD_PACKAGE,
DOWNLOAD_CLASS,
AddonType.DOWNLOAD
) as? DownloadLoadResult
result?.let {
if (it is DownloadLoadResult.Success) {
extension = it.extension
hasUpdate = AddonDownloader.hasUpdate(REPO, it.extension.versionName)
}
}
withContext(Dispatchers.Main) {
_isInitialized.value = true
}
} catch (e: Exception) {
Logger.log("Error initializing Download extension")
Logger.log(e)
error = e.message
}
}
override fun isAvailable(): Boolean {
return extension?.extension != null
}
override fun getVersion(): String? {
return extension?.versionName
}
override fun getPackageName(): String? {
return extension?.pkgName
}
override fun hadError(context: Context): String? {
return if (isInitialized.value == true) {
if (error != null) {
error
} else if (extension != null) {
context.getString(R.string.loaded_successfully)
} else {
null
}
} else {
null
}
}
private inner class InstallationListener : AddonListener {
override fun onAddonInstalled(result: LoadResult?) {
if (result is DownloadLoadResult.Success) {
extension = result.extension
hasUpdate = false
onListenerAction?.invoke(AddonListener.ListenerAction.INSTALL)
}
}
override fun onAddonUpdated(result: LoadResult?) {
if (result is DownloadLoadResult.Success) {
extension = result.extension
hasUpdate = false
onListenerAction?.invoke(AddonListener.ListenerAction.UPDATE)
}
}
override fun onAddonUninstalled(pkgName: String) {
if (extension?.pkgName == pkgName) {
extension = null
hasUpdate = false
onListenerAction?.invoke(AddonListener.ListenerAction.UNINSTALL)
}
}
}
override fun updateInstallStep(id: Long, step: InstallStep) {
installer.updateInstallStep(id, step)
}
override fun setInstalling(id: Long) {
installer.updateInstallStep(id, InstallStep.Installing)
}
companion object {
const val DOWNLOAD_PACKAGE = "dantotsu.downloadAddon"
const val DOWNLOAD_CLASS = "ani.dantotsu.downloadAddon.DownloadAddon"
const val REPO = "rebelonion/Dantotsu-Download-Addon"
}
}

View File

@@ -0,0 +1,7 @@
package ani.dantotsu.addons.download
import ani.dantotsu.addons.LoadResult
open class DownloadLoadResult : LoadResult() {
class Success(val extension: DownloadAddon.Installed) : DownloadLoadResult()
}

View File

@@ -0,0 +1,16 @@
package ani.dantotsu.addons.torrent
import android.graphics.drawable.Drawable
import ani.dantotsu.addons.Addon
sealed class TorrentAddon : Addon() {
data class Installed(
override val name: String,
override val pkgName: String,
override val versionName: String,
override val versionCode: Long,
val extension: TorrentAddonApi,
val icon: Drawable?,
val hasUpdate: Boolean = false,
) : Addon.Installed(name, pkgName, versionName, versionCode)
}

View File

@@ -0,0 +1,24 @@
package ani.dantotsu.addons.torrent
import eu.kanade.tachiyomi.data.torrentServer.model.Torrent
interface TorrentAddonApi {
fun startServer(path: String)
fun stopServer()
fun echo(): String
fun removeTorrent(torrent: String)
fun addTorrent(
link: String,
title: String,
poster: String,
data: String,
save: Boolean,
): Torrent
fun getLink(torrent: Torrent, index: Int): String
}

View File

@@ -0,0 +1,137 @@
package ani.dantotsu.addons.torrent
import android.content.Context
import android.os.Build
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import ani.dantotsu.R
import ani.dantotsu.addons.AddonDownloader.Companion.hasUpdate
import ani.dantotsu.addons.AddonListener
import ani.dantotsu.addons.AddonLoader
import ani.dantotsu.addons.AddonManager
import ani.dantotsu.addons.LoadResult
import ani.dantotsu.addons.AddonInstallReceiver
import ani.dantotsu.media.AddonType
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.extension.InstallStep
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class TorrentAddonManager(
private val context: Context
) : AddonManager<TorrentAddon.Installed>(context) {
override var extension: TorrentAddon.Installed? = null
override var name: String = "Torrent Addon"
override var type: AddonType = AddonType.TORRENT
var torrentHash: String? = null
private val _isInitialized = MutableLiveData<Boolean>().apply { value = false }
val isInitialized: LiveData<Boolean> = _isInitialized
private var error: String? = null
override suspend fun init() {
extension = null
error = null
hasUpdate = false
withContext(Dispatchers.Main) {
_isInitialized.value = false
}
if (Build.VERSION.SDK_INT < 23) {
Logger.log("Torrent extension is not supported on this device.")
error = context.getString(R.string.torrent_extension_not_supported)
return
}
AddonInstallReceiver()
.setListener(InstallationListener(), type)
.register(context)
try {
val result = AddonLoader.loadExtension(
context,
TORRENT_PACKAGE,
TORRENT_CLASS,
type
) as TorrentLoadResult?
result?.let {
if (it is TorrentLoadResult.Success) {
extension = it.extension
hasUpdate = hasUpdate(REPO, it.extension.versionName)
}
}
withContext(Dispatchers.Main) {
_isInitialized.value = true
}
} catch (e: Exception) {
Logger.log("Error initializing torrent extension")
Logger.log(e)
error = e.message
}
}
override fun isAvailable(): Boolean {
return extension?.extension != null
}
override fun getVersion(): String? {
return extension?.versionName
}
override fun getPackageName(): String? {
return extension?.pkgName
}
override fun hadError(context: Context): String? {
return if (isInitialized.value == true) {
if (error != null) {
error
} else if (extension != null) {
context.getString(R.string.loaded_successfully)
} else {
null
}
} else {
null
}
}
private inner class InstallationListener : AddonListener {
override fun onAddonInstalled(result: LoadResult?) {
if (result is TorrentLoadResult.Success) {
extension = result.extension
hasUpdate = false
onListenerAction?.invoke(AddonListener.ListenerAction.INSTALL)
}
}
override fun onAddonUpdated(result: LoadResult?) {
if (result is TorrentLoadResult.Success) {
extension = result.extension
hasUpdate = false
onListenerAction?.invoke(AddonListener.ListenerAction.UPDATE)
}
}
override fun onAddonUninstalled(pkgName: String) {
if (pkgName == TORRENT_PACKAGE) {
extension = null
hasUpdate = false
onListenerAction?.invoke(AddonListener.ListenerAction.UNINSTALL)
}
}
}
override fun updateInstallStep(id: Long, step: InstallStep) {
installer.updateInstallStep(id, step)
}
override fun setInstalling(id: Long) {
installer.updateInstallStep(id, InstallStep.Installing)
}
companion object {
const val TORRENT_PACKAGE = "dantotsu.torrentAddon"
const val TORRENT_CLASS = "ani.dantotsu.torrentAddon.TorrentAddon"
const val REPO = "rebelonion/Dantotsu-Torrent-Addon"
}
}

View File

@@ -0,0 +1,7 @@
package ani.dantotsu.addons.torrent
import ani.dantotsu.addons.LoadResult
open class TorrentLoadResult : LoadResult() {
class Success(val extension: TorrentAddon.Installed) : TorrentLoadResult()
}

View File

@@ -0,0 +1,168 @@
package ani.dantotsu.addons.torrent
import android.app.ActivityManager
import android.app.Application
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import ani.dantotsu.R
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_TORRENT_SERVER
import eu.kanade.tachiyomi.data.notification.Notifications.ID_TORRENT_SERVER
import eu.kanade.tachiyomi.util.system.cancelNotification
import eu.kanade.tachiyomi.util.system.notificationBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.coroutines.EmptyCoroutineContext
class ServerService : Service() {
private val serviceScope = CoroutineScope(EmptyCoroutineContext)
private val applicationContext = Injekt.get<Application>()
private val extension = Injekt.get<TorrentAddonManager>().extension!!.extension
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(
intent: Intent?,
flags: Int,
startId: Int,
): Int {
intent?.let {
if (it.action != null) {
when (it.action) {
ACTION_START -> {
startServer()
notification(applicationContext)
return START_STICKY
}
ACTION_STOP -> {
stopServer()
return START_NOT_STICKY
}
}
}
}
return START_NOT_STICKY
}
private fun startServer() {
serviceScope.launch {
val echo = extension.echo()
if (echo == "") {
extension.startServer(filesDir.absolutePath)
}
}
}
private fun stopServer() {
serviceScope.launch {
extension.stopServer()
applicationContext.cancelNotification(ID_TORRENT_SERVER)
stopSelf()
}
}
private fun notification(context: Context) {
val exitPendingIntent =
PendingIntent.getService(
applicationContext,
0,
Intent(applicationContext, ServerService::class.java).apply {
action = ACTION_STOP
},
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
val builder = context.notificationBuilder(CHANNEL_TORRENT_SERVER) {
setSmallIcon(R.drawable.notification_icon)
setContentText("Torrent Server")
setContentTitle("Server is running…")
setAutoCancel(false)
setOngoing(true)
setUsesChronometer(true)
addAction(
R.drawable.ic_circle_cancel,
"Stop",
exitPendingIntent,
)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(
ID_TORRENT_SERVER,
builder.build(),
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
)
} else {
startForeground(ID_TORRENT_SERVER, builder.build())
}
}
companion object {
const val ACTION_START = "start_torrent_server"
const val ACTION_STOP = "stop_torrent_server"
fun isRunning(): Boolean {
with(Injekt.get<Application>().getSystemService(ACTIVITY_SERVICE) as ActivityManager) {
@Suppress("DEPRECATION") // We only need our services
getRunningServices(Int.MAX_VALUE).forEach {
if (ServerService::class.java.name.equals(it.service.className)) {
return true
}
}
}
return false
}
fun start() {
try {
val intent =
Intent(Injekt.get<Application>(), ServerService::class.java).apply {
action = ACTION_START
}
Injekt.get<Application>().startService(intent)
} catch (e: Exception) {
e.printStackTrace()
}
}
fun stop() {
try {
val intent =
Intent(Injekt.get<Application>(), ServerService::class.java).apply {
action = ACTION_STOP
}
Injekt.get<Application>().startService(intent)
} catch (e: Exception) {
e.printStackTrace()
}
}
fun wait(timeout: Int = -1): Boolean {
var count = 0
if (timeout < 0) {
count = -20
}
var echo = Injekt.get<TorrentAddonManager>().extension?.extension?.echo()
while (echo == "") {
Thread.sleep(1000)
count++
if (count > timeout) {
return false
}
echo = Injekt.get<TorrentAddonManager>().extension?.extension?.echo()
}
Logger.log("ServerService: Server started: $echo")
return true
}
}
}

View File

@@ -6,6 +6,8 @@ import androidx.annotation.OptIn
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.database.StandaloneDatabaseProvider import androidx.media3.database.StandaloneDatabaseProvider
import ani.dantotsu.addons.download.DownloadAddonManager
import ani.dantotsu.addons.torrent.TorrentAddonManager
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface 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
@@ -18,6 +20,7 @@ import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
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.ExperimentalSerializationApi
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
import tachiyomi.domain.source.anime.service.AnimeSourceManager import tachiyomi.domain.source.anime.service.AnimeSourceManager
@@ -29,6 +32,7 @@ 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 {
@kotlin.OptIn(ExperimentalSerializationApi::class)
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
override fun InjektRegistrar.registerInjectables() { override fun InjektRegistrar.registerInjectables() {
addSingleton(app) addSingleton(app)
@@ -36,10 +40,13 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { DownloadsManager(app) } addSingletonFactory { DownloadsManager(app) }
addSingletonFactory { NetworkHelper(app) } addSingletonFactory { NetworkHelper(app) }
addSingletonFactory { NetworkHelper(app).client }
addSingletonFactory { AnimeExtensionManager(app) } addSingletonFactory { AnimeExtensionManager(app) }
addSingletonFactory { MangaExtensionManager(app) } addSingletonFactory { MangaExtensionManager(app) }
addSingletonFactory { NovelExtensionManager(app) } addSingletonFactory { NovelExtensionManager(app) }
addSingletonFactory { TorrentAddonManager(app) }
addSingletonFactory { DownloadAddonManager(app) }
addSingletonFactory<AnimeSourceManager> { AndroidAnimeSourceManager(app, get()) } addSingletonFactory<AnimeSourceManager> { AndroidAnimeSourceManager(app, get()) }
addSingletonFactory<MangaSourceManager> { AndroidMangaSourceManager(app, get()) } addSingletonFactory<MangaSourceManager> { AndroidMangaSourceManager(app, get()) }

View File

@@ -3,17 +3,16 @@ 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.connections.comments.CommentsAPI
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.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.toast import ani.dantotsu.toast
import ani.dantotsu.tryWithSuspend
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import java.util.Calendar import java.util.Calendar
@@ -40,20 +39,54 @@ object Anilist {
"SCORE_DESC", "SCORE_DESC",
"POPULARITY_DESC", "POPULARITY_DESC",
"TRENDING_DESC", "TRENDING_DESC",
"START_DATE_DESC",
"TITLE_ENGLISH", "TITLE_ENGLISH",
"TITLE_ENGLISH_DESC", "TITLE_ENGLISH_DESC",
"SCORE" "SCORE"
) )
val source = listOf(
"ORIGINAL",
"MANGA",
"LIGHT NOVEL",
"VISUAL NOVEL",
"VIDEO GAME",
"OTHER",
"NOVEL",
"DOUJINSHI",
"ANIME",
"WEB NOVEL",
"LIVE ACTION",
"GAME",
"COMIC",
"MULTIMEDIA PROJECT",
"PICTURE BOOK"
)
val animeStatus = listOf(
"FINISHED",
"RELEASING",
"NOT YET RELEASED",
"CANCELLED"
)
val mangaStatus = listOf(
"FINISHED",
"RELEASING",
"NOT YET RELEASED",
"HIATUS",
"CANCELLED"
)
val seasons = listOf( val seasons = listOf(
"WINTER", "SPRING", "SUMMER", "FALL" "WINTER", "SPRING", "SUMMER", "FALL"
) )
val anime_formats = listOf( val animeFormats = listOf(
"TV", "TV SHORT", "MOVIE", "SPECIAL", "OVA", "ONA", "MUSIC" "TV", "TV SHORT", "MOVIE", "SPECIAL", "OVA", "ONA", "MUSIC"
) )
val manga_formats = listOf( val mangaFormats = listOf(
"MANGA", "NOVEL", "ONE SHOT" "MANGA", "NOVEL", "ONE SHOT"
) )
@@ -117,6 +150,9 @@ object Anilist {
episodesWatched = null episodesWatched = null
chapterRead = null chapterRead = null
PrefManager.removeVal(PrefName.AnilistToken) PrefManager.removeVal(PrefName.AnilistToken)
//logout from comments api
CommentsAPI.logout()
} }
suspend inline fun <reified T : Any> executeQuery( suspend inline fun <reified T : Any> executeQuery(
@@ -163,7 +199,9 @@ object Anilist {
toast("Rate limited. Try after $retry seconds") toast("Rate limited. Try after $retry seconds")
throw Exception("Rate limited after $retry seconds") throw Exception("Rate limited after $retry seconds")
} }
if (!json.text.startsWith("{")) {throw Exception(currContext()?.getString(R.string.anilist_down))} if (!json.text.startsWith("{")) {
throw Exception(currContext()?.getString(R.string.anilist_down))
}
if (show) Logger.log("Anilist Response: ${json.text}") if (show) Logger.log("Anilist Response: ${json.text}")
json.parsed() json.parsed()
} else null } else null

View File

@@ -20,6 +20,7 @@ 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.profile.User
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
@@ -72,18 +73,19 @@ 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}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}}}""" """{Media(id:${media.id}){id favourites popularity episodes chapters 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 voiceActors { id name { first middle last full native userPreferred } image { large medium } languageV2 } 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}}Page(page:1){pageInfo{total perPage currentPage lastPage hasNextPage}mediaList(isFollowing:true,sort:[STATUS],mediaId:${media.id}){id status score(format: POINT_100) progress progressVolumes user{id name avatar{large medium}}}}}"""
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)
if (response != null) { if (response != null) {
fun parse() { fun parse() {
val fetchedMedia = response?.data?.media ?: return val fetchedMedia = response?.data?.media ?: return
val user = response?.data?.page
media.source = fetchedMedia.source?.toString() media.source = fetchedMedia.source?.toString()
media.countryOfOrigin = fetchedMedia.countryOfOrigin media.countryOfOrigin = fetchedMedia.countryOfOrigin
media.format = fetchedMedia.format?.toString() media.format = fetchedMedia.format?.toString()
media.favourites = fetchedMedia.favourites
media.popularity = fetchedMedia.popularity
media.startDate = fetchedMedia.startDate media.startDate = fetchedMedia.startDate
media.endDate = fetchedMedia.endDate media.endDate = fetchedMedia.endDate
@@ -138,7 +140,15 @@ class AnilistQueries {
?: "SUPPORTING" ?: "SUPPORTING"
else -> i.role.toString() else -> i.role.toString()
} },
voiceActor = i.voiceActors?.map {
Author(
id = it.id,
name = it.name?.userPreferred,
image = it.image?.large,
role = it.languageV2
)
} as ArrayList<Author>
) )
) )
} }
@@ -152,7 +162,7 @@ class AnilistQueries {
Author( Author(
id = id, id = id,
name = i.node?.name?.userPreferred, name = i.node?.name?.userPreferred,
image = i.node?.image?.medium, image = i.node?.image?.large,
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"
@@ -199,7 +209,24 @@ class AnilistQueries {
} }
} }
} }
if (user?.mediaList?.isNotEmpty() == true) {
media.users = user.mediaList?.mapNotNull {
it.user?.let { user ->
if (user.id != Anilist.userid) {
User(
user.id,
user.name ?: "Unknown",
user.avatar?.large,
"",
it.status?.toString(),
it.score,
it.progress,
fetchedMedia.episodes ?: fetchedMedia.chapters,
)
} else null
}
}?.toCollection(arrayListOf()) ?: arrayListOf()
}
if (fetchedMedia.mediaListEntry != null) { if (fetchedMedia.mediaListEntry != null) {
fetchedMedia.mediaListEntry?.apply { fetchedMedia.mediaListEntry?.apply {
media.userProgress = progress media.userProgress = progress
@@ -386,6 +413,7 @@ class AnilistQueries {
returnArray.addAll(map.values) returnArray.addAll(map.values)
return returnArray return returnArray
} }
@Suppress("UNCHECKED_CAST")
val list = PrefManager.getNullableCustomVal( val list = PrefManager.getNullableCustomVal(
"continueAnimeList", "continueAnimeList",
listOf<Int>(), listOf<Int>(),
@@ -543,6 +571,7 @@ class AnilistQueries {
returnMap["current$type"] = returnArray returnMap["current$type"] = returnArray
return return
} }
@Suppress("UNCHECKED_CAST")
val list = PrefManager.getNullableCustomVal( val list = PrefManager.getNullableCustomVal(
"continueAnimeList", "continueAnimeList",
listOf<Int>(), listOf<Int>(),
@@ -572,6 +601,7 @@ class AnilistQueries {
subMap[m.id] = m subMap[m.id] = m
} }
} }
@Suppress("UNCHECKED_CAST")
val list = PrefManager.getNullableCustomVal( val list = PrefManager.getNullableCustomVal(
"continueAnimeList", "continueAnimeList",
listOf<Int>(), listOf<Int>(),
@@ -733,7 +763,7 @@ class AnilistQueries {
} }
sorted["All"] = all sorted["All"] = all
val listSort: String = if (anime) PrefManager.getVal(PrefName.AnimeListSortOrder) val listSort: String? = if (anime) PrefManager.getVal(PrefName.AnimeListSortOrder)
else PrefManager.getVal(PrefName.MangaListSortOrder) else PrefManager.getVal(PrefName.MangaListSortOrder)
val sort = listSort ?: sortOrder ?: options?.rowOrder val sort = listSort ?: sortOrder ?: options?.rowOrder
for (i in sorted.keys) { for (i in sorted.keys) {
@@ -881,18 +911,23 @@ class AnilistQueries {
sort: String? = null, sort: String? = null,
genres: MutableList<String>? = null, genres: MutableList<String>? = null,
tags: MutableList<String>? = null, tags: MutableList<String>? = null,
status: String? = null,
source: String? = null,
format: String? = null, format: String? = null,
countryOfOrigin: String? = null,
isAdult: Boolean = false, isAdult: Boolean = false,
onList: Boolean? = null, onList: Boolean? = null,
excludedGenres: MutableList<String>? = null, excludedGenres: MutableList<String>? = null,
excludedTags: MutableList<String>? = null, excludedTags: MutableList<String>? = null,
startYear: Int? = null,
seasonYear: Int? = null, seasonYear: Int? = null,
season: String? = null, season: String? = null,
id: Int? = null, id: Int? = null,
hd: Boolean = false, hd: Boolean = false,
adultOnly: Boolean = false
): SearchResults? { ): SearchResults? {
val query = """ val query = """
query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: Boolean = false, ${"$"}search: String, ${"$"}format: [MediaFormat], ${"$"}status: MediaStatus, ${"$"}countryOfOrigin: CountryCode, ${"$"}source: MediaSource, ${"$"}season: MediaSeason, ${"$"}seasonYear: Int, ${"$"}year: String, ${"$"}onList: Boolean, ${"$"}yearLesser: FuzzyDateInt, ${"$"}yearGreater: FuzzyDateInt, ${"$"}episodeLesser: Int, ${"$"}episodeGreater: Int, ${"$"}durationLesser: Int, ${"$"}durationGreater: Int, ${"$"}chapterLesser: Int, ${"$"}chapterGreater: Int, ${"$"}volumeLesser: Int, ${"$"}volumeGreater: Int, ${"$"}licensedBy: [String], ${"$"}isLicensed: Boolean, ${"$"}genres: [String], ${"$"}excludedGenres: [String], ${"$"}tags: [String], ${"$"}excludedTags: [String], ${"$"}minimumTagRank: Int, ${"$"}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC]) { query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: Boolean = false, ${"$"}search: String, ${"$"}format: [MediaFormat], ${"$"}status: MediaStatus, ${"$"}countryOfOrigin: CountryCode, ${"$"}source: MediaSource, ${"$"}season: MediaSeason, ${"$"}seasonYear: Int, ${"$"}year: String, ${"$"}onList: Boolean, ${"$"}yearLesser: FuzzyDateInt, ${"$"}yearGreater: FuzzyDateInt, ${"$"}episodeLesser: Int, ${"$"}episodeGreater: Int, ${"$"}durationLesser: Int, ${"$"}durationGreater: Int, ${"$"}chapterLesser: Int, ${"$"}chapterGreater: Int, ${"$"}volumeLesser: Int, ${"$"}volumeGreater: Int, ${"$"}licensedBy: [String], ${"$"}isLicensed: Boolean, ${"$"}genres: [String], ${"$"}excludedGenres: [String], ${"$"}tags: [String], ${"$"}excludedTags: [String], ${"$"}minimumTagRank: Int, ${"$"}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC, START_DATE_DESC]) {
Page(page: ${"$"}page, perPage: ${perPage ?: 50}) { Page(page: ${"$"}page, perPage: ${perPage ?: 50}) {
pageInfo { pageInfo {
total total
@@ -937,14 +972,19 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
} }
""".replace("\n", " ").replace(""" """, "") """.replace("\n", " ").replace(""" """, "")
val variables = """{"type":"$type","isAdult":$isAdult val variables = """{"type":"$type","isAdult":$isAdult
${if (adultOnly) ""","isAdult":true""" else ""}
${if (onList != null) ""","onList":$onList""" else ""} ${if (onList != null) ""","onList":$onList""" else ""}
${if (page != null) ""","page":"$page"""" else ""} ${if (page != null) ""","page":"$page"""" else ""}
${if (id != null) ""","id":"$id"""" else ""} ${if (id != null) ""","id":"$id"""" else ""}
${if (seasonYear != null) ""","seasonYear":"$seasonYear"""" else ""} ${if (type == "ANIME" && seasonYear != null) ""","seasonYear":"$seasonYear"""" else ""}
${if (type == "MANGA" && startYear != null) ""","yearGreater":${startYear}0000,"yearLesser":${startYear + 1}0000""" else ""}
${if (season != null) ""","season":"$season"""" else ""} ${if (season != null) ""","season":"$season"""" else ""}
${if (search != null) ""","search":"$search"""" else ""} ${if (search != null) ""","search":"$search"""" else ""}
${if (source != null) ""","source":"$source"""" else ""}
${if (sort != null) ""","sort":"$sort"""" else ""} ${if (sort != null) ""","sort":"$sort"""" else ""}
${if (status != null) ""","status":"$status"""" else ""}
${if (format != null) ""","format":"${format.replace(" ", "_")}"""" else ""} ${if (format != null) ""","format":"${format.replace(" ", "_")}"""" else ""}
${if (countryOfOrigin != null) ""","countryOfOrigin":"$countryOfOrigin"""" else ""}
${if (genres?.isNotEmpty() == true) ""","genres":[${genres.joinToString { "\"$it\"" }}]""" else ""} ${if (genres?.isNotEmpty() == true) ""","genres":[${genres.joinToString { "\"$it\"" }}]""" else ""}
${ ${
if (excludedGenres?.isNotEmpty() == true) if (excludedGenres?.isNotEmpty() == true)
@@ -976,7 +1016,6 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
else "" else ""
} }
}""".replace("\n", " ").replace(""" """, "") }""".replace("\n", " ").replace(""" """, "")
val response = executeQuery<Query.Page>(query, variables, true)?.data?.page val response = executeQuery<Query.Page>(query, variables, true)?.data?.page
if (response?.media != null) { if (response?.media != null) {
val responseArray = arrayListOf<Media>() val responseArray = arrayListOf<Media>()
@@ -1008,7 +1047,11 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
excludedGenres = excludedGenres, excludedGenres = excludedGenres,
tags = tags, tags = tags,
excludedTags = excludedTags, excludedTags = excludedTags,
status = status,
source = source,
format = format, format = format,
countryOfOrigin = countryOfOrigin,
startYear = startYear,
seasonYear = seasonYear, seasonYear = seasonYear,
season = season, season = season,
results = responseArray, results = responseArray,
@@ -1019,11 +1062,156 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
return null return null
} }
private val onListAnime =
(if (PrefManager.getVal(PrefName.IncludeAnimeList)) "" else "onList:false").replace(
"\"",
""
)
private val isAdult =
(if (PrefManager.getVal(PrefName.AdultOnly)) "isAdult:true" else "").replace("\"", "")
private fun recentAnimeUpdates(page: Int): String {
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}airingSchedules(airingAt_greater:0 airingAt_lesser:${System.currentTimeMillis() / 1000 - 10000} sort:TIME_DESC){episode airingAt media{id idMal status chapters episodes nextAiringEpisode{episode} isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large} title{english romaji userPreferred} mediaListEntry{progress private score(format:POINT_100) status}}}}"""
}
private fun trendingMovies(page: Int): String {
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: ANIME, format: MOVIE, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun topRatedAnime(page: Int): String {
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort: SCORE_DESC, type: ANIME, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun mostFavAnime(page: Int): String {
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:FAVOURITES_DESC,type: ANIME, $onListAnime, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
suspend fun loadAnimeList(): Map<String, ArrayList<Media>> {
val list = mutableMapOf<String, ArrayList<Media>>()
fun query(): String {
return """{
recentUpdates:${recentAnimeUpdates(1)}
recentUpdates2:${recentAnimeUpdates(2)}
trendingMovies:${trendingMovies(1)}
trendingMovies2:${trendingMovies(2)}
topRated:${topRatedAnime(1)}
topRated2:${topRatedAnime(2)}
mostFav:${mostFavAnime(1)}
mostFav2:${mostFavAnime(2)}
}""".trimIndent()
}
executeQuery<Query.AnimeList>(query(), force = true)?.data?.apply {
val listOnly: Boolean = PrefManager.getVal(PrefName.RecentlyListOnly)
val adultOnly: Boolean = PrefManager.getVal(PrefName.AdultOnly)
val idArr = mutableListOf<Int>()
list["recentUpdates"] = recentUpdates?.airingSchedules?.mapNotNull { i ->
i.media?.let {
if (!idArr.contains(it.id))
if (!listOnly && it.countryOfOrigin == "JP" && Anilist.adult && adultOnly && it.isAdult == true) {
idArr.add(it.id)
Media(it)
} else if (!listOnly && !adultOnly && (it.countryOfOrigin == "JP" && it.isAdult == false)) {
idArr.add(it.id)
Media(it)
} else if ((listOnly && it.mediaListEntry != null)) {
idArr.add(it.id)
Media(it)
} else null
else null
}
} as ArrayList<Media>
list["trendingMovies"] = trendingMovies?.media?.map { Media(it) } as ArrayList<Media>
list["topRated"] = topRated?.media?.map { Media(it) } as ArrayList<Media>
list["mostFav"] = mostFav?.media?.map { Media(it) } as ArrayList<Media>
list["recentUpdates"]?.addAll(recentUpdates2?.airingSchedules?.mapNotNull { i ->
i.media?.let {
if (!idArr.contains(it.id))
if (!listOnly && it.countryOfOrigin == "JP" && Anilist.adult && adultOnly && it.isAdult == true) {
idArr.add(it.id)
Media(it)
} else if (!listOnly && !adultOnly && (it.countryOfOrigin == "JP" && it.isAdult == false)) {
idArr.add(it.id)
Media(it)
} else if ((listOnly && it.mediaListEntry != null)) {
idArr.add(it.id)
Media(it)
} else null
else null
}
} as ArrayList<Media>)
list["trendingMovies"]?.addAll(trendingMovies2?.media?.map { Media(it) } as ArrayList<Media>)
list["topRated"]?.addAll(topRated2?.media?.map { Media(it) } as ArrayList<Media>)
list["mostFav"]?.addAll(mostFav2?.media?.map { Media(it) } as ArrayList<Media>)
}
return list
}
private val onListManga =
(if (PrefManager.getVal(PrefName.IncludeMangaList)) "" else "onList:false").replace(
"\"",
""
)
private fun trendingManga(page: Int): String {
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA,countryOfOrigin:JP, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun trendingManhwa(page: Int): String {
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA, countryOfOrigin:KR, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun trendingNovel(page: Int): String {
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:POPULARITY_DESC, type: MANGA, format: NOVEL, countryOfOrigin:JP, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun topRatedManga(page: Int): String {
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort: SCORE_DESC, type: MANGA, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
private fun mostFavManga(page: Int): String {
return """Page(page:$page,perPage:50){pageInfo{hasNextPage total}media(sort:FAVOURITES_DESC,type: MANGA, $onListManga, $isAdult){id idMal status chapters episodes nextAiringEpisode{episode}isAdult type meanScore isFavourite format bannerImage countryOfOrigin coverImage{large}title{english romaji userPreferred}mediaListEntry{progress private score(format:POINT_100)status}}}"""
}
suspend fun loadMangaList(): Map<String, ArrayList<Media>> {
val list = mutableMapOf<String, ArrayList<Media>>()
fun query(): String {
return """{
trendingManga:${trendingManga(1)}
trendingManga2:${trendingManga(2)}
trendingManhwa:${trendingManhwa(1)}
trendingManhwa2:${trendingManhwa(2)}
trendingNovel:${trendingNovel(1)}
trendingNovel2:${trendingNovel(2)}
topRated:${topRatedManga(1)}
topRated2:${topRatedManga(2)}
mostFav:${mostFavManga(1)}
mostFav2:${mostFavManga(2)}
}""".trimIndent()
}
executeQuery<Query.MangaList>(query(), force = true)?.data?.apply {
list["trendingManga"] = trendingManga?.media?.map { Media(it) } as ArrayList<Media>
list["trendingManhwa"] = trendingManhwa?.media?.map { Media(it) } as ArrayList<Media>
list["trendingNovel"] = trendingNovel?.media?.map { Media(it) } as ArrayList<Media>
list["topRated"] = topRated?.media?.map { Media(it) } as ArrayList<Media>
list["mostFav"] = mostFav?.media?.map { Media(it) } as ArrayList<Media>
list["trendingManga"]?.addAll(trendingManga2?.media?.map { Media(it) } as ArrayList<Media>)
list["trendingManhwa"]?.addAll(trendingManhwa2?.media?.map { Media(it) } as ArrayList<Media>)
list["trendingNovel"]?.addAll(trendingNovel2?.media?.map { Media(it) } as ArrayList<Media>)
list["topRated"]?.addAll(topRated2?.media?.map { Media(it) } as ArrayList<Media>)
list["mostFav"]?.addAll(mostFav2?.media?.map { Media(it) } as ArrayList<Media>)
}
return list
}
suspend fun recentlyUpdated( suspend fun recentlyUpdated(
smaller: Boolean = true,
greater: Long = 0, greater: Long = 0,
lesser: Long = System.currentTimeMillis() / 1000 - 10000 lesser: Long = System.currentTimeMillis() / 1000 - 10000
): MutableList<Media>? { ): MutableList<Media> {
suspend fun execute(page: Int = 1): Page? { suspend fun execute(page: Int = 1): Page? {
val query = """{ val query = """{
Page(page:$page,perPage:50) { Page(page:$page,perPage:50) {
@@ -1070,41 +1258,26 @@ Page(page:$page,perPage:50) {
}""".replace("\n", " ").replace(""" """, "") }""".replace("\n", " ").replace(""" """, "")
return executeQuery<Query.Page>(query, force = true)?.data?.page return executeQuery<Query.Page>(query, force = true)?.data?.page
} }
if (smaller) {
val response = execute()?.airingSchedules ?: return null var i = 1
val idArr = mutableListOf<Int>() val list = mutableListOf<Media>()
val listOnly: Boolean = PrefManager.getVal(PrefName.RecentlyListOnly) var res: Page? = null
return response.mapNotNull { i -> suspend fun next() {
i.media?.let { res = execute(i)
if (!idArr.contains(it.id)) list.addAll(res?.airingSchedules?.mapNotNull { j ->
if (!listOnly && (it.countryOfOrigin == "JP" && (if (!Anilist.adult) it.isAdult == false else true)) || (listOnly && it.mediaListEntry != null)) { j.media?.let {
idArr.add(it.id) if (it.countryOfOrigin == "JP" && (if (!Anilist.adult) it.isAdult == false else true)) {
Media(it) Media(it).apply { relation = "${j.episode},${j.airingAt}" }
} else null } else null
else null
} }
}.toMutableList() } ?: listOf())
} else {
var i = 1
val list = mutableListOf<Media>()
var res: Page? = null
suspend fun next() {
res = execute(i)
list.addAll(res?.airingSchedules?.mapNotNull { j ->
j.media?.let {
if (it.countryOfOrigin == "JP" && (if (!Anilist.adult) it.isAdult == false else true)) {
Media(it).apply { relation = "${j.episode},${j.airingAt}" }
} else null
}
} ?: listOf())
}
next()
while (res?.pageInfo?.hasNextPage == true) {
next()
i++
}
return list.reversed().toMutableList()
} }
next()
while (res?.pageInfo?.hasNextPage == true) {
next()
i++
}
return list.reversed().toMutableList()
} }
suspend fun getCharacterDetails(character: Character): Character { suspend fun getCharacterDetails(character: Character): Character {
@@ -1290,19 +1463,39 @@ Page(page:$page,perPage:50) {
} }
} }
} }
characters(page: $page,sort:FAVOURITES_DESC) {
pageInfo{
hasNextPage
}
nodes{
id
name {
first
middle
last
full
native
userPreferred
}
image {
large
medium
}
}
}
} }
}""".replace("\n", " ").replace(""" """, "") }""".replace("\n", " ").replace(""" """, "")
var hasNextPage = true var hasNextPage = true
val yearMedia = mutableMapOf<String, ArrayList<Media>>() val yearMedia = mutableMapOf<String, ArrayList<Media>>()
var page = 0 var page = 0
val characters = arrayListOf<Character>()
while (hasNextPage) { while (hasNextPage) {
page++ page++
hasNextPage = executeQuery<Query.Author>( val query = executeQuery<Query.Author>(
query(page), query(page), force = true
force = true )?.data?.author
)?.data?.author?.staffMedia?.let { hasNextPage = query?.staffMedia?.let {
it.edges?.forEach { i -> it.edges?.forEach { i ->
i.node?.apply { i.node?.apply {
val status = status.toString() val status = status.toString()
@@ -1317,6 +1510,20 @@ Page(page:$page,perPage:50) {
} }
it.pageInfo?.hasNextPage == true it.pageInfo?.hasNextPage == true
} ?: false } ?: false
query?.characters?.let {
it.nodes?.forEach { i ->
characters.add(
Character(
i.id,
i.name?.userPreferred,
i.image?.large,
i.image?.medium,
"",
false
)
)
}
}
} }
if (yearMedia.contains("CANCELLED")) { if (yearMedia.contains("CANCELLED")) {
@@ -1324,6 +1531,7 @@ Page(page:$page,perPage:50) {
yearMedia.remove("CANCELLED") yearMedia.remove("CANCELLED")
yearMedia["CANCELLED"] = a yearMedia["CANCELLED"] = a
} }
author.character = characters
author.yearMedia = yearMedia author.yearMedia = yearMedia
return author return author
} }
@@ -1390,17 +1598,16 @@ Page(page:$page,perPage:50) {
"""{ """{
favoriteAnime:${userFavMediaQuery(true, 1, id)} favoriteAnime:${userFavMediaQuery(true, 1, id)}
favoriteManga:${userFavMediaQuery(false, 1, id)} favoriteManga:${userFavMediaQuery(false, 1, id)}
animeMediaList:${bannerImageQuery("ANIME", id)}
mangaMediaList:${bannerImageQuery("MANGA", id)}
}""".trimIndent(), force = true }""".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? { suspend fun getNotifications(
id: Int,
page: Int = 1,
resetNotification: Boolean = true
): NotificationResponse? {
val reset = if (resetNotification) "true" else "false" val reset = if (resetNotification) "true" else "false"
val res = executeQuery<NotificationResponse>( 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,}}}}""", """{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,}}}}""",
@@ -1415,7 +1622,12 @@ Page(page:$page,perPage:50) {
return res return res
} }
suspend fun getFeed(userId: Int?, global: Boolean = false, page: Int = 1, activityId: Int? = null): FeedResponse? { suspend fun getFeed(
userId: Int?,
global: Boolean = false,
page: Int = 1,
activityId: Int? = null
): FeedResponse? {
val filter = if (activityId != null) "id:$activityId," val filter = if (activityId != null) "id:$activityId,"
else if (userId != null) "userId:$userId," else if (userId != null) "userId:$userId,"
else if (global) "isFollowing:false,hasRepliesOrTypeText:true," else if (global) "isFollowing:false,hasRepliesOrTypeText:true,"
@@ -1426,14 +1638,44 @@ Page(page:$page,perPage:50) {
) )
} }
suspend fun isUserFav(favType: AnilistMutations.FavType, id: Int): Boolean { //anilist isFavourite is broken, so we need to check it manually suspend fun getUpcomingAnime(id: String): List<Media> {
val res = getUserProfile(Anilist.userid?: return false) val res = executeQuery<Query.MediaListCollection>(
"""{MediaListCollection(userId:$id,type:ANIME){lists{name entries{media{id,isFavourite,title{userPreferred,romaji}coverImage{medium}nextAiringEpisode{timeUntilAiring}}}}}}""",
force = true
)
val list = mutableListOf<Media>()
res?.data?.mediaListCollection?.lists?.forEach { listEntry ->
listEntry.entries?.forEach { entry ->
entry.media?.nextAiringEpisode?.timeUntilAiring?.let {
list.add(Media(entry.media!!))
}
}
}
return list.sortedBy { it.timeUntilAiring }
.distinctBy { it.id }
.filter { it.timeUntilAiring != null }
}
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) { return when (favType) {
AnilistMutations.FavType.ANIME -> res?.data?.user?.favourites?.anime?.nodes?.any { it.id == id } ?: false AnilistMutations.FavType.ANIME -> res?.data?.user?.favourites?.anime?.nodes?.any { it.id == id }
AnilistMutations.FavType.MANGA -> res?.data?.user?.favourites?.manga?.nodes?.any { it.id == id } ?: false ?: 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.MANGA -> res?.data?.user?.favourites?.manga?.nodes?.any { it.id == id }
AnilistMutations.FavType.STUDIO -> res?.data?.user?.favourites?.studios?.nodes?.any { it.id == id } ?: false ?: 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
} }
} }

View File

@@ -58,45 +58,36 @@ class AnilistHomeViewModel : ViewModel() {
MutableLiveData<ArrayList<Media>>(null) MutableLiveData<ArrayList<Media>>(null)
fun getAnimeContinue(): LiveData<ArrayList<Media>> = animeContinue fun getAnimeContinue(): LiveData<ArrayList<Media>> = animeContinue
suspend fun setAnimeContinue() = animeContinue.postValue(Anilist.query.continueMedia("ANIME"))
private val animeFav: MutableLiveData<ArrayList<Media>> = private val animeFav: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null) MutableLiveData<ArrayList<Media>>(null)
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
suspend fun setAnimeFav() = animeFav.postValue(Anilist.query.favMedia(true))
private val animePlanned: MutableLiveData<ArrayList<Media>> = private val animePlanned: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null) MutableLiveData<ArrayList<Media>>(null)
fun getAnimePlanned(): LiveData<ArrayList<Media>> = animePlanned fun getAnimePlanned(): LiveData<ArrayList<Media>> = animePlanned
suspend fun setAnimePlanned() =
animePlanned.postValue(Anilist.query.continueMedia("ANIME", true))
private val mangaContinue: MutableLiveData<ArrayList<Media>> = private val mangaContinue: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null) MutableLiveData<ArrayList<Media>>(null)
fun getMangaContinue(): LiveData<ArrayList<Media>> = mangaContinue fun getMangaContinue(): LiveData<ArrayList<Media>> = mangaContinue
suspend fun setMangaContinue() = mangaContinue.postValue(Anilist.query.continueMedia("MANGA"))
private val mangaFav: MutableLiveData<ArrayList<Media>> = private val mangaFav: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null) MutableLiveData<ArrayList<Media>>(null)
fun getMangaFav(): LiveData<ArrayList<Media>> = mangaFav fun getMangaFav(): LiveData<ArrayList<Media>> = mangaFav
suspend fun setMangaFav() = mangaFav.postValue(Anilist.query.favMedia(false))
private val mangaPlanned: MutableLiveData<ArrayList<Media>> = private val mangaPlanned: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null) MutableLiveData<ArrayList<Media>>(null)
fun getMangaPlanned(): LiveData<ArrayList<Media>> = mangaPlanned fun getMangaPlanned(): LiveData<ArrayList<Media>> = mangaPlanned
suspend fun setMangaPlanned() =
mangaPlanned.postValue(Anilist.query.continueMedia("MANGA", true))
private val recommendation: MutableLiveData<ArrayList<Media>> = private val recommendation: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null) MutableLiveData<ArrayList<Media>>(null)
fun getRecommendation(): LiveData<ArrayList<Media>> = recommendation fun getRecommendation(): LiveData<ArrayList<Media>> = recommendation
suspend fun setRecommendation() = recommendation.postValue(Anilist.query.recommendations())
suspend fun initHomePage() { suspend fun initHomePage() {
val res = Anilist.query.initHomePage() val res = Anilist.query.initHomePage()
@@ -112,8 +103,8 @@ class AnilistHomeViewModel : ViewModel() {
suspend fun loadMain(context: FragmentActivity) { suspend fun loadMain(context: FragmentActivity) {
Anilist.getSavedToken() Anilist.getSavedToken()
MAL.getSavedToken(context) MAL.getSavedToken()
Discord.getSavedToken(context) Discord.getSavedToken()
if (!BuildConfig.FLAVOR.contains("fdroid")) { if (!BuildConfig.FLAVOR.contains("fdroid")) {
if (PrefManager.getVal(PrefName.CheckUpdate)) AppUpdater.check(context) if (PrefManager.getVal(PrefName.CheckUpdate)) AppUpdater.check(context)
} }
@@ -144,22 +135,19 @@ class AnilistAnimeViewModel : ViewModel() {
sort = Anilist.sortBy[2], sort = Anilist.sortBy[2],
season = season, season = season,
seasonYear = year, seasonYear = year,
hd = true hd = true,
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
)?.results )?.results
) )
} }
private val updated: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getUpdated(): LiveData<MutableList<Media>> = updated
suspend fun loadUpdated() = updated.postValue(Anilist.query.recentlyUpdated())
private val animePopular = MutableLiveData<SearchResults?>(null) private val animePopular = MutableLiveData<SearchResults?>(null)
fun getPopular(): LiveData<SearchResults?> = animePopular fun getPopular(): LiveData<SearchResults?> = animePopular
suspend fun loadPopular( suspend fun loadPopular(
type: String, type: String,
search_val: String? = null, searchVal: String? = null,
genres: ArrayList<String>? = null, genres: ArrayList<String>? = null,
sort: String = Anilist.sortBy[1], sort: String = Anilist.sortBy[1],
onList: Boolean = true, onList: Boolean = true,
@@ -167,10 +155,11 @@ class AnilistAnimeViewModel : ViewModel() {
animePopular.postValue( animePopular.postValue(
Anilist.query.search( Anilist.query.search(
type, type,
search = search_val, search = searchVal,
onList = if (onList) null else false, onList = if (onList) null else false,
sort = sort, sort = sort,
genres = genres genres = genres,
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
) )
) )
} }
@@ -185,13 +174,43 @@ class AnilistAnimeViewModel : ViewModel() {
r.sort, r.sort,
r.genres, r.genres,
r.tags, r.tags,
r.status,
r.source,
r.format, r.format,
r.countryOfOrigin,
r.isAdult, r.isAdult,
r.onList r.onList,
adultOnly = PrefManager.getVal(PrefName.AdultOnly),
) )
) )
var loaded: Boolean = false var loaded: Boolean = false
private val updated: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getUpdated(): LiveData<MutableList<Media>> = updated
private val popularMovies: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getMovies(): LiveData<MutableList<Media>> = popularMovies
private val topRatedAnime: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getTopRated(): LiveData<MutableList<Media>> = topRatedAnime
private val mostFavAnime: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getMostFav(): LiveData<MutableList<Media>> = mostFavAnime
suspend fun loadAll() {
val list = Anilist.query.loadAnimeList()
updated.postValue(list["recentUpdates"])
popularMovies.postValue(list["trendingMovies"])
topRatedAnime.postValue(list["topRated"])
mostFavAnime.postValue(list["mostFav"])
}
} }
class AnilistMangaViewModel : ViewModel() { class AnilistMangaViewModel : ViewModel() {
@@ -209,29 +228,17 @@ class AnilistMangaViewModel : ViewModel() {
type, type,
perPage = 10, perPage = 10,
sort = Anilist.sortBy[2], sort = Anilist.sortBy[2],
hd = true hd = true,
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
)?.results )?.results
) )
private val updated: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getTrendingNovel(): LiveData<MutableList<Media>> = updated
suspend fun loadTrendingNovel() =
updated.postValue(
Anilist.query.search(
type,
perPage = 10,
sort = Anilist.sortBy[2],
format = "NOVEL"
)?.results
)
private val mangaPopular = MutableLiveData<SearchResults?>(null) private val mangaPopular = MutableLiveData<SearchResults?>(null)
fun getPopular(): LiveData<SearchResults?> = mangaPopular fun getPopular(): LiveData<SearchResults?> = mangaPopular
suspend fun loadPopular( suspend fun loadPopular(
type: String, type: String,
search_val: String? = null, searchVal: String? = null,
genres: ArrayList<String>? = null, genres: ArrayList<String>? = null,
sort: String = Anilist.sortBy[1], sort: String = Anilist.sortBy[1],
onList: Boolean = true, onList: Boolean = true,
@@ -239,10 +246,11 @@ class AnilistMangaViewModel : ViewModel() {
mangaPopular.postValue( mangaPopular.postValue(
Anilist.query.search( Anilist.query.search(
type, type,
search = search_val, search = searchVal,
onList = if (onList) null else false, onList = if (onList) null else false,
sort = sort, sort = sort,
genres = genres genres = genres,
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
) )
) )
} }
@@ -257,17 +265,55 @@ class AnilistMangaViewModel : ViewModel() {
r.sort, r.sort,
r.genres, r.genres,
r.tags, r.tags,
r.status,
r.source,
r.format, r.format,
r.countryOfOrigin,
r.isAdult, r.isAdult,
r.onList, r.onList,
r.excludedGenres, r.excludedGenres,
r.excludedTags, r.excludedTags,
r.startYear,
r.seasonYear, r.seasonYear,
r.season r.season,
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
) )
) )
var loaded: Boolean = false var loaded: Boolean = false
private val popularManga: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getPopularManga(): LiveData<MutableList<Media>> = popularManga
private val popularManhwa: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getPopularManhwa(): LiveData<MutableList<Media>> = popularManhwa
private val popularNovel: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getPopularNovel(): LiveData<MutableList<Media>> = popularNovel
private val topRatedManga: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getTopRated(): LiveData<MutableList<Media>> = topRatedManga
private val mostFavManga: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getMostFav(): LiveData<MutableList<Media>> = mostFavManga
suspend fun loadAll() {
val list = Anilist.query.loadMangaList()
popularManga.postValue(list["trendingManga"])
popularManhwa.postValue(list["trendingManhwa"])
popularNovel.postValue(list["trendingNovel"])
topRatedManga.postValue(list["topRated"])
mostFavManga.postValue(list["mostFav"])
}
} }
class AnilistSearch : ViewModel() { class AnilistSearch : ViewModel() {
@@ -286,13 +332,17 @@ class AnilistSearch : ViewModel() {
r.sort, r.sort,
r.genres, r.genres,
r.tags, r.tags,
r.status,
r.source,
r.format, r.format,
r.countryOfOrigin,
r.isAdult, r.isAdult,
r.onList, r.onList,
r.excludedGenres, r.excludedGenres,
r.excludedTags, r.excludedTags,
r.startYear,
r.seasonYear, r.seasonYear,
r.season r.season,
) )
) )
@@ -305,11 +355,15 @@ class AnilistSearch : ViewModel() {
r.sort, r.sort,
r.genres, r.genres,
r.tags, r.tags,
r.status,
r.source,
r.format, r.format,
r.countryOfOrigin,
r.isAdult, r.isAdult,
r.onList, r.onList,
r.excludedGenres, r.excludedGenres,
r.excludedTags, r.excludedTags,
r.startYear,
r.seasonYear, r.seasonYear,
r.season r.season
) )
@@ -347,11 +401,6 @@ class ProfileViewModel : ViewModel() {
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav 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) { suspend fun setData(id: Int) {
val res = Anilist.query.initProfilePage(id) val res = Anilist.query.initProfilePage(id)
val mangaList = res?.data?.favoriteManga?.favourites?.manga?.edges?.mapNotNull { val mangaList = res?.data?.favoriteManga?.favourites?.manga?.edges?.mapNotNull {
@@ -367,30 +416,11 @@ class ProfileViewModel : ViewModel() {
} }
animeFav.postValue(ArrayList(animeList ?: arrayListOf())) 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() { fun refresh() {
mangaFav.postValue(mangaFav.value) mangaFav.postValue(mangaFav.value)
animeFav.postValue(animeFav.value) animeFav.postValue(animeFav.value)
listImages.postValue(listImages.value)
} }
} }

View File

@@ -4,7 +4,6 @@ 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.util.Logger
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.startMainActivity import ani.dantotsu.startMainActivity

View File

@@ -11,13 +11,17 @@ data class SearchResults(
var onList: Boolean? = null, var onList: Boolean? = null,
var perPage: Int? = null, var perPage: Int? = null,
var search: String? = null, var search: String? = null,
var countryOfOrigin: String? = null,
var sort: String? = null, var sort: String? = null,
var genres: MutableList<String>? = null, var genres: MutableList<String>? = null,
var excludedGenres: MutableList<String>? = null, var excludedGenres: MutableList<String>? = null,
var tags: MutableList<String>? = null, var tags: MutableList<String>? = null,
var excludedTags: MutableList<String>? = null, var excludedTags: MutableList<String>? = null,
var status: String? = null,
var source: String? = null,
var format: String? = null, var format: String? = null,
var seasonYear: Int? = null, var seasonYear: Int? = null,
var startYear: Int? = null,
var season: String? = null, var season: String? = null,
var page: Int = 1, var page: Int = 1,
var results: MutableList<Media>, var results: MutableList<Media>,
@@ -37,12 +41,24 @@ data class SearchResults(
) )
) )
} }
status?.let {
list.add(SearchChip("STATUS", currContext()!!.getString(R.string.filter_status, it)))
}
source?.let {
list.add(SearchChip("SOURCE", currContext()!!.getString(R.string.filter_source, it)))
}
format?.let { format?.let {
list.add(SearchChip("FORMAT", currContext()!!.getString(R.string.filter_format, it))) list.add(SearchChip("FORMAT", currContext()!!.getString(R.string.filter_format, it)))
} }
countryOfOrigin?.let {
list.add(SearchChip("COUNTRY", currContext()!!.getString(R.string.filter_country, it)))
}
season?.let { season?.let {
list.add(SearchChip("SEASON", it)) list.add(SearchChip("SEASON", it))
} }
startYear?.let {
list.add(SearchChip("START_YEAR", it.toString()))
}
seasonYear?.let { seasonYear?.let {
list.add(SearchChip("SEASON_YEAR", it.toString())) list.add(SearchChip("SEASON_YEAR", it.toString()))
} }
@@ -74,8 +90,12 @@ data class SearchResults(
fun removeChip(chip: SearchChip) { fun removeChip(chip: SearchChip) {
when (chip.type) { when (chip.type) {
"SORT" -> sort = null "SORT" -> sort = null
"STATUS" -> status = null
"SOURCE" -> source = null
"FORMAT" -> format = null "FORMAT" -> format = null
"COUNTRY" -> countryOfOrigin = null
"SEASON" -> season = null "SEASON" -> season = null
"START_YEAR" -> startYear = null
"SEASON_YEAR" -> seasonYear = null "SEASON_YEAR" -> seasonYear = null
"GENRE" -> genres?.remove(chip.text) "GENRE" -> genres?.remove(chip.text)
"EXCLUDED_GENRE" -> excludedGenres?.remove(chip.text) "EXCLUDED_GENRE" -> excludedGenres?.remove(chip.text)

View File

@@ -55,7 +55,7 @@ data class CharacterConnection(
@SerialName("nodes") var nodes: List<Character>?, @SerialName("nodes") var nodes: List<Character>?,
// The pagination information // The pagination information
// @SerialName("pageInfo") var pageInfo: PageInfo?, @SerialName("pageInfo") var pageInfo: PageInfo?,
) : java.io.Serializable ) : java.io.Serializable
@Serializable @Serializable
@@ -72,7 +72,7 @@ data class CharacterEdge(
@SerialName("name") var name: String?, @SerialName("name") var name: String?,
// The voice actors of the character // The voice actors of the character
// @SerialName("voiceActors") var voiceActors: List<Staff>?, @SerialName("voiceActors") var voiceActors: List<Staff>?,
// The voice actors of the character with role date // The voice actors of the character with role date
// @SerialName("voiceActorRoles") var voiceActorRoles: List<StaffRoleType>?, // @SerialName("voiceActorRoles") var voiceActorRoles: List<StaffRoleType>?,

View File

@@ -24,7 +24,9 @@ class Query {
@Serializable @Serializable
data class Data( data class Data(
@SerialName("Media") @SerialName("Media")
val media: ani.dantotsu.connections.anilist.api.Media? val media: ani.dantotsu.connections.anilist.api.Media?,
@SerialName("Page")
val page: ani.dantotsu.connections.anilist.api.Page?
) )
} }
@@ -147,9 +149,45 @@ class Query {
@Serializable @Serializable
data class Data( data class Data(
@SerialName("favoriteAnime") val favoriteAnime: ani.dantotsu.connections.anilist.api.User?, @SerialName("favoriteAnime") val favoriteAnime: ani.dantotsu.connections.anilist.api.User?,
@SerialName("favoriteManga") val favoriteManga: 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 AnimeList(
@SerialName("data")
val data: Data?
) {
@Serializable
data class Data(
@SerialName("recentUpdates") val recentUpdates: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("recentUpdates2") val recentUpdates2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingMovies") val trendingMovies: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingMovies2") val trendingMovies2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("topRated") val topRated: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("topRated2") val topRated2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("mostFav") val mostFav: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("mostFav2") val mostFav2: ani.dantotsu.connections.anilist.api.Page?,
)
}
@Serializable
data class MangaList(
@SerialName("data")
val data: Data?
) {
@Serializable
data class Data(
@SerialName("trendingManga") val trendingManga: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingManga2") val trendingManga2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingManhwa") val trendingManhwa: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingManhwa2") val trendingManhwa2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingNovel") val trendingNovel: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingNovel2") val trendingNovel2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("topRated") val topRated: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("topRated2") val topRated2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("mostFav") val mostFav: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("mostFav2") val mostFav2: ani.dantotsu.connections.anilist.api.Page?,
) )
} }
@@ -283,13 +321,13 @@ class Query {
val statistics: NNUserStatisticTypes, val statistics: NNUserStatisticTypes,
@SerialName("siteUrl") @SerialName("siteUrl")
val siteUrl: String, val siteUrl: String,
): java.io.Serializable ) : java.io.Serializable
@Serializable @Serializable
data class NNUserStatisticTypes( data class NNUserStatisticTypes(
@SerialName("anime") var anime: NNUserStatistics, @SerialName("anime") var anime: NNUserStatistics,
@SerialName("manga") var manga: NNUserStatistics @SerialName("manga") var manga: NNUserStatistics
): java.io.Serializable ) : java.io.Serializable
@Serializable @Serializable
data class NNUserStatistics( data class NNUserStatistics(
@@ -300,9 +338,9 @@ class Query {
@SerialName("episodesWatched") var episodesWatched: Int, @SerialName("episodesWatched") var episodesWatched: Int,
@SerialName("chaptersRead") var chaptersRead: Int, @SerialName("chaptersRead") var chaptersRead: Int,
@SerialName("volumesRead") var volumesRead: Int, @SerialName("volumesRead") var volumesRead: Int,
): java.io.Serializable ) : java.io.Serializable
@Serializable @Serializable
data class UserFavourites( data class UserFavourites(
@SerialName("anime") @SerialName("anime")
val anime: UserMediaFavouritesCollection, val anime: UserMediaFavouritesCollection,
@@ -314,13 +352,13 @@ class Query {
val staff: UserStaffFavouritesCollection, val staff: UserStaffFavouritesCollection,
@SerialName("studios") @SerialName("studios")
val studios: UserStudioFavouritesCollection, val studios: UserStudioFavouritesCollection,
): java.io.Serializable ) : java.io.Serializable
@Serializable @Serializable
data class UserMediaFavouritesCollection( data class UserMediaFavouritesCollection(
@SerialName("nodes") @SerialName("nodes")
val nodes: List<UserMediaImageFavorite>, val nodes: List<UserMediaImageFavorite>,
): java.io.Serializable ) : java.io.Serializable
@Serializable @Serializable
data class UserMediaImageFavorite( data class UserMediaImageFavorite(
@@ -328,13 +366,13 @@ class Query {
val id: Int, val id: Int,
@SerialName("coverImage") @SerialName("coverImage")
val coverImage: MediaCoverImage val coverImage: MediaCoverImage
): java.io.Serializable ) : java.io.Serializable
@Serializable @Serializable
data class UserCharacterFavouritesCollection( data class UserCharacterFavouritesCollection(
@SerialName("nodes") @SerialName("nodes")
val nodes: List<UserCharacterImageFavorite>, val nodes: List<UserCharacterImageFavorite>,
): java.io.Serializable ) : java.io.Serializable
@Serializable @Serializable
data class UserCharacterImageFavorite( data class UserCharacterImageFavorite(
@@ -346,19 +384,19 @@ class Query {
val image: CharacterImage, val image: CharacterImage,
@SerialName("isFavourite") @SerialName("isFavourite")
val isFavourite: Boolean val isFavourite: Boolean
): java.io.Serializable ) : java.io.Serializable
@Serializable @Serializable
data class UserStaffFavouritesCollection( data class UserStaffFavouritesCollection(
@SerialName("nodes") @SerialName("nodes")
val nodes: List<UserCharacterImageFavorite>, //downstream it's the same as character val nodes: List<UserCharacterImageFavorite>, //downstream it's the same as character
): java.io.Serializable ) : java.io.Serializable
@Serializable @Serializable
data class UserStudioFavouritesCollection( data class UserStudioFavouritesCollection(
@SerialName("nodes") @SerialName("nodes")
val nodes: List<UserStudioFavorite>, val nodes: List<UserStudioFavorite>,
): java.io.Serializable ) : java.io.Serializable
@Serializable @Serializable
data class UserStudioFavorite( data class UserStudioFavorite(
@@ -366,7 +404,7 @@ class Query {
val id: Int, val id: Int,
@SerialName("name") @SerialName("name")
val name: String, val name: String,
): java.io.Serializable ) : java.io.Serializable
//---------------------------------------- //----------------------------------------
// Statistics // Statistics
@@ -375,12 +413,12 @@ class Query {
data class StatisticsResponse( data class StatisticsResponse(
@SerialName("data") @SerialName("data")
val data: Data val data: Data
): java.io.Serializable { ) : java.io.Serializable {
@Serializable @Serializable
data class Data( data class Data(
@SerialName("User") @SerialName("User")
val user: StatisticsUser? val user: StatisticsUser?
): java.io.Serializable ) : java.io.Serializable
} }
@Serializable @Serializable

View File

@@ -21,6 +21,7 @@ enum class NotificationType(val value: String) {
MEDIA_DATA_CHANGE("MEDIA_DATA_CHANGE"), MEDIA_DATA_CHANGE("MEDIA_DATA_CHANGE"),
MEDIA_MERGE("MEDIA_MERGE"), MEDIA_MERGE("MEDIA_MERGE"),
MEDIA_DELETION("MEDIA_DELETION"), MEDIA_DELETION("MEDIA_DELETION"),
//custom //custom
COMMENT_REPLY("COMMENT_REPLY"), COMMENT_REPLY("COMMENT_REPLY"),
} }
@@ -84,9 +85,9 @@ data class Notification(
@SerialName("createdAt") @SerialName("createdAt")
val createdAt: Int, val createdAt: Int,
@SerialName("media") @SerialName("media")
val media: ani.dantotsu.connections.anilist.api.Media? = null, val media: Media? = null,
@SerialName("user") @SerialName("user")
val user: ani.dantotsu.connections.anilist.api.User? = null, val user: User? = null,
@SerialName("message") @SerialName("message")
val message: MessageActivity? = null, val message: MessageActivity? = null,
@SerialName("activity") @SerialName("activity")

View File

@@ -93,6 +93,7 @@ data class StaffConnection(
// The pagination information // The pagination information
// @SerialName("pageInfo") var pageInfo: PageInfo?, // @SerialName("pageInfo") var pageInfo: PageInfo?,
) )
@Serializable @Serializable
data class StaffImage( data class StaffImage(
// The character's image of media at its largest size // The character's image of media at its largest size
@@ -101,6 +102,7 @@ data class StaffImage(
// 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 ) : java.io.Serializable
@Serializable @Serializable
data class StaffEdge( data class StaffEdge(
var role: String?, var role: String?,

View File

@@ -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 ) : java.io.Serializable
@Serializable @Serializable
data class UserStatisticTypes( data class UserStatisticTypes(

View File

@@ -0,0 +1,128 @@
package ani.dantotsu.connections.bakaupdates
import android.content.Context
import ani.dantotsu.R
import ani.dantotsu.client
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.tryWithSuspend
import ani.dantotsu.util.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import okio.ByteString.Companion.encode
import org.json.JSONException
import org.json.JSONObject
import java.nio.charset.Charset
class MangaUpdates {
private val Int?.dateFormat get() = String.format("%02d", this)
private val apiUrl = "https://api.mangaupdates.com/v1/releases/search"
suspend fun search(title: String, startDate: FuzzyDate?): MangaUpdatesResponse.Results? {
return tryWithSuspend {
val query = JSONObject().apply {
try {
put("search", title.encode(Charset.forName("UTF-8")))
startDate?.let {
put(
"start_date",
"${it.year}-${it.month.dateFormat}-${it.day.dateFormat}"
)
}
put("include_metadata", true)
} catch (e: JSONException) {
e.printStackTrace()
}
}
val res = client.post(apiUrl, json = query).parsed<MangaUpdatesResponse>()
coroutineScope {
res.results?.map {
async(Dispatchers.IO) {
Logger.log(it.toString())
}
}
}?.awaitAll()
res.results?.first {
it.metadata.series.lastUpdated?.timestamp != null
&& (it.metadata.series.latestChapter != null
|| (it.record.volume.isNullOrBlank() && it.record.chapter != null))
}
}
}
companion object {
fun getLatestChapter(context: Context, results: MangaUpdatesResponse.Results): String {
return results.metadata.series.latestChapter?.let {
context.getString(R.string.chapter_number, it)
} ?: results.record.chapter!!.substringAfterLast("-").trim().let { chapter ->
chapter.takeIf {
it.toIntOrNull() == null
} ?: context.getString(R.string.chapter_number, chapter.toInt())
}
}
}
@Serializable
data class MangaUpdatesResponse(
@SerialName("total_hits")
val totalHits: Int?,
@SerialName("page")
val page: Int?,
@SerialName("per_page")
val perPage: Int?,
val results: List<Results>? = null
) {
@Serializable
data class Results(
val record: Record,
val metadata: MetaData
) {
@Serializable
data class Record(
@SerialName("id")
val id: Int,
@SerialName("title")
val title: String,
@SerialName("volume")
val volume: String?,
@SerialName("chapter")
val chapter: String?,
@SerialName("release_date")
val releaseDate: String
)
@Serializable
data class MetaData(
val series: Series
) {
@Serializable
data class Series(
@SerialName("series_id")
val seriesId: Long?,
@SerialName("title")
val title: String?,
@SerialName("latest_chapter")
val latestChapter: Int?,
@SerialName("last_updated")
val lastUpdated: LastUpdated?
) {
@Serializable
data class LastUpdated(
@SerialName("timestamp")
val timestamp: Long,
@SerialName("as_rfc3339")
val asRfc3339: String,
@SerialName("as_string")
val asString: String
)
}
}
}
}
}

View File

@@ -24,7 +24,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
object CommentsAPI { object CommentsAPI {
val address: String = "https://1224665.xyz:443" private const val ADDRESS: String = "https://1224665.xyz:443"
var authToken: String? = null var authToken: String? = null
var userId: String? = null var userId: String? = null
var isBanned: Boolean = false var isBanned: Boolean = false
@@ -32,8 +32,13 @@ object CommentsAPI {
var isMod: Boolean = false var isMod: Boolean = false
var totalVotes: Int = 0 var totalVotes: Int = 0
suspend fun getCommentsForId(id: Int, page: Int = 1, tag: Int?, sort: String?): CommentResponse? { suspend fun getCommentsForId(
var url = "$address/comments/$id/$page" id: Int,
page: Int = 1,
tag: Int?,
sort: String?
): CommentResponse? {
var url = "$ADDRESS/comments/$id/$page"
val request = requestBuilder() val request = requestBuilder()
tag?.let { tag?.let {
url += "?tag=$it" url += "?tag=$it"
@@ -61,7 +66,7 @@ object CommentsAPI {
} }
suspend fun getRepliesFromId(id: Int, page: Int = 1): CommentResponse? { suspend fun getRepliesFromId(id: Int, page: Int = 1): CommentResponse? {
val url = "$address/comments/parent/$id/$page" val url = "$ADDRESS/comments/parent/$id/$page"
val request = requestBuilder() val request = requestBuilder()
val json = try { val json = try {
request.get(url) request.get(url)
@@ -83,7 +88,7 @@ object CommentsAPI {
} }
suspend fun getSingleComment(id: Int): Comment? { suspend fun getSingleComment(id: Int): Comment? {
val url = "$address/comments/$id" val url = "$ADDRESS/comments/$id"
val request = requestBuilder() val request = requestBuilder()
val json = try { val json = try {
request.get(url) request.get(url)
@@ -105,7 +110,7 @@ object CommentsAPI {
} }
suspend fun vote(commentId: Int, voteType: Int): Boolean { suspend fun vote(commentId: Int, voteType: Int): Boolean {
val url = "$address/comments/vote/$commentId/$voteType" val url = "$ADDRESS/comments/vote/$commentId/$voteType"
val request = requestBuilder() val request = requestBuilder()
val json = try { val json = try {
request.post(url) request.post(url)
@@ -121,7 +126,7 @@ object CommentsAPI {
} }
suspend fun comment(mediaId: Int, parentCommentId: Int?, content: String, tag: Int?): Comment? { suspend fun comment(mediaId: Int, parentCommentId: Int?, content: String, tag: Int?): Comment? {
val url = "$address/comments" val url = "$ADDRESS/comments"
val body = FormBody.Builder() val body = FormBody.Builder()
.add("user_id", userId ?: return null) .add("user_id", userId ?: return null)
.add("media_id", mediaId.toString()) .add("media_id", mediaId.toString())
@@ -169,7 +174,7 @@ object CommentsAPI {
} }
suspend fun deleteComment(commentId: Int): Boolean { suspend fun deleteComment(commentId: Int): Boolean {
val url = "$address/comments/$commentId" val url = "$ADDRESS/comments/$commentId"
val request = requestBuilder() val request = requestBuilder()
val json = try { val json = try {
request.delete(url) request.delete(url)
@@ -185,7 +190,7 @@ object CommentsAPI {
} }
suspend fun editComment(commentId: Int, content: String): Boolean { suspend fun editComment(commentId: Int, content: String): Boolean {
val url = "$address/comments/$commentId" val url = "$ADDRESS/comments/$commentId"
val body = FormBody.Builder() val body = FormBody.Builder()
.add("content", content) .add("content", content)
.build() .build()
@@ -204,7 +209,7 @@ object CommentsAPI {
} }
suspend fun banUser(userId: String): Boolean { suspend fun banUser(userId: String): Boolean {
val url = "$address/ban/$userId" val url = "$ADDRESS/ban/$userId"
val request = requestBuilder() val request = requestBuilder()
val json = try { val json = try {
request.post(url) request.post(url)
@@ -225,7 +230,7 @@ object CommentsAPI {
mediaTitle: String, mediaTitle: String,
reportedId: String reportedId: String
): Boolean { ): Boolean {
val url = "$address/report/$commentId" val url = "$ADDRESS/report/$commentId"
val body = FormBody.Builder() val body = FormBody.Builder()
.add("username", username) .add("username", username)
.add("mediaName", mediaTitle) .add("mediaName", mediaTitle)
@@ -247,7 +252,7 @@ object CommentsAPI {
} }
suspend fun getNotifications(client: OkHttpClient): NotificationResponse? { suspend fun getNotifications(client: OkHttpClient): NotificationResponse? {
val url = "$address/notification/reply" val url = "$ADDRESS/notification/reply"
val request = requestBuilder(client) val request = requestBuilder(client)
val json = try { val json = try {
request.get(url) request.get(url)
@@ -268,7 +273,7 @@ object CommentsAPI {
} }
private suspend fun getUserDetails(client: OkHttpClient? = null): User? { private suspend fun getUserDetails(client: OkHttpClient? = null): User? {
val url = "$address/user" val url = "$ADDRESS/user"
val request = if (client != null) requestBuilder(client) else requestBuilder() val request = if (client != null) requestBuilder(client) else requestBuilder()
val json = try { val json = try {
request.get(url) request.get(url)
@@ -310,7 +315,7 @@ object CommentsAPI {
} }
} }
val url = "$address/authenticate" val url = "$ADDRESS/authenticate"
val token = PrefManager.getVal(PrefName.AnilistToken, null as String?) ?: return val token = PrefManager.getVal(PrefName.AnilistToken, null as String?) ?: return
repeat(MAX_RETRIES) { repeat(MAX_RETRIES) {
try { try {
@@ -348,6 +353,17 @@ object CommentsAPI {
snackString("Failed to login after multiple attempts") snackString("Failed to login after multiple attempts")
} }
fun logout() {
PrefManager.removeVal(PrefName.CommentAuthResponse)
PrefManager.removeVal(PrefName.CommentTokenExpiry)
authToken = null
userId = null
isBanned = false
isAdmin = false
isMod = false
totalVotes = 0
}
private suspend fun authRequest( private suspend fun authRequest(
token: String, token: String,
url: String, url: String,
@@ -388,7 +404,7 @@ object CommentsAPI {
null null
} }
val message = parsed?.message ?: reason ?: error val message = parsed?.message ?: reason ?: error
val fullMessage = if(code == 500) message else "$code: $message" val fullMessage = if (code == 500) message else "$code: $message"
toast(fullMessage) toast(fullMessage)
} }

View File

@@ -7,6 +7,7 @@ class CrashlyticsStub : CrashlyticsInterface {
override fun initialize(context: Context) { override fun initialize(context: Context) {
//no-op //no-op
} }
override fun logException(e: Throwable) { override fun logException(e: Throwable) {
Logger.log(e) Logger.log(e)
} }

View File

@@ -20,14 +20,14 @@ object Discord {
var avatar: String? = null var avatar: String? = null
fun getSavedToken(context: Context): Boolean { fun getSavedToken(): Boolean {
token = PrefManager.getVal( token = PrefManager.getVal(
PrefName.DiscordToken, null as String? PrefName.DiscordToken, null as String?
) )
return token != null return token != null
} }
fun saveToken(context: Context, token: String) { fun saveToken(token: String) {
PrefManager.setVal(PrefName.DiscordToken, token) PrefManager.setVal(PrefName.DiscordToken, token)
} }
@@ -71,4 +71,6 @@ object Discord {
const val application_Id = "1163925779692912771" const val application_Id = "1163925779692912771"
const val small_Image: String = const val small_Image: String =
"mp:external/GJEe4hKzr8w56IW6ZKQz43HFVEo8pOtA_C-dJiWwxKo/https/cdn.discordapp.com/app-icons/1163925779692912771/f6b42d41dfdf0b56fcc79d4a12d2ac66.png" "mp:external/GJEe4hKzr8w56IW6ZKQz43HFVEo8pOtA_C-dJiWwxKo/https/cdn.discordapp.com/app-icons/1163925779692912771/f6b42d41dfdf0b56fcc79d4a12d2ac66.png"
const val small_Image_AniList: String =
"mp:external/rHOIjjChluqQtGyL_UHk6Z4oAqiVYlo_B7HSGPLSoUg/%3Fsize%3D128/https/cdn.discordapp.com/icons/210521487378087947/a_f54f910e2add364a3da3bb2f2fce0c72.webp"
} }

View File

@@ -5,16 +5,12 @@ import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.content.ContentValues
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build import android.os.Build
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 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
@@ -37,7 +33,6 @@ import okhttp3.Response
import okhttp3.WebSocket import okhttp3.WebSocket
import okhttp3.WebSocketListener import okhttp3.WebSocketListener
import java.io.File import java.io.File
import java.io.OutputStreamWriter
class DiscordService : Service() { class DiscordService : Service() {
private var heartbeat: Int = 0 private var heartbeat: Int = 0
@@ -49,6 +44,7 @@ class DiscordService : Service() {
private lateinit var heartbeatThread: Thread private lateinit var heartbeatThread: Thread
private lateinit var client: OkHttpClient private lateinit var client: OkHttpClient
private lateinit var wakeLock: PowerManager.WakeLock private lateinit var wakeLock: PowerManager.WakeLock
private val shouldLog = false
var presenceStore = "" var presenceStore = ""
val json = Json { val json = Json {
encodeDefaults = true encodeDefaults = true
@@ -67,7 +63,7 @@ class DiscordService : Service() {
PowerManager.PARTIAL_WAKE_LOCK, PowerManager.PARTIAL_WAKE_LOCK,
"discordRPC:backgroundPresence" "discordRPC:backgroundPresence"
) )
wakeLock.acquire() wakeLock.acquire(30 * 60 * 1000L /*30 minutes*/)
log("WakeLock Acquired") log("WakeLock Acquired")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel( val serviceChannel = NotificationChannel(
@@ -162,8 +158,8 @@ class DiscordService : Service() {
inner class DiscordWebSocketListener : WebSocketListener() { inner class DiscordWebSocketListener : WebSocketListener() {
var retryAttempts = 0 private var retryAttempts = 0
val maxRetryAttempts = 10 private val maxRetryAttempts = 10
override fun onOpen(webSocket: WebSocket, response: Response) { override fun onOpen(webSocket: WebSocket, response: Response) {
super.onOpen(webSocket, response) super.onOpen(webSocket, response)
this@DiscordService.webSocket = webSocket this@DiscordService.webSocket = webSocket
@@ -232,7 +228,7 @@ class DiscordService : Service() {
resume() resume()
resume = false resume = false
} else { } else {
identify(webSocket, baseContext) identify(webSocket)
log("WebSocket: Identified") log("WebSocket: Identified")
} }
} }
@@ -245,13 +241,13 @@ class DiscordService : Service() {
} }
} }
fun identify(webSocket: WebSocket, context: Context) { private fun identify(webSocket: WebSocket) {
val properties = JsonObject() val properties = JsonObject()
properties.addProperty("os", "linux") properties.addProperty("os", "linux")
properties.addProperty("browser", "unknown") properties.addProperty("browser", "unknown")
properties.addProperty("device", "unknown") properties.addProperty("device", "unknown")
val d = JsonObject() val d = JsonObject()
d.addProperty("token", getToken(context)) d.addProperty("token", getToken())
d.addProperty("intents", 0) d.addProperty("intents", 0)
d.add("properties", properties) d.add("properties", properties)
val payload = JsonObject() val payload = JsonObject()
@@ -270,7 +266,7 @@ class DiscordService : Service() {
retryAttempts++ retryAttempts++
if (retryAttempts >= maxRetryAttempts) { if (retryAttempts >= maxRetryAttempts) {
log("WebSocket: Error, onFailure() reason: Max Retry Attempts") log("WebSocket: Error, onFailure() reason: Max Retry Attempts")
errorNotification("Could not set the presence", "Max Retry Attempts") errorNotification("Timeout setting presence", "Max Retry Attempts")
return return
} }
} }
@@ -311,7 +307,7 @@ class DiscordService : Service() {
} }
} }
fun getToken(context: Context): String { fun getToken(): String {
val token = PrefManager.getVal(PrefName.DiscordToken, null as String?) val token = PrefManager.getVal(PrefName.DiscordToken, null as String?)
return if (token == null) { return if (token == null) {
log("WebSocket: Token not found") log("WebSocket: Token not found")
@@ -349,13 +345,13 @@ class DiscordService : Service() {
Manifest.permission.POST_NOTIFICATIONS Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED ) != PackageManager.PERMISSION_GRANTED
) { ) {
//TODO: Request permission
return return
} }
notificationManager.notify(2, builder.build()) notificationManager.notify(2, builder.build())
log("Error Notified") log("Error Notified")
} }
@Suppress("unused")
fun saveSimpleTestPresence() { fun saveSimpleTestPresence() {
val file = File(baseContext.cacheDir, "payload") val file = File(baseContext.cacheDir, "payload")
//fill with test payload //fill with test payload
@@ -375,20 +371,22 @@ class DiscordService : Service() {
log("WebSocket: Simple Test Presence Saved") log("WebSocket: Simple Test Presence Saved")
} }
fun setPresence(String: String) { fun setPresence(string: String) {
log("WebSocket: Sending Presence payload") log("WebSocket: Sending Presence payload")
log(String) log(string)
webSocket.send(String) webSocket.send(string)
} }
fun log(string: String) { fun log(string: String) {
//Logger.log(string) if (shouldLog) {
Logger.log(string)
}
} }
fun resume() { fun resume() {
log("Sending Resume payload") log("Sending Resume payload")
val d = JsonObject() val d = JsonObject()
d.addProperty("token", getToken(baseContext)) d.addProperty("token", getToken())
d.addProperty("session_id", sessionId) d.addProperty("session_id", sessionId)
d.addProperty("seq", sequence) d.addProperty("seq", sequence)
val json = JsonObject() val json = JsonObject()
@@ -404,7 +402,7 @@ class DiscordService : Service() {
Thread.sleep(heartbeat.toLong()) Thread.sleep(heartbeat.toLong())
heartbeatSend(webSocket, sequence) heartbeatSend(webSocket, sequence)
log("WebSocket: Heartbeat Sent") log("WebSocket: Heartbeat Sent")
} catch (e: InterruptedException) { } catch (ignored: InterruptedException) {
} }
} }
} }

View File

@@ -75,7 +75,7 @@ class Login : AppCompatActivity() {
} }
Toast.makeText(this, "Logged in successfully", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Logged in successfully", Toast.LENGTH_SHORT).show()
finish() finish()
saveToken(this, token) saveToken(token)
startMainActivity(this@Login) startMainActivity(this@Login)
} }

View File

@@ -71,8 +71,8 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
assets = Activity.Assets( assets = Activity.Assets(
largeImage = data.largeImage?.url?.discordUrl(), largeImage = data.largeImage?.url?.discordUrl(),
largeText = data.largeImage?.label, largeText = data.largeImage?.label,
smallImage = data.smallImage?.url?.discordUrl(), smallImage = if (PrefManager.getVal(PrefName.ShowAniListIcon)) Discord.small_Image_AniList.discordUrl() else Discord.small_Image.discordUrl(),
smallText = data.smallImage?.label smallText = if (PrefManager.getVal(PrefName.ShowAniListIcon)) "Anilist" else "Dantotsu",
), ),
buttons = data.buttons.map { it.label }, buttons = data.buttons.map { it.label },
metadata = Activity.Metadata( metadata = Activity.Metadata(

View File

@@ -0,0 +1,114 @@
package ani.dantotsu.connections.github
import ani.dantotsu.Mapper
import ani.dantotsu.R
import ani.dantotsu.client
import ani.dantotsu.getAppString
import ani.dantotsu.settings.Developer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.decodeFromJsonElement
class Contributors {
fun getContributors(): Array<Developer> {
var developers = arrayOf<Developer>()
runBlocking(Dispatchers.IO) {
val repo = getAppString(R.string.repo)
val res = client.get("https://api.github.com/repos/$repo/contributors")
.parsed<JsonArray>().map {
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
}
res.forEach {
if (it.login == "SunglassJerry") return@forEach
val role = when (it.login) {
"rebelonion" -> "Owner & Maintainer"
"sneazy-ibo" -> "Contributor & Comment Moderator"
"WaiWhat" -> "Icon Designer"
else -> "Contributor"
}
developers = developers.plus(
Developer(
it.login,
it.avatarUrl,
role,
it.htmlUrl
)
)
}
developers = developers.plus(
arrayOf(
Developer(
"MarshMeadow",
"https://avatars.githubusercontent.com/u/88599122?v=4",
"Beta Icon Designer & Website Maintainer",
"https://github.com/MarshMeadow?tab=repositories"
),
Developer(
"Zaxx69",
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6342562-kxE8m4i7KUMK.png",
"Telegram Admin",
"https://anilist.co/user/6342562"
),
Developer(
"Arif Alam",
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6011177-2n994qtayiR9.jpg",
"Discord & Comment Moderator",
"https://anilist.co/user/6011177"
),
Developer(
"SunglassJeery",
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b5804776-FEKfP5wbz2xv.png",
"Head Discord & Comment Moderator",
"https://anilist.co/user/5804776"
),
Developer(
"Excited",
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6131921-toSoGWmKbRA1.png",
"Comment Moderator",
"https://anilist.co/user/6131921"
),
Developer(
"Gurjshan",
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6363228-rWQ3Pl3WuxzL.png",
"Comment Moderator",
"https://anilist.co/user/6363228"
),
Developer(
"NekoMimi",
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6244220-HOpImMGMQAxW.jpg",
"Comment Moderator",
"https://anilist.co/user/6244220"
),
Developer(
"Zaidsenior",
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6049773-8cjYeUOFUguv.jpg",
"Comment Moderator",
"https://anilist.co/user/6049773"
),
Developer(
"hastsu",
"https://cdn.discordapp.com/avatars/602422545077108749/20b4a6efa4314550e4ed51cdbe4fef3d.webp?size=160",
"Comment Moderator",
"https://anilist.co/user/6183359"
),
)
)
}
return developers
}
@Serializable
data class GithubResponse(
@SerialName("login")
val login: String,
@SerialName("avatar_url")
val avatarUrl: String,
@SerialName("html_url")
val htmlUrl: String
)
}

View File

@@ -0,0 +1,54 @@
package ani.dantotsu.connections.github
import ani.dantotsu.Mapper
import ani.dantotsu.client
import ani.dantotsu.settings.Developer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.decodeFromJsonElement
class Forks {
fun getForks(): Array<Developer> {
var forks = arrayOf<Developer>()
runBlocking(Dispatchers.IO) {
val res =
client.get("https://api.github.com/repos/rebelonion/Dantotsu/forks?sort=stargazers")
.parsed<JsonArray>().map {
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
}
res.forEach {
forks = forks.plus(
Developer(
it.name,
it.owner.avatarUrl,
it.owner.login,
it.htmlUrl
)
)
}
}
return forks
}
@Serializable
data class GithubResponse(
@SerialName("name")
val name: String,
val owner: Owner,
@SerialName("html_url")
val htmlUrl: String,
) {
@Serializable
data class Owner(
@SerialName("login")
val login: String,
@SerialName("avatar_url")
val avatarUrl: String
)
}
}

View File

@@ -5,7 +5,6 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import android.util.Base64 import android.util.Base64
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
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
@@ -64,7 +63,7 @@ object MAL {
} }
suspend fun getSavedToken(context: FragmentActivity): Boolean { suspend fun getSavedToken(): Boolean {
return tryWithSuspend(false) { return tryWithSuspend(false) {
var res: ResponseToken = var res: ResponseToken =
PrefManager.getNullableVal<ResponseToken>(PrefName.MALToken, null) PrefManager.getNullableVal<ResponseToken>(PrefName.MALToken, null)
@@ -77,7 +76,7 @@ object MAL {
} ?: false } ?: false
} }
fun removeSavedToken(context: Context) { fun removeSavedToken() {
token = null token = null
username = null username = null
userid = null userid = null

View File

@@ -0,0 +1,381 @@
package ani.dantotsu.download
import android.content.Context
import android.net.Uri
import android.os.Environment
import android.widget.Toast
import ani.dantotsu.R
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.currActivity
import ani.dantotsu.currContext
import ani.dantotsu.download.anime.OfflineAnimeModel
import ani.dantotsu.download.manga.OfflineMangaModel
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.MediaType
import ani.dantotsu.parsers.Episode
import ani.dantotsu.parsers.MangaChapter
import ani.dantotsu.parsers.MangaImage
import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.SubtitleType
import ani.dantotsu.util.Logger
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 eu.kanade.tachiyomi.source.model.SManga
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.util.Locale
@Deprecated("external storage is deprecated, use SAF instead")
class DownloadCompat {
companion object {
@Deprecated("external storage is deprecated, use SAF instead")
fun loadMediaCompat(downloadedType: DownloadedType): Media? {
val type = when (downloadedType.type) {
MediaType.MANGA -> "Manga"
MediaType.ANIME -> "Anime"
else -> "Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.titleName}"
)
//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
}
}
@Deprecated("external storage is deprecated, use SAF instead")
fun loadOfflineAnimeModelCompat(downloadedType: DownloadedType): OfflineAnimeModel {
val type = when (downloadedType.type) {
MediaType.MANGA -> "Manga"
MediaType.ANIME -> "Anime"
else -> "Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.titleName}"
)
//load media.json and convert to media class with gson
try {
val mediaModel = loadMediaCompat(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
)
}
}
@Deprecated("external storage is deprecated, use SAF instead")
fun loadOfflineMangaModelCompat(downloadedType: DownloadedType): OfflineMangaModel {
val type = when (downloadedType.type) {
MediaType.MANGA -> "Manga"
MediaType.ANIME -> "Anime"
else -> "Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.titleName}"
)
//load media.json and convert to media class with gson
try {
val mediaModel = loadMediaCompat(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 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) {
Logger.log("Error loading media.json: ${e.message}")
Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e)
return OfflineMangaModel(
"unknown",
"0",
"??",
"??",
"movie",
"hmm",
isOngoing = false,
isUserScored = false,
null,
null
)
}
}
@Deprecated("external storage is deprecated, use SAF instead")
suspend fun loadEpisodesCompat(
animeLink: String,
extra: Map<String, String>?,
sAnime: SAnime
): List<Episode> {
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"${animeLocation}/$animeLink"
)
//get all of the folder names and add them to the list
val episodes = mutableListOf<Episode>()
if (directory.exists()) {
directory.listFiles()?.forEach {
//put the title and episdode number in the extra data
val extraData = mutableMapOf<String, String>()
extraData["title"] = animeLink
extraData["episode"] = it.name
if (it.isDirectory) {
val episode = Episode(
it.name,
"$animeLink - ${it.name}",
it.name,
null,
null,
extra = extraData,
sEpisode = SEpisodeImpl()
)
episodes.add(episode)
}
}
episodes.sortBy { MediaNameAdapter.findEpisodeNumber(it.number) }
return episodes
}
return emptyList()
}
@Deprecated("external storage is deprecated, use SAF instead")
suspend fun loadChaptersCompat(
mangaLink: String,
extra: Map<String, String>?,
sManga: SManga
): List<MangaChapter> {
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/$mangaLink"
)
//get all of the folder names and add them to the list
val chapters = mutableListOf<MangaChapter>()
if (directory.exists()) {
directory.listFiles()?.forEach {
if (it.isDirectory) {
val chapter = MangaChapter(
it.name,
"$mangaLink/${it.name}",
it.name,
null,
null,
SChapter.create()
)
chapters.add(chapter)
}
}
chapters.sortBy { MediaNameAdapter.findChapterNumber(it.number) }
return chapters
}
return emptyList()
}
@Deprecated("external storage is deprecated, use SAF instead")
suspend fun loadImagesCompat(chapterLink: String, sChapter: SChapter): List<MangaImage> {
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/$chapterLink"
)
val images = mutableListOf<MangaImage>()
val imageNumberRegex = Regex("""(\d+)\.jpg$""")
if (directory.exists()) {
directory.listFiles()?.forEach {
if (it.isFile) {
val image = MangaImage(it.absolutePath, false, null)
images.add(image)
}
}
images.sortBy { image ->
val matchResult = imageNumberRegex.find(image.url.url)
matchResult?.groups?.get(1)?.value?.toIntOrNull() ?: Int.MAX_VALUE
}
for (image in images) {
Logger.log("imageNumber: ${image.url.url}")
}
return images
}
return emptyList()
}
@Deprecated("external storage is deprecated, use SAF instead")
fun loadSubtitleCompat(title: String, episode: String): List<Subtitle>? {
currContext()?.let {
File(
it.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$animeLocation/$title/$episode"
).listFiles()?.forEach {
if (it.name.contains("subtitle")) {
return listOf(
Subtitle(
"Downloaded Subtitle",
Uri.fromFile(it).toString(),
determineSubtitletype(it.absolutePath)
)
)
}
}
}
return null
}
private fun determineSubtitletype(url: String): SubtitleType {
return when {
url.lowercase(Locale.ROOT).endsWith("ass") -> SubtitleType.ASS
url.lowercase(Locale.ROOT).endsWith("vtt") -> SubtitleType.VTT
else -> SubtitleType.SRT
}
}
@Deprecated("external storage is deprecated, use SAF instead")
fun removeMediaCompat(context: Context, title: String, type: MediaType) {
val subDirectory = if (type == MediaType.MANGA) {
"Manga"
} else if (type == MediaType.ANIME) {
"Anime"
} else {
"Novel"
}
val directory = File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$subDirectory/$title"
)
if (directory.exists()) {
directory.deleteRecursively()
}
}
@Deprecated("external storage is deprecated, use SAF instead")
fun removeDownloadCompat(context: Context, downloadedType: DownloadedType) {
val directory = if (downloadedType.type == MediaType.MANGA) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${downloadedType.titleName}/${downloadedType.chapterName}"
)
} else if (downloadedType.type == MediaType.ANIME) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Anime/${downloadedType.titleName}/${downloadedType.chapterName}"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Novel/${downloadedType.titleName}/${downloadedType.chapterName}"
)
}
// Check if the directory exists and delete it recursively
if (directory.exists()) {
val deleted = directory.deleteRecursively()
if (deleted) {
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
}
}
}
private val animeLocation = "Dantotsu/Anime"
}
}

View File

@@ -1,13 +1,27 @@
package ani.dantotsu.download package ani.dantotsu.download
import android.content.Context import android.content.Context
import android.os.Environment import android.net.Uri
import android.widget.Toast import androidx.documentfile.provider.DocumentFile
import ani.dantotsu.download.DownloadCompat.Companion.removeDownloadCompat
import ani.dantotsu.download.DownloadCompat.Companion.removeMediaCompat
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaType
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import com.anggrayudi.storage.callback.FolderCallback
import com.anggrayudi.storage.file.deleteRecursively
import com.anggrayudi.storage.file.findFolder
import com.anggrayudi.storage.file.moveFolderTo
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.xdrop.fuzzywuzzy.FuzzySearch
import java.io.Serializable import java.io.Serializable
class DownloadsManager(private val context: Context) { class DownloadsManager(private val context: Context) {
@@ -15,11 +29,11 @@ class DownloadsManager(private val context: Context) {
private val downloadsList = loadDownloads().toMutableList() private val downloadsList = loadDownloads().toMutableList()
val mangaDownloadedTypes: List<DownloadedType> val mangaDownloadedTypes: List<DownloadedType>
get() = downloadsList.filter { it.type == DownloadedType.Type.MANGA } get() = downloadsList.filter { it.type == MediaType.MANGA }
val animeDownloadedTypes: List<DownloadedType> val animeDownloadedTypes: List<DownloadedType>
get() = downloadsList.filter { it.type == DownloadedType.Type.ANIME } get() = downloadsList.filter { it.type == MediaType.ANIME }
val novelDownloadedTypes: List<DownloadedType> val novelDownloadedTypes: List<DownloadedType>
get() = downloadsList.filter { it.type == DownloadedType.Type.NOVEL } get() = downloadsList.filter { it.type == MediaType.NOVEL }
private fun saveDownloads() { private fun saveDownloads() {
val jsonString = gson.toJson(downloadsList) val jsonString = gson.toJson(downloadsList)
@@ -41,84 +55,72 @@ class DownloadsManager(private val context: Context) {
saveDownloads() saveDownloads()
} }
fun removeDownload(downloadedType: DownloadedType) { fun removeDownload(
downloadedType: DownloadedType,
toast: Boolean = true,
onFinished: () -> Unit
) {
removeDownloadCompat(context, downloadedType)
downloadsList.remove(downloadedType) downloadsList.remove(downloadedType)
removeDirectory(downloadedType) CoroutineScope(Dispatchers.IO).launch {
removeDirectory(downloadedType, toast)
withContext(Dispatchers.Main) {
onFinished()
}
}
saveDownloads() saveDownloads()
} }
fun removeMedia(title: String, type: DownloadedType.Type) { fun removeMedia(title: String, type: MediaType) {
val subDirectory = if (type == DownloadedType.Type.MANGA) { removeMediaCompat(context, title, type)
"Manga" val baseDirectory = getBaseDirectory(context, type)
} else if (type == DownloadedType.Type.ANIME) { val directory = baseDirectory?.findFolder(title)
"Anime" if (directory?.exists() == true) {
} else { val deleted = directory.deleteRecursively(context, false)
"Novel"
}
val directory = File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$subDirectory/$title"
)
if (directory.exists()) {
val deleted = directory.deleteRecursively()
if (deleted) { if (deleted) {
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show() snackString("Successfully deleted")
} else { } else {
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show() snackString("Failed to delete directory")
} }
} else { } else {
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show() snackString("Directory does not exist")
cleanDownloads() cleanDownloads()
} }
when (type) { when (type) {
DownloadedType.Type.MANGA -> { MediaType.MANGA -> {
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.MANGA } downloadsList.removeAll { it.titleName == title && it.type == MediaType.MANGA }
} }
DownloadedType.Type.ANIME -> { MediaType.ANIME -> {
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.ANIME } downloadsList.removeAll { it.titleName == title && it.type == MediaType.ANIME }
} }
DownloadedType.Type.NOVEL -> { MediaType.NOVEL -> {
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.NOVEL } downloadsList.removeAll { it.titleName == title && it.type == MediaType.NOVEL }
} }
} }
saveDownloads() saveDownloads()
} }
private fun cleanDownloads() { private fun cleanDownloads() {
cleanDownload(DownloadedType.Type.MANGA) cleanDownload(MediaType.MANGA)
cleanDownload(DownloadedType.Type.ANIME) cleanDownload(MediaType.ANIME)
cleanDownload(DownloadedType.Type.NOVEL) cleanDownload(MediaType.NOVEL)
} }
private fun cleanDownload(type: DownloadedType.Type) { private fun cleanDownload(type: MediaType) {
// remove all folders that are not in the downloads list // remove all folders that are not in the downloads list
val subDirectory = if (type == DownloadedType.Type.MANGA) { val directory = getBaseDirectory(context, type)
"Manga" val downloadsSubLists = when (type) {
} else if (type == DownloadedType.Type.ANIME) { MediaType.MANGA -> mangaDownloadedTypes
"Anime" MediaType.ANIME -> animeDownloadedTypes
} else { else -> novelDownloadedTypes
"Novel"
} }
val directory = File( if (directory?.exists() == true && directory.isDirectory) {
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$subDirectory"
)
val downloadsSubLists = if (type == DownloadedType.Type.MANGA) {
mangaDownloadedTypes
} else if (type == DownloadedType.Type.ANIME) {
animeDownloadedTypes
} else {
novelDownloadedTypes
}
if (directory.exists()) {
val files = directory.listFiles() val files = directory.listFiles()
if (files != null) { for (file in files) {
for (file in files) { if (!downloadsSubLists.any { it.titleName == file.name }) {
if (!downloadsSubLists.any { it.title == file.name }) { file.deleteRecursively(context, false)
val deleted = file.deleteRecursively()
}
} }
} }
} }
@@ -126,122 +128,129 @@ class DownloadsManager(private val context: Context) {
val iterator = downloadsList.iterator() val iterator = downloadsList.iterator()
while (iterator.hasNext()) { while (iterator.hasNext()) {
val download = iterator.next() val download = iterator.next()
val downloadDir = File(directory, download.title) val downloadDir = directory?.findFolder(download.titleName)
if ((!downloadDir.exists() && download.type == type) || download.title.isBlank()) { if ((downloadDir?.exists() == false && download.type == type) || download.titleName.isBlank()) {
iterator.remove() iterator.remove()
} }
} }
} }
fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List<DownloadedType>) //for debugging fun moveDownloadsDir(
{ context: Context,
val jsonString = gson.toJson(downloadsList) oldUri: Uri,
val file = File( newUri: Uri,
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), finished: (Boolean, String) -> Unit
"Dantotsu/downloads.json" ) {
) try {
if (file.parentFile?.exists() == false) { if (oldUri == newUri) {
file.parentFile?.mkdirs() finished(false, "Source and destination are the same")
return
}
CoroutineScope(Dispatchers.IO).launch {
val oldBase =
DocumentFile.fromTreeUri(context, oldUri) ?: throw Exception("Old base is null")
val newBase =
DocumentFile.fromTreeUri(context, newUri) ?: throw Exception("New base is null")
val folder =
oldBase.findFolder(BASE_LOCATION) ?: throw Exception("Base folder not found")
folder.moveFolderTo(context, newBase, false, BASE_LOCATION, object :
FolderCallback() {
override fun onFailed(errorCode: ErrorCode) {
when (errorCode) {
ErrorCode.CANCELED -> finished(false, "Move canceled")
ErrorCode.CANNOT_CREATE_FILE_IN_TARGET -> finished(
false,
"Cannot create file in target"
)
ErrorCode.INVALID_TARGET_FOLDER -> finished(
true,
"Invalid target folder"
) // seems to still work
ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH -> finished(
false,
"No space left on target path"
)
ErrorCode.UNKNOWN_IO_ERROR -> finished(false, "Unknown IO error")
ErrorCode.SOURCE_FOLDER_NOT_FOUND -> finished(
false,
"Source folder not found"
)
ErrorCode.STORAGE_PERMISSION_DENIED -> finished(
false,
"Storage permission denied"
)
ErrorCode.TARGET_FOLDER_CANNOT_HAVE_SAME_PATH_WITH_SOURCE_FOLDER -> finished(
false,
"Target folder cannot have same path with source folder"
)
else -> finished(false, "Failed to move downloads: $errorCode")
}
Logger.log("Failed to move downloads: $errorCode")
super.onFailed(errorCode)
}
override fun onCompleted(result: Result) {
finished(true, "Successfully moved downloads")
super.onCompleted(result)
}
})
}
} catch (e: Exception) {
snackString("Error: ${e.message}")
finished(false, "Failed to move downloads: ${e.message}")
return
} }
if (!file.exists()) {
file.createNewFile()
}
file.writeText(jsonString)
} }
fun queryDownload(downloadedType: DownloadedType): Boolean { fun queryDownload(downloadedType: DownloadedType): Boolean {
return downloadsList.contains(downloadedType) return downloadsList.contains(downloadedType)
} }
fun queryDownload(title: String, chapter: String, type: DownloadedType.Type? = null): Boolean { fun queryDownload(title: String, chapter: String, type: MediaType? = null): Boolean {
return if (type == null) { return if (type == null) {
downloadsList.any { it.title == title && it.chapter == chapter } downloadsList.any { it.titleName == title && it.chapterName == chapter }
} else { } else {
downloadsList.any { it.title == title && it.chapter == chapter && it.type == type } downloadsList.any { it.titleName == title && it.chapterName == chapter && it.type == type }
} }
} }
private fun removeDirectory(downloadedType: DownloadedType) { private fun removeDirectory(downloadedType: DownloadedType, toast: Boolean) {
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) { val baseDirectory = getBaseDirectory(context, downloadedType.type)
File( val directory =
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), baseDirectory?.findFolder(downloadedType.titleName)
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}" ?.findFolder(downloadedType.chapterName)
) downloadsList.remove(downloadedType)
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
)
}
// Check if the directory exists and delete it recursively // Check if the directory exists and delete it recursively
if (directory.exists()) { if (directory?.exists() == true) {
val deleted = directory.deleteRecursively() val deleted = directory.deleteRecursively(context, false)
if (deleted) { if (deleted) {
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show() if (toast) snackString("Successfully deleted")
} else { } else {
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show() snackString("Failed to delete directory")
} }
} else { } else {
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show() snackString("Directory does not exist")
} }
} }
fun exportDownloads(downloadedType: DownloadedType) { //copies to the downloads folder available to the user fun purgeDownloads(type: MediaType) {
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) { val directory = getBaseDirectory(context, type)
File( if (directory?.exists() == true) {
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), val deleted = directory.deleteRecursively(context, false)
"Dantotsu/Manga/${downloadedType.title}/${downloadedType.chapter}"
)
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Anime/${downloadedType.title}/${downloadedType.chapter}"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Novel/${downloadedType.title}/${downloadedType.chapter}"
)
}
val destination = File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/${downloadedType.title}/${downloadedType.chapter}"
)
if (directory.exists()) {
val copied = directory.copyRecursively(destination, true)
if (copied) {
Toast.makeText(context, "Successfully copied", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "Failed to copy directory", Toast.LENGTH_SHORT).show()
}
} else {
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
}
}
fun purgeDownloads(type: DownloadedType.Type) {
val directory = if (type == DownloadedType.Type.MANGA) {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Manga")
} else if (type == DownloadedType.Type.ANIME) {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Anime")
} else {
File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/Novel")
}
if (directory.exists()) {
val deleted = directory.deleteRecursively()
if (deleted) { if (deleted) {
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show() snackString("Successfully deleted")
} else { } else {
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show() snackString("Failed to delete directory")
} }
} else { } else {
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show() snackString("Directory does not exist")
} }
downloadsList.removeAll { it.type == type } downloadsList.removeAll { it.type == type }
@@ -249,62 +258,132 @@ class DownloadsManager(private val context: Context) {
} }
companion object { companion object {
const val novelLocation = "Dantotsu/Novel" private const val BASE_LOCATION = "Dantotsu"
const val mangaLocation = "Dantotsu/Manga" private const val MANGA_SUB_LOCATION = "Manga"
const val animeLocation = "Dantotsu/Anime" private const val ANIME_SUB_LOCATION = "Anime"
private const val NOVEL_SUB_LOCATION = "Novel"
fun getDirectory(
context: Context, /**
type: DownloadedType.Type, * Get and create a base directory for the given type
title: String, * @param context the context
chapter: String? = null * @param type the type of media
): File { * @return the base directory
return if (type == DownloadedType.Type.MANGA) { */
if (chapter != null) { private fun getBaseDirectory(context: Context, type: MediaType): DocumentFile? {
File( val baseDirectory = Uri.parse(PrefManager.getVal<String>(PrefName.DownloadsDir))
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), if (baseDirectory == Uri.EMPTY) return null
"$mangaLocation/$title/$chapter" var base = DocumentFile.fromTreeUri(context, baseDirectory) ?: return null
) base = base.findOrCreateFolder(BASE_LOCATION, false) ?: return null
} else { return when (type) {
File( MediaType.MANGA -> {
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), base.findOrCreateFolder(MANGA_SUB_LOCATION, false)
"$mangaLocation/$title"
)
} }
} else if (type == DownloadedType.Type.ANIME) {
if (chapter != null) { MediaType.ANIME -> {
File( base.findOrCreateFolder(ANIME_SUB_LOCATION, false)
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$animeLocation/$title/$chapter"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$animeLocation/$title"
)
} }
} else {
if (chapter != null) { else -> {
File( base.findOrCreateFolder(NOVEL_SUB_LOCATION, false)
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$novelLocation/$title/$chapter"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$novelLocation/$title"
)
} }
} }
} }
}
} /**
* Get and create a subdirectory for the given type
* @param context the context
* @param type the type of media
* @param title the title of the media
* @param chapter the chapter of the media
* @return the subdirectory
*/
fun getSubDirectory(
context: Context,
type: MediaType,
overwrite: Boolean,
title: String,
chapter: String? = null
): DocumentFile? {
val baseDirectory = getBaseDirectory(context, type) ?: return null
return if (chapter != null) {
baseDirectory.findOrCreateFolder(title, false)
?.findOrCreateFolder(chapter, overwrite)
} else {
baseDirectory.findOrCreateFolder(title, overwrite)
}
}
data class DownloadedType(val title: String, val chapter: String, val type: Type) : Serializable { fun getDirSize(
enum class Type { context: Context,
MANGA, type: MediaType,
ANIME, title: String,
NOVEL chapter: String? = null
): Long {
val directory = getSubDirectory(context, type, false, title, chapter) ?: return 0
var size = 0L
directory.listFiles().forEach {
size += it.length()
}
return size
}
fun addNoMedia(context: Context) {
val baseDirectory = getBaseDirectory(context) ?: return
if (baseDirectory.findFile(".nomedia") == null) {
baseDirectory.createFile("application/octet-stream", ".nomedia")
}
}
private fun getBaseDirectory(context: Context): DocumentFile? {
val baseDirectory = Uri.parse(PrefManager.getVal<String>(PrefName.DownloadsDir))
if (baseDirectory == Uri.EMPTY) return null
return DocumentFile.fromTreeUri(context, baseDirectory)
}
private fun DocumentFile.findOrCreateFolder(
name: String, overwrite: Boolean
): DocumentFile? {
return if (overwrite) {
findFolder(name.findValidName())?.delete()
createDirectory(name.findValidName())
} else {
findFolder(name.findValidName()) ?: createDirectory(name.findValidName())
}
}
private const val RATIO_THRESHOLD = 95
fun Media.compareName(name: String): Boolean {
val mainName = mainName().findValidName().lowercase()
val ratio = FuzzySearch.ratio(mainName, name.lowercase())
return ratio > RATIO_THRESHOLD
}
fun String.compareName(name: String): Boolean {
val mainName = findValidName().lowercase()
val compareName = name.findValidName().lowercase()
val ratio = FuzzySearch.ratio(mainName, compareName)
return ratio > RATIO_THRESHOLD
}
} }
} }
private const val RESERVED_CHARS = "|\\?*<\":>+[]/'"
fun String?.findValidName(): String {
return this?.filterNot { RESERVED_CHARS.contains(it) } ?: ""
}
data class DownloadedType(
private val pTitle: String?,
private val pChapter: String?,
val type: MediaType,
@Deprecated("use pTitle instead")
private val title: String? = null,
@Deprecated("use pChapter instead")
private val chapter: String? = null
) : Serializable {
val titleName: String
get() = title?:pTitle.findValidName()
val chapterName: String
get() = chapter?:pChapter.findValidName()
}

View File

@@ -9,32 +9,35 @@ import android.content.IntentFilter
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.ServiceInfo import android.content.pm.ServiceInfo
import android.os.Build import android.os.Build
import android.os.Environment
import android.os.IBinder import android.os.IBinder
import android.widget.Toast import android.widget.Toast
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
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import androidx.media3.common.util.UnstableApi 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.FileUrl
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.addons.download.DownloadAddonManager
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.currActivity import ani.dantotsu.defaultHeaders
import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.video.ExoplayerDownloadService import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.download.video.Helper import ani.dantotsu.download.anime.AnimeDownloaderService.AnimeDownloadTask.Companion.getTaskName
import ani.dantotsu.util.Logger
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.SubtitleDownloader import ani.dantotsu.media.SubtitleDownloader
import ani.dantotsu.media.anime.AnimeWatchFragment import ani.dantotsu.media.anime.AnimeWatchFragment
import ani.dantotsu.parsers.Subtitle import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.Video import ani.dantotsu.parsers.Video
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.toast
import ani.dantotsu.util.Logger
import com.anggrayudi.storage.file.forceDelete
import com.anggrayudi.storage.file.openOutputStream
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.animesource.model.SAnime
@@ -45,25 +48,21 @@ import eu.kanade.tachiyomi.data.notification.Notifications
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.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch 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.FileOutputStream
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import java.util.Queue import java.util.Queue
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
class AnimeDownloaderService : Service() { class AnimeDownloaderService : Service() {
private lateinit var notificationManager: NotificationManagerCompat private lateinit var notificationManager: NotificationManagerCompat
@@ -74,6 +73,7 @@ class AnimeDownloaderService : Service() {
private val mutex = Mutex() private val mutex = Mutex()
private var isCurrentlyProcessing = false private var isCurrentlyProcessing = false
private var currentTasks: MutableList<AnimeDownloadTask> = mutableListOf() private var currentTasks: MutableList<AnimeDownloadTask> = mutableListOf()
private val ffExtension = Injekt.get<DownloadAddonManager>().extension?.extension
override fun onBind(intent: Intent?): IBinder? { override fun onBind(intent: Intent?): IBinder? {
// This is only required for bound services. // This is only required for bound services.
@@ -82,6 +82,11 @@ class AnimeDownloaderService : Service() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
if (ffExtension == null) {
toast(getString(R.string.download_addon_not_found))
stopSelf()
return
}
notificationManager = NotificationManagerCompat.from(this) notificationManager = NotificationManagerCompat.from(this)
builder = builder =
NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER_PROGRESS).apply { NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER_PROGRESS).apply {
@@ -89,6 +94,7 @@ class AnimeDownloaderService : Service() {
setSmallIcon(R.drawable.ic_download_24) setSmallIcon(R.drawable.ic_download_24)
priority = NotificationCompat.PRIORITY_DEFAULT priority = NotificationCompat.PRIORITY_DEFAULT
setOnlyAlertOnce(true) setOnlyAlertOnce(true)
setProgress(100, 0, false)
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground( startForeground(
@@ -157,27 +163,14 @@ class AnimeDownloaderService : Service() {
@UnstableApi @UnstableApi
fun cancelDownload(taskName: String) { fun cancelDownload(taskName: String) {
val url = val sessionIds =
AnimeServiceDataSingleton.downloadQueue.find { it.getTaskName() == taskName }?.video?.file?.url AnimeServiceDataSingleton.downloadQueue.filter { it.getTaskName() == taskName }
?: currentTasks.find { it.getTaskName() == taskName }?.video?.file?.url ?: "" .map { it.sessionId }.toMutableList()
if (url.isEmpty()) { sessionIds.addAll(currentTasks.filter { it.getTaskName() == taskName }.map { it.sessionId })
snackString("Failed to cancel download") sessionIds.forEach {
return ffExtension!!.cancelDownload(it)
} }
currentTasks.removeAll { it.getTaskName() == taskName } 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 { CoroutineScope(Dispatchers.Default).launch {
mutex.withLock { mutex.withLock {
downloadJobs[taskName]?.cancel() downloadJobs[taskName]?.cancel()
@@ -210,7 +203,6 @@ class AnimeDownloaderService : Service() {
@androidx.annotation.OptIn(UnstableApi::class) @androidx.annotation.OptIn(UnstableApi::class)
suspend fun download(task: AnimeDownloadTask) { suspend fun download(task: AnimeDownloadTask) {
try { try {
val downloadManager = Helper.downloadManager(this@AnimeDownloaderService)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission( ContextCompat.checkSelfPermission(
@@ -221,18 +213,63 @@ class AnimeDownloaderService : Service() {
true true
} }
builder.setContentText("Downloading ${task.title} - ${task.episode}") builder.setContentText("Downloading ${getTaskName(task.title, task.episode)}")
if (notifi) { if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build()) notificationManager.notify(NOTIFICATION_ID, builder.build())
} }
currActivity()?.let { val outputDir = getSubDirectory(
Helper.downloadVideo( this@AnimeDownloaderService,
it, MediaType.ANIME,
task.video, false,
task.subtitle task.title,
) task.episode
) ?: throw Exception("Failed to create output directory")
outputDir.findFile("${task.getTaskName()}.mp4")?.delete()
val outputFile = outputDir.createFile("video/mp4", "${task.getTaskName()}.mp4")
?: throw Exception("Failed to create output file")
var percent = 0
var totalLength = 0.0
val path = ffExtension!!.setDownloadPath(
this@AnimeDownloaderService,
outputFile.uri
)
val headersStringBuilder = StringBuilder()
task.video.file.headers.forEach {
headersStringBuilder.append("\"${it.key}: ${it.value}\"\'\r\n\'")
} }
if (!task.video.file.headers.containsKey("User-Agent")) { //headers should never be empty now
headersStringBuilder.append("\"").append("User-Agent: ")
.append(defaultHeaders["User-Agent"]).append("\"\'\r\n\'")
}
val probeRequest =
"-headers $headersStringBuilder -i ${task.video.file.url} -show_entries format=duration -v quiet -of csv=\"p=0\""
ffExtension.executeFFProbe(
probeRequest
) {
if (it.toDoubleOrNull() != null) {
totalLength = it.toDouble()
}
}
val headers = headersStringBuilder.toString()
var request = "-headers $headers "
request += "-i ${task.video.file.url} -c copy -bsf:a aac_adtstoasc -tls_verify 0 $path -v trace"
Logger.log("Request: $request")
val ffTask =
ffExtension.executeFFMpeg(request) {
// CALLED WHEN SESSION GENERATES STATISTICS
val timeInMilliseconds = it
if (timeInMilliseconds > 0 && totalLength > 0) {
percent = ((it / 1000) / totalLength * 100).toInt()
}
Logger.log("Statistics: $it")
}
task.sessionId = ffTask
currentTasks.find { it.getTaskName() == task.getTaskName() }?.sessionId =
ffTask
saveMediaInfo(task) saveMediaInfo(task)
task.subtitle?.let { task.subtitle?.let {
@@ -242,90 +279,120 @@ class AnimeDownloaderService : Service() {
DownloadedType( DownloadedType(
task.title, task.title,
task.episode, task.episode,
DownloadedType.Type.ANIME, MediaType.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 // periodically check if the download is complete
while (downloadManager.downloadIndex.getDownload(task.video.file.url) != null) { while (ffExtension.getState(ffTask) != "COMPLETED") {
val download = downloadManager.downloadIndex.getDownload(task.video.file.url) if (ffExtension.getState(ffTask) == "FAILED") {
if (download != null) { Logger.log("Download failed")
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_FAILED) { builder.setContentText(
Logger.log("Download failed") "${
builder.setContentText("${task.title} - ${task.episode} Download failed") getTaskName(
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.title,
task.episode, task.episode
DownloadedType.Type.ANIME,
) )
) } Download failed"
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())
notificationManager.notify(NOTIFICATION_ID, builder.build()) toast("${getTaskName(task.title, task.episode)} Download failed")
} Logger.log("Download failed: ${ffExtension.getStackTrace(ffTask)}")
downloadsManager.removeDownload(
DownloadedType(
task.title,
task.episode,
MediaType.ANIME,
),
false
) {}
Injekt.get<CrashlyticsInterface>().logException(
Exception(
"Anime Download failed:" +
" ${getTaskName(task.title, task.episode)}" +
" url: ${task.video.file.url}" +
" title: ${task.title}" +
" episode: ${task.episode}"
)
)
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
broadcastDownloadFailed(task.episode)
break
}
builder.setProgress(
100, percent.coerceAtMost(99),
false
)
broadcastDownloadProgress(
task.episode,
percent.coerceAtMost(99)
)
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
} }
kotlinx.coroutines.delay(2000) kotlinx.coroutines.delay(2000)
} }
if (ffExtension.getState(ffTask) == "COMPLETED") {
if (ffExtension.hadError(ffTask)) {
Logger.log("Download failed")
builder.setContentText(
"${
getTaskName(
task.title,
task.episode
)
} Download failed"
)
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${getTaskName(task.title, task.episode)} Download failed")
downloadsManager.removeDownload(
DownloadedType(
task.title,
task.episode,
MediaType.ANIME,
)
) {}
Injekt.get<CrashlyticsInterface>().logException(
Exception(
"Anime Download failed:" +
" ${getTaskName(task.title, task.episode)}" +
" url: ${task.video.file.url}" +
" title: ${task.title}" +
" episode: ${task.episode}"
)
)
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
broadcastDownloadFailed(task.episode)
return@withContext
}
Logger.log("Download completed")
builder.setContentText(
"${
getTaskName(
task.title,
task.episode
)
} Download completed"
)
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${getTaskName(task.title, task.episode)} Download completed")
PrefManager.getAnimeDownloadPreferences().edit().putString(
task.getTaskName(),
task.video.file.url
).apply()
downloadsManager.addDownload(
DownloadedType(
task.title,
task.episode,
MediaType.ANIME,
)
)
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
broadcastDownloadFinished(task.episode)
} else throw Exception("Download failed")
} }
} catch (e: Exception) { } catch (e: Exception) {
if (e.message?.contains("Coroutine was cancelled") == false) { //wut if (e.message?.contains("Coroutine was cancelled") == false) { //wut
@@ -338,34 +405,24 @@ class AnimeDownloaderService : Service() {
} }
} }
@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) { private fun saveMediaInfo(task: AnimeDownloadTask) {
launchIO { CoroutineScope(Dispatchers.IO).launch {
val directory = File( val directory =
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), getSubDirectory(this@AnimeDownloaderService, MediaType.ANIME, false, task.title)
"${DownloadsManager.animeLocation}/${task.title}" ?: throw Exception("Directory not found")
) directory.findFile("media.json")?.forceDelete(this@AnimeDownloaderService)
val episodeDirectory = File(directory, task.episode) val file = directory.createFile("application/json", "media.json")
if (!episodeDirectory.exists()) episodeDirectory.mkdirs() ?: throw Exception("File not created")
val episodeDirectory =
getSubDirectory(
this@AnimeDownloaderService,
MediaType.ANIME,
false,
task.title,
task.episode
)
?: throw Exception("Directory not found")
val file = File(directory, "media.json")
val gson = GsonBuilder() val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> { .registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl SChapterImpl() // Provide an instance of SChapterImpl
@@ -399,14 +456,25 @@ class AnimeDownloaderService : Service() {
val jsonString = gson.toJson(media) val jsonString = gson.toJson(media)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
file.writeText(jsonString) try {
file.openOutputStream(this@AnimeDownloaderService, false).use { output ->
if (output == null) throw Exception("Output stream is null")
output.write(jsonString.toByteArray())
}
} catch (e: android.system.ErrnoException) {
e.printStackTrace()
Toast.makeText(
this@AnimeDownloaderService,
"Error while saving: ${e.localizedMessage}",
Toast.LENGTH_LONG
).show()
}
} }
} }
} }
} }
private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
var connection: HttpURLConnection? = null var connection: HttpURLConnection? = null
println("Downloading url $url") println("Downloading url $url")
@@ -417,13 +485,16 @@ class AnimeDownloaderService : Service() {
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}") throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
} }
val file = File(directory, name) directory.findFile(name)?.forceDelete(this@AnimeDownloaderService)
FileOutputStream(file).use { output -> val file =
directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
file.openOutputStream(this@AnimeDownloaderService, false).use { output ->
if (output == null) throw Exception("Output stream is null")
connection.inputStream.use { input -> connection.inputStream.use { input ->
input.copyTo(output) input.copyTo(output)
} }
} }
return@withContext file.absolutePath return@withContext file.uri.toString()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@@ -490,14 +561,15 @@ class AnimeDownloaderService : Service() {
val episodeImage: String? = null, val episodeImage: String? = null,
val retries: Int = 2, val retries: Int = 2,
val simultaneousDownloads: Int = 2, val simultaneousDownloads: Int = 2,
var sessionId: Long = -1
) { ) {
fun getTaskName(): String { fun getTaskName(): String {
return "$title - $episode" return "${title.replace("/", "")}/${episode.replace("/", "")}"
} }
companion object { companion object {
fun getTaskName(title: String, episode: String): String { fun getTaskName(title: String, episode: String): String {
return "$title - $episode" return "${title.replace("/", "")}/${episode.replace("/", "")}"
} }
} }
} }
@@ -511,7 +583,6 @@ class AnimeDownloaderService : Service() {
object AnimeServiceDataSingleton { object AnimeServiceDataSingleton {
var video: Video? = null var video: Video? = null
var sourceMedia: Media? = null
var downloadQueue: Queue<AnimeDownloaderService.AnimeDownloadTask> = ConcurrentLinkedQueue() var downloadQueue: Queue<AnimeDownloaderService.AnimeDownloadTask> = ConcurrentLinkedQueue()
@Volatile @Volatile

View File

@@ -1,7 +1,6 @@
package ani.dantotsu.download.anime package ani.dantotsu.download.anime
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
@@ -38,7 +37,6 @@ class OfflineAnimeAdapter(
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 {
val view: View = convertView ?: when (style) { val view: View = convertView ?: when (style) {
@@ -51,28 +49,27 @@ class OfflineAnimeAdapter(
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 ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing) val ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing)
val totalepisodes = view.findViewById<TextView>(R.id.itemCompactTotal) val totalEpisodes = view.findViewById<TextView>(R.id.itemCompactTotal)
val typeimage = view.findViewById<ImageView>(R.id.itemCompactTypeImage) val typeImage = view.findViewById<ImageView>(R.id.itemCompactTypeImage)
val type = view.findViewById<TextView>(R.id.itemCompactRelation) val type = view.findViewById<TextView>(R.id.itemCompactRelation)
val typeView = view.findViewById<LinearLayout>(R.id.itemCompactType) val typeView = view.findViewById<LinearLayout>(R.id.itemCompactType)
if (style == 0) { if (style == 0) {
val bannerView = view.findViewById<ImageView>(R.id.itemCompactBanner) // for large view val bannerView = view.findViewById<ImageView>(R.id.itemCompactBanner) // for large view
val episodes = view.findViewById<TextView>(R.id.itemTotal) val episodes = view.findViewById<TextView>(R.id.itemTotal)
episodes.text = " Episodes" episodes.text = context.getString(R.string.episodes)
bannerView.setImageURI(item.banner) bannerView.setImageURI(item.banner ?: item.image)
totalepisodes.text = item.totalEpisodeList totalEpisodes.text = item.totalEpisodeList
} else if (style == 1) { } else if (style == 1) {
val watchedEpisodes = val watchedEpisodes =
view.findViewById<TextView>(R.id.itemCompactUserProgress) // for compact view view.findViewById<TextView>(R.id.itemCompactUserProgress) // for compact view
watchedEpisodes.text = item.watchedEpisode watchedEpisodes.text = item.watchedEpisode
totalepisodes.text = " | " + item.totalEpisode totalEpisodes.text = context.getString(R.string.total_divider, item.totalEpisode)
} }
// Bind item data to the views // Bind item data to the views
typeimage.setImageResource(R.drawable.ic_round_movie_filter_24) typeImage.setImageResource(R.drawable.ic_round_movie_filter_24)
type.text = item.type type.text = item.type
typeView.visibility = View.VISIBLE typeView.visibility = View.VISIBLE
imageView.setImageURI(item.image) imageView.setImageURI(item.image)

View File

@@ -4,7 +4,6 @@ package ani.dantotsu.download.anime
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Environment
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.util.TypedValue import android.util.TypedValue
@@ -22,26 +21,34 @@ import androidx.annotation.OptIn
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.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.marginBottom import androidx.core.view.marginBottom
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.bottomBar import ani.dantotsu.bottomBar
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.currActivity import ani.dantotsu.currActivity
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadCompat.Companion.loadMediaCompat
import ani.dantotsu.download.DownloadCompat.Companion.loadOfflineAnimeModelCompat
import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.compareName
import ani.dantotsu.download.findValidName
import ani.dantotsu.initActivity 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.media.MediaType
import ani.dantotsu.navBarHeight 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.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import com.anggrayudi.storage.file.openInputStream
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
@@ -53,9 +60,13 @@ import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl
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.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
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 OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener { class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
@@ -64,6 +75,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
private lateinit var gridView: GridView private lateinit var gridView: GridView
private lateinit var adapter: OfflineAnimeAdapter private lateinit var adapter: OfflineAnimeAdapter
private lateinit var total: TextView private lateinit var total: TextView
private var downloadsJob: Job = Job()
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@@ -110,10 +122,10 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
}) })
var style: Int = PrefManager.getVal(PrefName.OfflineView) var style: Int = PrefManager.getVal(PrefName.OfflineView)
val layoutList = view.findViewById<ImageView>(R.id.downloadedList) val layoutList = view.findViewById<ImageView>(R.id.downloadedList)
val layoutcompact = view.findViewById<ImageView>(R.id.downloadedGrid) val layoutCompact = view.findViewById<ImageView>(R.id.downloadedGrid)
var selected = when (style) { var selected = when (style) {
0 -> layoutList 0 -> layoutList
1 -> layoutcompact 1 -> layoutCompact
else -> layoutList else -> layoutList
} }
selected.alpha = 1f selected.alpha = 1f
@@ -134,7 +146,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
grid() grid()
} }
layoutcompact.setOnClickListener { layoutCompact.setOnClickListener {
selected(it as ImageView) selected(it as ImageView)
style = 1 style = 1
PrefManager.setVal(PrefName.OfflineView, style) PrefManager.setVal(PrefName.OfflineView, style)
@@ -154,11 +166,11 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
private fun grid() { private fun grid() {
gridView.visibility = View.VISIBLE gridView.visibility = View.VISIBLE
getDownloads()
val fadeIn = AlphaAnimation(0f, 1f) val fadeIn = AlphaAnimation(0f, 1f)
fadeIn.duration = 300 // animations pog fadeIn.duration = 300 // animations pog
gridView.layoutAnimation = LayoutAnimationController(fadeIn) gridView.layoutAnimation = LayoutAnimationController(fadeIn)
adapter = OfflineAnimeAdapter(requireContext(), downloads, this) adapter = OfflineAnimeAdapter(requireContext(), downloads, this)
getDownloads()
gridView.adapter = adapter gridView.adapter = adapter
gridView.scheduleLayoutAnimation() gridView.scheduleLayoutAnimation()
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List" total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
@@ -166,20 +178,22 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
// Get the OfflineAnimeModel that was clicked // Get the OfflineAnimeModel that was clicked
val item = adapter.getItem(position) as OfflineAnimeModel val item = adapter.getItem(position) as OfflineAnimeModel
val media = val media =
downloadManager.animeDownloadedTypes.firstOrNull { it.title == item.title } downloadManager.animeDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) }
media?.let { media?.let {
val mediaModel = getMedia(it) lifecycleScope.launch {
if (mediaModel == null) { val mediaModel = getMedia(it)
snackString("Error loading media.json") if (mediaModel == null) {
return@let snackString("Error loading media.json")
return@launch
}
MediaDetailsActivity.mediaSingleton = mediaModel
ContextCompat.startActivity(
requireActivity(),
Intent(requireContext(), MediaDetailsActivity::class.java)
.putExtra("download", true),
null
)
} }
MediaDetailsActivity.mediaSingleton = mediaModel
ContextCompat.startActivity(
requireActivity(),
Intent(requireContext(), MediaDetailsActivity::class.java)
.putExtra("download", true),
null
)
} ?: run { } ?: run {
snackString("no media found") snackString("no media found")
} }
@@ -187,8 +201,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
gridView.setOnItemLongClickListener { _, _, position, _ -> gridView.setOnItemLongClickListener { _, _, position, _ ->
// Get the OfflineAnimeModel that was clicked // Get the OfflineAnimeModel that was clicked
val item = adapter.getItem(position) as OfflineAnimeModel val item = adapter.getItem(position) as OfflineAnimeModel
val type: DownloadedType.Type = val type: MediaType = MediaType.ANIME
DownloadedType.Type.ANIME
// Alert dialog to confirm deletion // Alert dialog to confirm deletion
val builder = val builder =
@@ -203,13 +216,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
if (mediaIds.isEmpty()) { if (mediaIds.isEmpty()) {
snackString("No media found") // if this happens, terrible things have happened 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() getDownloads()
adapter.setItems(downloads)
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
} }
builder.setNegativeButton("No") { _, _ -> builder.setNegativeButton("No") { _, _ ->
// Do nothing // Do nothing
@@ -237,7 +244,6 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
gridView.setOnScrollListener(object : AbsListView.OnScrollListener { gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) { override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
// Implement behavior for different scroll states if needed
} }
override fun onScroll( override fun onScroll(
@@ -250,7 +256,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
val visibility = first != null && first.top < 0 val visibility = first != null && first.top < 0
scrollTop.translationY = scrollTop.translationY =
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat() -(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE scrollTop.isVisible = visibility
} }
}) })
initActivity(requireActivity()) initActivity(requireActivity())
@@ -260,7 +266,6 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
getDownloads() getDownloads()
adapter.notifyDataSetChanged()
} }
override fun onPause() { override fun onPause() {
@@ -280,29 +285,39 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
private fun getDownloads() { private fun getDownloads() {
downloads = listOf() downloads = listOf()
val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct() if (downloadsJob.isActive) {
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>() downloadsJob.cancel()
for (title in animeTitles) { }
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.title == title } downloadsJob = Job()
val download = tDownloads.first() CoroutineScope(Dispatchers.IO + downloadsJob).launch {
val offlineAnimeModel = loadOfflineAnimeModel(download) val animeTitles = downloadManager.animeDownloadedTypes.map { it.titleName.findValidName() }.distinct()
newAnimeDownloads += offlineAnimeModel val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
for (title in animeTitles) {
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.titleName == title }
val download = tDownloads.first()
val offlineAnimeModel = loadOfflineAnimeModel(download)
newAnimeDownloads += offlineAnimeModel
}
downloads = newAnimeDownloads
withContext(Dispatchers.Main) {
adapter.setItems(downloads)
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
adapter.notifyDataSetChanged()
}
} }
downloads = newAnimeDownloads
} }
private fun getMedia(downloadedType: DownloadedType): Media? { /**
val type = when (downloadedType.type) { * Load media.json file from the directory and convert it to Media class
DownloadedType.Type.MANGA -> "Manga" * @param downloadedType DownloadedType object
DownloadedType.Type.ANIME -> "Anime" * @return Media object
else -> "Novel" */
} private suspend fun getMedia(downloadedType: DownloadedType): Media? {
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
return try { return try {
val directory = DownloadsManager.getSubDirectory(
context ?: currContext()!!, downloadedType.type,
false, downloadedType.titleName
)
val gson = GsonBuilder() val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> { .registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl SChapterImpl() // Provide an instance of SChapterImpl
@@ -314,8 +329,13 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
SEpisodeImpl() // Provide an instance of SEpisodeImpl SEpisodeImpl() // Provide an instance of SEpisodeImpl
}) })
.create() .create()
val media = File(directory, "media.json") val media = directory?.findFile("media.json")
val mediaJson = media.readText() ?: return loadMediaCompat(downloadedType)
val mediaJson =
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
it?.readText()
}
?: return null
gson.fromJson(mediaJson, Media::class.java) gson.fromJson(mediaJson, Media::class.java)
} catch (e: Exception) { } catch (e: Exception) {
Logger.log("Error loading media.json: ${e.message}") Logger.log("Error loading media.json: ${e.message}")
@@ -325,27 +345,28 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
} }
} }
private fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel { /**
val type = when (downloadedType.type) { * Load OfflineAnimeModel from the directory
DownloadedType.Type.MANGA -> "Manga" * @param downloadedType DownloadedType object
DownloadedType.Type.ANIME -> "Anime" * @return OfflineAnimeModel object
else -> "Novel" */
} private suspend fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel {
val directory = File( val type = downloadedType.type.asText()
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
try { try {
val directory = DownloadsManager.getSubDirectory(
context ?: currContext()!!, downloadedType.type,
false, downloadedType.titleName
)
val mediaModel = getMedia(downloadedType)!! val mediaModel = getMedia(downloadedType)!!
val cover = File(directory, "cover.jpg") val cover = directory?.findFile("cover.jpg")
val coverUri: Uri? = if (cover.exists()) { val coverUri: Uri? = if (cover?.exists() == true) {
Uri.fromFile(cover) cover.uri
} else null } else null
val banner = File(directory, "banner.jpg") val banner = directory?.findFile("banner.jpg")
val bannerUri: Uri? = if (banner.exists()) { val bannerUri: Uri? = if (banner?.exists() == true) {
Uri.fromFile(banner) banner.uri
} else null } else null
if (coverUri == null && bannerUri == null) throw Exception("No cover or banner found, probably compat")
val title = mediaModel.mainName() 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()
@@ -374,22 +395,26 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
bannerUri bannerUri
) )
} catch (e: Exception) { } catch (e: Exception) {
Logger.log("Error loading media.json: ${e.message}") return try {
Logger.log(e) loadOfflineAnimeModelCompat(downloadedType)
Injekt.get<CrashlyticsInterface>().logException(e) } catch (e: Exception) {
return OfflineAnimeModel( Logger.log("Error loading media.json: ${e.message}")
"unknown", Logger.log(e)
"0", Injekt.get<CrashlyticsInterface>().logException(e)
"??", OfflineAnimeModel(
"??", "unknown",
"??", "0",
"movie", "??",
"hmm", "??",
isOngoing = false, "??",
isUserScored = false, "movie",
null, "hmm",
null isOngoing = false,
) isUserScored = false,
null,
null
)
}
} }
} }
} }

View File

@@ -10,19 +10,20 @@ import android.content.pm.PackageManager
import android.content.pm.ServiceInfo import android.content.pm.ServiceInfo
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Build import android.os.Build
import android.os.Environment
import android.os.IBinder import android.os.IBinder
import android.widget.Toast import android.widget.Toast
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
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.util.Logger import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaType
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
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FINISHED import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_FINISHED
@@ -30,6 +31,10 @@ 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 ani.dantotsu.util.Logger
import com.anggrayudi.storage.file.deleteRecursively
import com.anggrayudi.storage.file.forceDelete
import com.anggrayudi.storage.file.openOutputStream
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
@@ -39,7 +44,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async import kotlinx.coroutines.async
@@ -51,8 +55,6 @@ import kotlinx.coroutines.withContext
import tachiyomi.core.util.lang.launchIO 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.FileOutputStream
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import java.util.Queue import java.util.Queue
@@ -189,13 +191,20 @@ class MangaDownloaderService : Service() {
true true
} }
//val deferredList = mutableListOf<Deferred<Bitmap?>>()
val deferredMap = mutableMapOf<Int, 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())
} }
getSubDirectory(
this@MangaDownloaderService,
MediaType.MANGA,
false,
task.title,
task.chapter
)?.deleteRecursively(this@MangaDownloaderService)
// 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()) {
@@ -211,8 +220,7 @@ class MangaDownloaderService : Service() {
while (bitmap == null && retryCount < task.retries) { while (bitmap == null && retryCount < task.retries) {
bitmap = image.fetchAndProcessImage( bitmap = image.fetchAndProcessImage(
image.page, image.page,
image.source, image.source
this@MangaDownloaderService
) )
retryCount++ retryCount++
} }
@@ -246,7 +254,7 @@ class MangaDownloaderService : Service() {
DownloadedType( DownloadedType(
task.title, task.title,
task.chapter, task.chapter,
DownloadedType.Type.MANGA MediaType.MANGA
) )
) )
broadcastDownloadFinished(task.chapter) broadcastDownloadFinished(task.chapter)
@@ -264,24 +272,18 @@ class MangaDownloaderService : Service() {
private fun saveToDisk(fileName: String, bitmap: Bitmap, title: String, chapter: String) { private fun saveToDisk(fileName: String, bitmap: Bitmap, title: String, chapter: String) {
try { try {
// Define the directory within the private external storage space // Define the directory within the private external storage space
val directory = File( val directory = getSubDirectory(this, MediaType.MANGA, false, title, chapter)
this.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), ?: throw Exception("Directory not found")
"Dantotsu/Manga/$title/$chapter" directory.findFile(fileName)?.forceDelete(this)
) // Create a file reference within that directory for the image
val file =
if (!directory.exists()) { directory.createFile("image/jpeg", fileName) ?: throw Exception("File not created")
directory.mkdirs()
}
// Create a file reference within that directory for your image
val file = File(directory, fileName)
// Use a FileOutputStream to write the bitmap to the file // Use a FileOutputStream to write the bitmap to the file
FileOutputStream(file).use { outputStream -> file.openOutputStream(this, false).use { outputStream ->
if (outputStream == null) throw Exception("Output stream is null")
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
} }
} 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}")
@@ -292,13 +294,12 @@ class MangaDownloaderService : Service() {
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
private fun saveMediaInfo(task: DownloadTask) { private fun saveMediaInfo(task: DownloadTask) {
launchIO { launchIO {
val directory = File( val directory =
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), getSubDirectory(this@MangaDownloaderService, MediaType.MANGA, false, task.title)
"Dantotsu/Manga/${task.title}" ?: throw Exception("Directory not found")
) directory.findFile("media.json")?.forceDelete(this@MangaDownloaderService)
if (!directory.exists()) directory.mkdirs() val file = directory.createFile("application/json", "media.json")
?: throw Exception("File not created")
val file = File(directory, "media.json")
val gson = GsonBuilder() val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> { .registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl SChapterImpl() // Provide an instance of SChapterImpl
@@ -313,7 +314,10 @@ class MangaDownloaderService : Service() {
val jsonString = gson.toJson(media) val jsonString = gson.toJson(media)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
try { try {
file.writeText(jsonString) file.openOutputStream(this@MangaDownloaderService, false).use { output ->
if (output == null) throw Exception("Output stream is null")
output.write(jsonString.toByteArray())
}
} catch (e: android.system.ErrnoException) { } catch (e: android.system.ErrnoException) {
e.printStackTrace() e.printStackTrace()
Toast.makeText( Toast.makeText(
@@ -328,7 +332,7 @@ class MangaDownloaderService : Service() {
} }
private suspend fun downloadImage(url: String, directory: File, name: String): String? = private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
var connection: HttpURLConnection? = null var connection: HttpURLConnection? = null
println("Downloading url $url") println("Downloading url $url")
@@ -338,14 +342,16 @@ class MangaDownloaderService : Service() {
if (connection.responseCode != HttpURLConnection.HTTP_OK) { if (connection.responseCode != HttpURLConnection.HTTP_OK) {
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}") throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
} }
directory.findFile(name)?.forceDelete(this@MangaDownloaderService)
val file = File(directory, name) val file =
FileOutputStream(file).use { output -> directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
file.openOutputStream(this@MangaDownloaderService, false).use { output ->
if (output == null) throw Exception("Output stream is null")
connection.inputStream.use { input -> connection.inputStream.use { input ->
input.copyTo(output) input.copyTo(output)
} }
} }
return@withContext file.absolutePath return@withContext file.uri.toString()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {

View File

@@ -1,6 +1,5 @@
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
@@ -37,7 +36,6 @@ 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 {
val view: View = convertView ?: when (style) { val view: View = convertView ?: when (style) {
@@ -50,7 +48,6 @@ class OfflineMangaAdapter(
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 ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing) val ongoing = view.findViewById<CardView>(R.id.itemCompactOngoing)
val totalChapter = view.findViewById<TextView>(R.id.itemCompactTotal) val totalChapter = view.findViewById<TextView>(R.id.itemCompactTotal)
val typeImage = view.findViewById<ImageView>(R.id.itemCompactTypeImage) val typeImage = view.findViewById<ImageView>(R.id.itemCompactTypeImage)
@@ -60,14 +57,14 @@ class OfflineMangaAdapter(
if (style == 0) { if (style == 0) {
val bannerView = view.findViewById<ImageView>(R.id.itemCompactBanner) // for large view val bannerView = view.findViewById<ImageView>(R.id.itemCompactBanner) // for large view
val chapters = view.findViewById<TextView>(R.id.itemTotal) val chapters = view.findViewById<TextView>(R.id.itemTotal)
chapters.text = " Chapters" chapters.text = context.getString(R.string.chapters)
bannerView.setImageURI(item.banner) bannerView.setImageURI(item.banner ?: item.image)
totalChapter.text = item.totalChapter totalChapter.text = item.totalChapter
} else if (style == 1) { } else if (style == 1) {
val readChapter = val readChapter =
view.findViewById<TextView>(R.id.itemCompactUserProgress) // for compact view view.findViewById<TextView>(R.id.itemCompactUserProgress) // for compact view
readChapter.text = item.readChapter readChapter.text = item.readChapter
totalChapter.text = " | " + item.totalChapter totalChapter.text = context.getString(R.string.total_divider, item.totalChapter)
} }
// Bind item data to the views // Bind item data to the views

View File

@@ -3,7 +3,6 @@ package ani.dantotsu.download.manga
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Environment
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.util.TypedValue import android.util.TypedValue
@@ -20,25 +19,34 @@ 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.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.marginBottom import androidx.core.view.marginBottom
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.bottomBar import ani.dantotsu.bottomBar
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.currActivity import ani.dantotsu.currActivity
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadCompat
import ani.dantotsu.download.DownloadCompat.Companion.loadOfflineMangaModelCompat
import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.compareName
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.download.findValidName
import ani.dantotsu.initActivity 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.media.MediaType
import ani.dantotsu.navBarHeight 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.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import com.anggrayudi.storage.file.openInputStream
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
@@ -46,9 +54,13 @@ 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
import eu.kanade.tachiyomi.source.model.SChapterImpl import eu.kanade.tachiyomi.source.model.SChapterImpl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
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 OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
@@ -57,6 +69,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
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 private lateinit var total: TextView
private var downloadsJob: Job = Job()
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@@ -146,11 +159,11 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private fun grid() { private fun grid() {
gridView.visibility = View.VISIBLE gridView.visibility = View.VISIBLE
getDownloads()
val fadeIn = AlphaAnimation(0f, 1f) val fadeIn = AlphaAnimation(0f, 1f)
fadeIn.duration = 300 // animations pog fadeIn.duration = 300 // animations pog
gridView.layoutAnimation = LayoutAnimationController(fadeIn) gridView.layoutAnimation = LayoutAnimationController(fadeIn)
adapter = OfflineMangaAdapter(requireContext(), downloads, this) adapter = OfflineMangaAdapter(requireContext(), downloads, this)
getDownloads()
gridView.adapter = adapter gridView.adapter = adapter
gridView.scheduleLayoutAnimation() gridView.scheduleLayoutAnimation()
total.text = total.text =
@@ -159,17 +172,18 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
// 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.mangaDownloadedTypes.firstOrNull { it.title == item.title } downloadManager.mangaDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) }
?: downloadManager.novelDownloadedTypes.firstOrNull { it.title == item.title } ?: downloadManager.novelDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) }
media?.let { media?.let {
lifecycleScope.launch {
ContextCompat.startActivity( ContextCompat.startActivity(
requireActivity(), 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 null
) )
}
} ?: run { } ?: run {
snackString("no media found") snackString("no media found")
} }
@@ -178,11 +192,11 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
gridView.setOnItemLongClickListener { _, _, position, _ -> 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: DownloadedType.Type = val type: MediaType =
if (downloadManager.mangaDownloadedTypes.any { it.title == item.title }) { if (downloadManager.mangaDownloadedTypes.any { it.titleName == item.title }) {
DownloadedType.Type.MANGA MediaType.MANGA
} else { } else {
DownloadedType.Type.NOVEL MediaType.NOVEL
} }
// Alert dialog to confirm deletion // Alert dialog to confirm deletion
val builder = val builder =
@@ -192,9 +206,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
builder.setPositiveButton("Yes") { _, _ -> builder.setPositiveButton("Yes") { _, _ ->
downloadManager.removeMedia(item.title, type) downloadManager.removeMedia(item.title, type)
getDownloads() getDownloads()
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
@@ -223,7 +234,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
gridView.setOnScrollListener(object : AbsListView.OnScrollListener { gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) { override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
// Implement behavior for different scroll states if needed
} }
override fun onScroll( override fun onScroll(
@@ -234,7 +244,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
) { ) {
val first = view.getChildAt(0) val first = view.getChildAt(0)
val visibility = first != null && first.top < 0 val visibility = first != null && first.top < 0
scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE scrollTop.isVisible = visibility
scrollTop.translationY = scrollTop.translationY =
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat() -(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
} }
@@ -246,7 +256,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
getDownloads() getDownloads()
adapter.notifyDataSetChanged()
} }
override fun onPause() { override fun onPause() {
@@ -266,46 +275,62 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private fun getDownloads() { private fun getDownloads() {
downloads = listOf() downloads = listOf()
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct() if (downloadsJob.isActive) {
val newMangaDownloads = mutableListOf<OfflineMangaModel>() downloadsJob.cancel()
for (title in mangaTitles) {
val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
val download = tDownloads.first()
val offlineMangaModel = loadOfflineMangaModel(download)
newMangaDownloads += offlineMangaModel
} }
downloads = newMangaDownloads downloads = listOf()
val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct() downloadsJob = Job()
val newNovelDownloads = mutableListOf<OfflineMangaModel>() CoroutineScope(Dispatchers.IO + downloadsJob).launch {
for (title in novelTitles) { val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.titleName.findValidName() }.distinct()
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.title == title } val newMangaDownloads = mutableListOf<OfflineMangaModel>()
val download = tDownloads.first() for (title in mangaTitles) {
val offlineMangaModel = loadOfflineMangaModel(download) val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.titleName == title }
newNovelDownloads += offlineMangaModel val download = tDownloads.first()
val offlineMangaModel = loadOfflineMangaModel(download)
newMangaDownloads += offlineMangaModel
}
downloads = newMangaDownloads
val novelTitles = downloadManager.novelDownloadedTypes.map { it.titleName }.distinct()
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
for (title in novelTitles) {
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.titleName == title }
val download = tDownloads.first()
val offlineMangaModel = loadOfflineMangaModel(download)
newNovelDownloads += offlineMangaModel
}
downloads += newNovelDownloads
withContext(Dispatchers.Main) {
adapter.setItems(downloads)
total.text =
if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
adapter.notifyDataSetChanged()
}
} }
downloads += newNovelDownloads
} }
private fun getMedia(downloadedType: DownloadedType): Media? { /**
val type = when (downloadedType.type) { * Load media.json file from the directory and convert it to Media class
DownloadedType.Type.MANGA -> "Manga" * @param downloadedType DownloadedType object
DownloadedType.Type.ANIME -> "Anime" * @return Media object
else -> "Novel" */
} private suspend fun getMedia(downloadedType: DownloadedType): Media? {
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
return try { return try {
val directory = getSubDirectory(
context ?: currContext()!!, downloadedType.type,
false, downloadedType.titleName
)
val gson = GsonBuilder() val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> { .registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl SChapterImpl() // Provide an instance of SChapterImpl
}) })
.create() .create()
val media = File(directory, "media.json") val media = directory?.findFile("media.json")
val mediaJson = media.readText() ?: return DownloadCompat.loadMediaCompat(downloadedType)
val mediaJson =
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
it?.readText()
}
gson.fromJson(mediaJson, Media::class.java) gson.fromJson(mediaJson, Media::class.java)
} catch (e: Exception) { } catch (e: Exception) {
Logger.log("Error loading media.json: ${e.message}") Logger.log("Error loading media.json: ${e.message}")
@@ -315,41 +340,38 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
} }
} }
private fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel { private suspend fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
val type = when (downloadedType.type) { val type = downloadedType.type.asText()
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 //load media.json and convert to media class with gson
try { try {
val directory = getSubDirectory(
context ?: currContext()!!, downloadedType.type,
false, downloadedType.titleName
)
val mediaModel = getMedia(downloadedType)!! val mediaModel = getMedia(downloadedType)!!
val cover = File(directory, "cover.jpg") val cover = directory?.findFile("cover.jpg")
val coverUri: Uri? = if (cover.exists()) { val coverUri: Uri? = if (cover?.exists() == true) {
Uri.fromFile(cover) cover.uri
} else null } else null
val banner = File(directory, "banner.jpg") val banner = directory?.findFile("banner.jpg")
val bannerUri: Uri? = if (banner.exists()) { val bannerUri: Uri? = if (banner?.exists() == true) {
Uri.fromFile(banner) banner.uri
} else null } else null
if (coverUri == null && bannerUri == null) throw Exception("No cover or banner found, probably compat")
val title = mediaModel.mainName() 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 = val isOngoing =
mediaModel.status == currActivity()!!.getString(R.string.status_releasing) mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
val isUserScored = mediaModel.userScore != 0 val isUserScored = mediaModel.userScore != 0
val readchapter = (mediaModel.userProgress ?: "~").toString() val readChapter = (mediaModel.userProgress ?: "~").toString()
val totalchapter = "${mediaModel.manga?.totalChapters ?: "??"}" val totalChapter = "${mediaModel.manga?.totalChapters ?: "??"}"
val chapters = " Chapters" val chapters = " Chapters"
return OfflineMangaModel( return OfflineMangaModel(
title, title,
score, score,
totalchapter, totalChapter,
readchapter, readChapter,
type, type,
chapters, chapters,
isOngoing, isOngoing,
@@ -358,21 +380,25 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
bannerUri bannerUri
) )
} catch (e: Exception) { } catch (e: Exception) {
Logger.log("Error loading media.json: ${e.message}") return try {
Logger.log(e) loadOfflineMangaModelCompat(downloadedType)
Injekt.get<CrashlyticsInterface>().logException(e) } catch (e: Exception) {
return OfflineMangaModel( Logger.log("Error loading media.json: ${e.message}")
"unknown", Logger.log(e)
"0", Injekt.get<CrashlyticsInterface>().logException(e)
"??", return OfflineMangaModel(
"??", "unknown",
"movie", "0",
"hmm", "??",
isOngoing = false, "??",
isUserScored = false, "movie",
null, "hmm",
null isOngoing = false,
) isUserScored = false,
null,
null
)
}
} }
} }
} }

View File

@@ -9,21 +9,25 @@ import android.content.IntentFilter
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.ServiceInfo import android.content.pm.ServiceInfo
import android.os.Build import android.os.Build
import android.os.Environment
import android.os.IBinder import android.os.IBinder
import android.widget.Toast import android.widget.Toast
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
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.documentfile.provider.DocumentFile
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.util.Logger import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.novel.NovelReadFragment import ani.dantotsu.media.novel.NovelReadFragment
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import com.anggrayudi.storage.file.forceDelete
import com.anggrayudi.storage.file.openOutputStream
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
@@ -33,7 +37,6 @@ import eu.kanade.tachiyomi.source.model.SChapterImpl
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -46,8 +49,6 @@ import okio.sink
import tachiyomi.core.util.lang.launchIO 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.FileOutputStream
import java.io.IOException import java.io.IOException
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
@@ -64,7 +65,7 @@ class NovelDownloaderService : Service() {
private val mutex = Mutex() private val mutex = Mutex()
private var isCurrentlyProcessing = false private var isCurrentlyProcessing = false
val networkHelper = Injekt.get<NetworkHelper>() private val networkHelper = Injekt.get<NetworkHelper>()
override fun onBind(intent: Intent?): IBinder? { override fun onBind(intent: Intent?): IBinder? {
// This is only required for bound services. // This is only required for bound services.
@@ -247,27 +248,30 @@ class NovelDownloaderService : Service() {
networkHelper.downloadClient.newCall(request).execute().use { response -> networkHelper.downloadClient.newCall(request).execute().use { response ->
// Ensure the response is successful and has a body // Ensure the response is successful and has a body
if (!response.isSuccessful || response.body == null) { if (!response.isSuccessful) {
throw IOException("Failed to download file: ${response.message}") throw IOException("Failed to download file: ${response.message}")
} }
val directory = getSubDirectory(
this@NovelDownloaderService,
MediaType.NOVEL,
false,
task.title,
task.chapter
) ?: throw Exception("Directory not found")
directory.findFile("0.epub")?.forceDelete(this@NovelDownloaderService)
val file = File( val file = directory.createFile("application/epub+zip", "0.epub")
this@NovelDownloaderService.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), ?: throw Exception("File not created")
"Dantotsu/Novel/${task.title}/${task.chapter}/0.epub"
)
// Create directories if they don't exist
file.parentFile?.takeIf { !it.exists() }?.mkdirs()
// Overwrite existing file
if (file.exists()) file.delete()
//download cover //download cover
task.coverUrl?.let { task.coverUrl?.let {
file.parentFile?.let { it1 -> downloadImage(it, it1, "cover.jpg") } file.parentFile?.let { it1 -> downloadImage(it, it1, "cover.jpg") }
} }
val outputStream =
this@NovelDownloaderService.contentResolver.openOutputStream(file.uri)
?: throw Exception("Could not open OutputStream")
val sink = file.sink().buffer() val sink = outputStream.sink().buffer()
val responseBody = response.body val responseBody = response.body
val totalBytes = responseBody.contentLength() val totalBytes = responseBody.contentLength()
var downloadedBytes = 0L var downloadedBytes = 0L
@@ -335,7 +339,7 @@ class NovelDownloaderService : Service() {
DownloadedType( DownloadedType(
task.title, task.title,
task.chapter, task.chapter,
DownloadedType.Type.NOVEL MediaType.NOVEL
) )
) )
broadcastDownloadFinished(task.originalLink) broadcastDownloadFinished(task.originalLink)
@@ -352,13 +356,16 @@ class NovelDownloaderService : Service() {
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
private fun saveMediaInfo(task: DownloadTask) { private fun saveMediaInfo(task: DownloadTask) {
launchIO { launchIO {
val directory = File( val directory =
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), getSubDirectory(
"Dantotsu/Novel/${task.title}" this@NovelDownloaderService,
) MediaType.NOVEL,
if (!directory.exists()) directory.mkdirs() false,
task.title
val file = File(directory, "media.json") ) ?: throw Exception("Directory not found")
directory.findFile("media.json")?.forceDelete(this@NovelDownloaderService)
val file = directory.createFile("application/json", "media.json")
?: throw Exception("File not created")
val gson = GsonBuilder() val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> { .registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl SChapterImpl() // Provide an instance of SChapterImpl
@@ -372,33 +379,47 @@ class NovelDownloaderService : Service() {
val jsonString = gson.toJson(media) val jsonString = gson.toJson(media)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
file.writeText(jsonString) try {
file.openOutputStream(this@NovelDownloaderService, false).use { output ->
if (output == null) throw Exception("Output stream is null")
output.write(jsonString.toByteArray())
}
} catch (e: android.system.ErrnoException) {
e.printStackTrace()
Toast.makeText(
this@NovelDownloaderService,
"Error while saving: ${e.localizedMessage}",
Toast.LENGTH_LONG
).show()
}
} }
} }
} }
} }
private suspend fun downloadImage(url: String, directory: File, name: String): String? = private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
withContext( withContext(
Dispatchers.IO Dispatchers.IO
) { ) {
var connection: HttpURLConnection? = null var connection: HttpURLConnection? = null
println("Downloading url $url") Logger.log("Downloading url $url")
try { try {
connection = URL(url).openConnection() as HttpURLConnection connection = URL(url).openConnection() as HttpURLConnection
connection.connect() connection.connect()
if (connection.responseCode != HttpURLConnection.HTTP_OK) { if (connection.responseCode != HttpURLConnection.HTTP_OK) {
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}") throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
} }
directory.findFile(name)?.forceDelete(this@NovelDownloaderService)
val file = File(directory, name) val file =
FileOutputStream(file).use { output -> directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
file.openOutputStream(this@NovelDownloaderService, false).use { output ->
if (output == null) throw Exception("Output stream is null")
connection.inputStream.use { input -> connection.inputStream.use { input ->
input.copyTo(output) input.copyTo(output)
} }
} }
return@withContext file.absolutePath return@withContext file.uri.toString()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@@ -473,7 +494,6 @@ class NovelDownloaderService : Service() {
} }
object NovelServiceDataSingleton { object NovelServiceDataSingleton {
var sourceMedia: Media? = null
var downloadQueue: Queue<NovelDownloaderService.DownloadTask> = ConcurrentLinkedQueue() var downloadQueue: Queue<NovelDownloaderService.DownloadTask> = ConcurrentLinkedQueue()
@Volatile @Volatile

View File

@@ -1,37 +0,0 @@
package ani.dantotsu.download.video
import android.app.Notification
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.Download
import androidx.media3.exoplayer.offline.DownloadManager
import androidx.media3.exoplayer.offline.DownloadNotificationHelper
import androidx.media3.exoplayer.offline.DownloadService
import androidx.media3.exoplayer.scheduler.PlatformScheduler
import androidx.media3.exoplayer.scheduler.Scheduler
import ani.dantotsu.R
@UnstableApi
class ExoplayerDownloadService :
DownloadService(1, 2000, "download_service", R.string.downloads, 0) {
companion object {
private const val JOB_ID = 1
private const val FOREGROUND_NOTIFICATION_ID = 1
}
override fun getDownloadManager(): DownloadManager = Helper.downloadManager(this)
override fun getScheduler(): Scheduler = PlatformScheduler(this, JOB_ID)
override fun getForegroundNotification(
downloads: MutableList<Download>,
notMetRequirements: Int
): Notification =
DownloadNotificationHelper(this, "download_service").buildProgressNotification(
this,
R.drawable.mono,
null,
null,
downloads,
notMetRequirements
)
}

View File

@@ -7,15 +7,10 @@ import android.app.AlertDialog
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build import android.os.Build
import android.util.Log
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.MimeTypes
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
@@ -23,11 +18,8 @@ import androidx.media3.datasource.HttpDataSource
import androidx.media3.datasource.cache.NoOpCacheEvictor 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.offline.Download import androidx.media3.exoplayer.offline.Download
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.scheduler.Requirements import androidx.media3.exoplayer.scheduler.Requirements
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.defaultHeaders import ani.dantotsu.defaultHeaders
@@ -35,93 +27,101 @@ import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.anime.AnimeDownloaderService import ani.dantotsu.download.anime.AnimeDownloaderService
import ani.dantotsu.download.anime.AnimeServiceDataSingleton import ani.dantotsu.download.anime.AnimeServiceDataSingleton
import ani.dantotsu.logError
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.okHttpClient import ani.dantotsu.media.MediaType
import ani.dantotsu.parsers.Subtitle import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.SubtitleType
import ani.dantotsu.parsers.Video import ani.dantotsu.parsers.Video
import ani.dantotsu.parsers.VideoType
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.util.Logger 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
import java.io.File import java.io.File
import java.io.IOException import java.util.concurrent.Executors
import java.util.concurrent.*
@SuppressLint("UnsafeOptInUsageError")
object Helper { object Helper {
@OptIn(UnstableApi::class)
private var simpleCache: SimpleCache? = null fun startAnimeDownloadService(
context: Context,
@SuppressLint("UnsafeOptInUsageError") title: String,
fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) { episode: String,
val dataSourceFactory = DataSource.Factory { video: Video,
val dataSource: HttpDataSource = subtitle: Subtitle? = null,
OkHttpDataSource.Factory(okHttpClient).createDataSource() sourceMedia: Media? = null,
defaultHeaders.forEach { episodeImage: String? = null
dataSource.setRequestProperty(it.key, it.value) ) {
} if (!isNotificationPermissionGranted(context)) {
video.file.headers.forEach { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
dataSource.setRequestProperty(it.key, it.value) ActivityCompat.requestPermissions(
} context as Activity,
dataSource arrayOf(Manifest.permission.POST_NOTIFICATIONS),
} 1
val mimeType = when (video.format) {
VideoType.M3U8 -> MimeTypes.APPLICATION_M3U8
VideoType.DASH -> MimeTypes.APPLICATION_MPD
else -> MimeTypes.APPLICATION_MP4
}
val builder = MediaItem.Builder().setUri(video.file.url).setMimeType(mimeType)
var sub: MediaItem.SubtitleConfiguration? = null
if (subtitle != null) {
sub = MediaItem.SubtitleConfiguration
.Builder(Uri.parse(subtitle.file.url))
.setSelectionFlags(C.SELECTION_FLAG_FORCED)
.setMimeType(
when (subtitle.type) {
SubtitleType.VTT -> MimeTypes.TEXT_VTT
SubtitleType.ASS -> MimeTypes.TEXT_SSA
SubtitleType.SRT -> MimeTypes.APPLICATION_SUBRIP
SubtitleType.UNKNOWN -> MimeTypes.TEXT_SSA
}
) )
.build() }
} }
if (sub != null) builder.setSubtitleConfigurations(mutableListOf(sub))
val mediaItem = builder.build()
val downloadHelper = DownloadHelper.forMediaItem(
context,
mediaItem,
DefaultRenderersFactory(context),
dataSourceFactory
)
downloadHelper.prepare(object : DownloadHelper.Callback {
override fun onPrepared(helper: DownloadHelper) {
helper.getDownloadRequest(null).let {
DownloadService.sendAddDownload(
context,
ExoplayerDownloadService::class.java,
it,
false
)
}
}
override fun onPrepareError(helper: DownloadHelper, e: IOException) { val animeDownloadTask = AnimeDownloaderService.AnimeDownloadTask(
logError(e) title,
episode,
video,
subtitle,
sourceMedia,
episodeImage
)
val downloadsManger = Injekt.get<DownloadsManager>()
val downloadCheck = downloadsManger
.queryDownload(title, episode, MediaType.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") { _, _ ->
PrefManager.getAnimeDownloadPreferences().edit()
.remove(animeDownloadTask.getTaskName())
.apply()
downloadsManger.removeDownload(
DownloadedType(
title,
episode,
MediaType.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
} }
}) }
} }
private fun isNotificationPermissionGranted(context: Context): Boolean {
private var download: DownloadManager? = null if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads" return ActivityCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
}
return true
}
@Synchronized @Synchronized
@UnstableApi @UnstableApi
@Deprecated("exoplayer download manager is no longer used")
fun downloadManager(context: Context): DownloadManager { fun downloadManager(context: Context): DownloadManager {
return download ?: let { return download ?: let {
val database = Injekt.get<StandaloneDatabaseProvider>() val database = Injekt.get<StandaloneDatabaseProvider>()
@@ -175,96 +175,7 @@ object Helper {
downloadManager downloadManager
} }
} }
@Deprecated("exoplayer download manager is no longer used")
private var downloadDirectory: File? = null
@Synchronized
private fun getDownloadDirectory(context: Context): File {
if (downloadDirectory == null) {
downloadDirectory = context.getExternalFilesDir(null)
if (downloadDirectory == null) {
downloadDirectory = context.filesDir
}
}
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) @OptIn(UnstableApi::class)
fun getSimpleCache(context: Context): SimpleCache { fun getSimpleCache(context: Context): SimpleCache {
return if (simpleCache == null) { return if (simpleCache == null) {
@@ -276,14 +187,23 @@ object Helper {
simpleCache!! simpleCache!!
} }
} }
@Synchronized
private fun isNotificationPermissionGranted(context: Context): Boolean { @Deprecated("exoplayer download manager is no longer used")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { private fun getDownloadDirectory(context: Context): File {
return ActivityCompat.checkSelfPermission( if (downloadDirectory == null) {
context, downloadDirectory = context.getExternalFilesDir(null)
Manifest.permission.POST_NOTIFICATIONS if (downloadDirectory == null) {
) == PackageManager.PERMISSION_GRANTED downloadDirectory = context.filesDir
}
} }
return true return downloadDirectory!!
} }
@Deprecated("exoplayer download manager is no longer used")
private var download: DownloadManager? = null
@Deprecated("exoplayer download manager is no longer used")
private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads"
@Deprecated("exoplayer download manager is no longer used")
private var simpleCache: SimpleCache? = null
@Deprecated("exoplayer download manager is no longer used")
private var downloadDirectory: File? = null
} }

View File

@@ -207,6 +207,21 @@ class AnimeFragment : Fragment() {
animePageAdapter.updateRecent(MediaAdaptor(0, it, requireActivity())) animePageAdapter.updateRecent(MediaAdaptor(0, it, requireActivity()))
} }
} }
model.getMovies().observe(viewLifecycleOwner) {
if (it != null) {
animePageAdapter.updateMovies(MediaAdaptor(0, it, requireActivity()))
}
}
model.getTopRated().observe(viewLifecycleOwner) {
if (it != null) {
animePageAdapter.updateTopRated(MediaAdaptor(0, it, requireActivity()))
}
}
model.getMostFav().observe(viewLifecycleOwner) {
if (it != null) {
animePageAdapter.updateMostFav(MediaAdaptor(0, it, requireActivity()))
}
}
if (animePageAdapter.trendingViewPager != null) { if (animePageAdapter.trendingViewPager != null) {
animePageAdapter.updateHeight() animePageAdapter.updateHeight()
model.getTrending().observe(viewLifecycleOwner) { model.getTrending().observe(viewLifecycleOwner) {
@@ -263,7 +278,7 @@ class AnimeFragment : Fragment() {
} }
model.loaded = true model.loaded = true
model.loadTrending(1) model.loadTrending(1)
model.loadUpdated() model.loadAll()
model.loadPopular( model.loadPopular(
"ANIME", sort = Anilist.sortBy[1], onList = PrefManager.getVal( "ANIME", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
PrefName.PopularAnimeList PrefName.PopularAnimeList

View File

@@ -4,12 +4,14 @@ import android.content.Intent
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
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
import android.view.animation.LayoutAnimationController import android.view.animation.LayoutAnimationController
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@@ -21,6 +23,7 @@ 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.databinding.LayoutTrendingBinding
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
@@ -41,6 +44,7 @@ import com.google.android.material.textfield.TextInputLayout
class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHolder>() { class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHolder>() {
val ready = MutableLiveData(false) val ready = MutableLiveData(false)
lateinit var binding: ItemAnimePageBinding lateinit var binding: ItemAnimePageBinding
private lateinit var trendingBinding: LayoutTrendingBinding
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
@@ -53,14 +57,15 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
override fun onBindViewHolder(holder: AnimePageViewHolder, position: Int) { override fun onBindViewHolder(holder: AnimePageViewHolder, position: Int) {
binding = holder.binding binding = holder.binding
trendingViewPager = binding.animeTrendingViewPager trendingBinding = LayoutTrendingBinding.bind(binding.root)
trendingViewPager = trendingBinding.trendingViewPager
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.animeSearchBar) val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.searchBar)
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
val materialCardView = val materialCardView =
holder.itemView.findViewById<MaterialCardView>(R.id.animeUserAvatarContainer) holder.itemView.findViewById<MaterialCardView>(R.id.userAvatarContainer)
materialCardView.setCardBackgroundColor(semiTransparentColor) materialCardView.setCardBackgroundColor(semiTransparentColor)
val typedValue = TypedValue() val typedValue = TypedValue()
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true) currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
@@ -69,16 +74,16 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000 textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000) materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
binding.animeTitleContainer.updatePadding(top = statusBarHeight) trendingBinding.titleContainer.updatePadding(top = statusBarHeight)
if (PrefManager.getVal(PrefName.SmallView)) binding.animeTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { if (PrefManager.getVal(PrefName.SmallView)) trendingBinding.trendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = (-108f).px bottomMargin = (-108f).px
} }
updateAvatar() updateAvatar()
binding.animeSearchBar.hint = "ANIME" trendingBinding.searchBar.hint = "ANIME"
binding.animeSearchBarText.setOnClickListener { trendingBinding.searchBarText.setOnClickListener {
ContextCompat.startActivity( ContextCompat.startActivity(
it.context, it.context,
Intent(it.context, SearchActivity::class.java).putExtra("type", "ANIME"), Intent(it.context, SearchActivity::class.java).putExtra("type", "ANIME"),
@@ -86,26 +91,28 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
) )
} }
binding.animeSearchBar.setEndIconOnClickListener { trendingBinding.userAvatar.setSafeOnClickListener {
binding.animeSearchBarText.performClick()
}
binding.animeUserAvatar.setSafeOnClickListener {
val dialogFragment = val dialogFragment =
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 -> trendingBinding.userAvatar.setOnLongClickListener { view ->
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
ContextCompat.startActivity( ContextCompat.startActivity(
view.context, view.context,
Intent(view.context, ProfileActivity::class.java) Intent(view.context, ProfileActivity::class.java)
.putExtra("userId", Anilist.userid),null .putExtra("userId", Anilist.userid), null
) )
false false
} }
binding.animeNotificationCount.visibility = if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE trendingBinding.searchBar.setEndIconOnClickListener {
binding.animeNotificationCount.text = Anilist.unreadNotificationCount.toString() trendingBinding.searchBar.performClick()
}
trendingBinding.notificationCount.visibility =
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
listOf( listOf(
binding.animePreviousSeason, binding.animePreviousSeason,
@@ -134,8 +141,7 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
) )
} }
binding.animeIncludeList.visibility = binding.animeIncludeList.isVisible = Anilist.userid != null
if (Anilist.userid != null) View.VISIBLE else View.GONE
binding.animeIncludeList.isChecked = PrefManager.getVal(PrefName.PopularAnimeList) binding.animeIncludeList.isChecked = PrefManager.getVal(PrefName.PopularAnimeList)
@@ -159,30 +165,31 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
} }
fun updateTrending(adaptor: MediaAdaptor) { fun updateTrending(adaptor: MediaAdaptor) {
binding.animeTrendingProgressBar.visibility = View.GONE trendingBinding.trendingProgressBar.visibility = View.GONE
binding.animeTrendingViewPager.adapter = adaptor trendingBinding.trendingViewPager.adapter = adaptor
binding.animeTrendingViewPager.offscreenPageLimit = 3 trendingBinding.trendingViewPager.offscreenPageLimit = 3
binding.animeTrendingViewPager.getChildAt(0).overScrollMode = RecyclerView.OVER_SCROLL_NEVER trendingBinding.trendingViewPager.getChildAt(0).overScrollMode =
binding.animeTrendingViewPager.setPageTransformer(MediaPageTransformer()) RecyclerView.OVER_SCROLL_NEVER
trendingBinding.trendingViewPager.setPageTransformer(MediaPageTransformer())
trendHandler = Handler(Looper.getMainLooper()) trendHandler = Handler(Looper.getMainLooper())
trendRun = Runnable { trendRun = Runnable {
binding.animeTrendingViewPager.currentItem = trendingBinding.trendingViewPager.currentItem += 1
binding.animeTrendingViewPager.currentItem + 1
} }
binding.animeTrendingViewPager.registerOnPageChangeCallback( trendingBinding.trendingViewPager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() { object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
super.onPageSelected(position) super.onPageSelected(position)
trendHandler!!.removeCallbacks(trendRun) trendHandler?.removeCallbacks(trendRun)
trendHandler!!.postDelayed(trendRun, 4000) if (PrefManager.getVal(PrefName.TrendingScroller)) {
trendHandler!!.postDelayed(trendRun, 4000)
}
} }
} }
) )
binding.animeTrendingViewPager.layoutAnimation = trendingBinding.trendingViewPager.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f) LayoutAnimationController(setSlideIn(), 0.25f)
binding.animeTitleContainer.startAnimation(setSlideUp()) trendingBinding.titleContainer.startAnimation(setSlideUp())
binding.animeListContainer.layoutAnimation = binding.animeListContainer.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f) LayoutAnimationController(setSlideIn(), 0.25f)
binding.animeSeasonsCont.layoutAnimation = binding.animeSeasonsCont.layoutAnimation =
@@ -190,36 +197,83 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
} }
fun updateRecent(adaptor: MediaAdaptor) { fun updateRecent(adaptor: MediaAdaptor) {
binding.animeUpdatedProgressBar.visibility = View.GONE binding.apply {
binding.animeUpdatedRecyclerView.adapter = adaptor init(
binding.animeUpdatedRecyclerView.layoutManager = adaptor,
animeUpdatedRecyclerView,
animeUpdatedProgressBar,
animeRecently
)
animePopular.visibility = View.VISIBLE
animePopular.startAnimation(setSlideUp())
if (adaptor.itemCount == 0) {
animeRecentlyContainer.visibility = View.GONE
}
}
}
fun updateMovies(adaptor: MediaAdaptor) {
binding.apply {
init(
adaptor,
animeMoviesRecyclerView,
animeMoviesProgressBar,
animeMovies
)
}
}
fun updateTopRated(adaptor: MediaAdaptor) {
binding.apply {
init(
adaptor,
animeTopRatedRecyclerView,
animeTopRatedProgressBar,
animeTopRated
)
}
}
fun updateMostFav(adaptor: MediaAdaptor) {
binding.apply {
init(
adaptor,
animeMostFavRecyclerView,
animeMostFavProgressBar,
animeMostFav
)
}
}
fun init(adaptor: MediaAdaptor, recyclerView: RecyclerView, progress: View, title: View) {
progress.visibility = View.GONE
recyclerView.adapter = adaptor
recyclerView.layoutManager =
LinearLayoutManager( LinearLayoutManager(
binding.animeUpdatedRecyclerView.context, recyclerView.context,
LinearLayoutManager.HORIZONTAL, LinearLayoutManager.HORIZONTAL,
false false
) )
binding.animeUpdatedRecyclerView.visibility = View.VISIBLE recyclerView.visibility = View.VISIBLE
title.visibility = View.VISIBLE
binding.animeRecently.visibility = View.VISIBLE title.startAnimation(setSlideUp())
binding.animeRecently.startAnimation(setSlideUp()) recyclerView.layoutAnimation =
binding.animeUpdatedRecyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f) LayoutAnimationController(setSlideIn(), 0.25f)
binding.animePopular.visibility = View.VISIBLE
binding.animePopular.startAnimation(setSlideUp())
} }
fun updateAvatar() { fun updateAvatar() {
if (Anilist.avatar != null && ready.value == true) { if (Anilist.avatar != null && ready.value == true) {
binding.animeUserAvatar.loadImage(Anilist.avatar) trendingBinding.userAvatar.loadImage(Anilist.avatar)
binding.animeUserAvatar.imageTintList = null trendingBinding.userAvatar.imageTintList = null
} }
} }
fun updateNotificationCount() { fun updateNotificationCount() {
if (this::binding.isInitialized) { if (this::binding.isInitialized) {
binding.animeNotificationCount.visibility = trendingBinding.notificationCount.visibility =
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
binding.animeNotificationCount.text = Anilist.unreadNotificationCount.toString() trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
} }
} }

View File

@@ -5,11 +5,13 @@ import android.content.Intent
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
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
import android.view.animation.LayoutAnimationController import android.view.animation.LayoutAnimationController
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@@ -78,10 +80,13 @@ class HomeFragment : Fragment() {
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 (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean)) binding.homeUserBg.pause() val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
blurImage(binding.homeUserBg, Anilist.bg) blurImage(
if (bannerAnimations) binding.homeUserBg else binding.homeUserBgNoKen,
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.isVisible = Anilist.unreadNotificationCount > 0
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString() binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
binding.homeAnimeList.setOnClickListener { binding.homeAnimeList.setOnClickListener {
@@ -123,9 +128,10 @@ class HomeFragment : Fragment() {
) )
} }
binding.homeUserAvatarContainer.setOnLongClickListener { binding.homeUserAvatarContainer.setOnLongClickListener {
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
ContextCompat.startActivity( ContextCompat.startActivity(
requireContext(), Intent(requireContext(), ProfileActivity::class.java) requireContext(), Intent(requireContext(), ProfileActivity::class.java)
.putExtra("userId", Anilist.userid),null .putExtra("userId", Anilist.userid), null
) )
false false
} }
@@ -134,6 +140,7 @@ class HomeFragment : Fragment() {
bottomMargin = navBarHeight bottomMargin = navBarHeight
} }
binding.homeUserBg.updateLayoutParams { height += statusBarHeight } binding.homeUserBg.updateLayoutParams { height += statusBarHeight }
binding.homeUserBgNoKen.updateLayoutParams { height += statusBarHeight }
binding.homeTopContainer.updatePadding(top = statusBarHeight) binding.homeTopContainer.updatePadding(top = statusBarHeight)
var reached = false var reached = false
@@ -372,10 +379,11 @@ 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) { if (_binding != null) {
binding.homeNotificationCount.visibility = if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString() binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
} }
super.onResume() super.onResume()

View File

@@ -160,11 +160,37 @@ class MangaFragment : Fragment() {
}) })
mangaPageAdapter.ready.observe(viewLifecycleOwner) { i -> mangaPageAdapter.ready.observe(viewLifecycleOwner) { i ->
if (i == true) { if (i == true) {
model.getTrendingNovel().observe(viewLifecycleOwner) { model.getPopularNovel().observe(viewLifecycleOwner) {
if (it != null) { if (it != null) {
mangaPageAdapter.updateNovel(MediaAdaptor(0, it, requireActivity())) mangaPageAdapter.updateNovel(MediaAdaptor(0, it, requireActivity()))
} }
} }
model.getPopularManga().observe(viewLifecycleOwner) {
if (it != null) {
mangaPageAdapter.updateTrendingManga(MediaAdaptor(0, it, requireActivity()))
}
}
model.getPopularManhwa().observe(viewLifecycleOwner) {
if (it != null) {
mangaPageAdapter.updateTrendingManhwa(
MediaAdaptor(
0,
it,
requireActivity()
)
)
}
}
model.getTopRated().observe(viewLifecycleOwner) {
if (it != null) {
mangaPageAdapter.updateTopRated(MediaAdaptor(0, it, requireActivity()))
}
}
model.getMostFav().observe(viewLifecycleOwner) {
if (it != null) {
mangaPageAdapter.updateMostFav(MediaAdaptor(0, it, requireActivity()))
}
}
if (mangaPageAdapter.trendingViewPager != null) { if (mangaPageAdapter.trendingViewPager != null) {
mangaPageAdapter.updateHeight() mangaPageAdapter.updateHeight()
model.getTrending().observe(viewLifecycleOwner) { model.getTrending().observe(viewLifecycleOwner) {
@@ -237,7 +263,7 @@ class MangaFragment : Fragment() {
} }
model.loaded = true model.loaded = true
model.loadTrending() model.loadTrending()
model.loadTrendingNovel() model.loadAll()
model.loadPopular( model.loadPopular(
"MANGA", sort = Anilist.sortBy[1], onList = PrefManager.getVal( "MANGA", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
PrefName.PopularMangaList PrefName.PopularMangaList

View File

@@ -4,12 +4,14 @@ import android.content.Intent
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
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
import android.view.animation.LayoutAnimationController import android.view.animation.LayoutAnimationController
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@@ -21,6 +23,7 @@ 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.databinding.LayoutTrendingBinding
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
@@ -40,6 +43,7 @@ import com.google.android.material.textfield.TextInputLayout
class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHolder>() { class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHolder>() {
val ready = MutableLiveData(false) val ready = MutableLiveData(false)
lateinit var binding: ItemMangaPageBinding lateinit var binding: ItemMangaPageBinding
private lateinit var trendingBinding: LayoutTrendingBinding
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
@@ -52,33 +56,34 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
override fun onBindViewHolder(holder: MangaPageViewHolder, position: Int) { override fun onBindViewHolder(holder: MangaPageViewHolder, position: Int) {
binding = holder.binding binding = holder.binding
trendingViewPager = binding.mangaTrendingViewPager trendingBinding = LayoutTrendingBinding.bind(binding.root)
trendingViewPager = trendingBinding.trendingViewPager
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.mangaSearchBar) val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.searchBar)
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
val materialCardView = val materialCardView =
holder.itemView.findViewById<MaterialCardView>(R.id.mangaUserAvatarContainer) holder.itemView.findViewById<MaterialCardView>(R.id.userAvatarContainer)
materialCardView.setCardBackgroundColor(semiTransparentColor) materialCardView.setCardBackgroundColor(semiTransparentColor)
val typedValue = TypedValue() val typedValue = TypedValue()
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() textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt()) materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
binding.mangaTitleContainer.updatePadding(top = statusBarHeight) trendingBinding.titleContainer.updatePadding(top = statusBarHeight)
if (PrefManager.getVal(PrefName.SmallView)) binding.mangaTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { if (PrefManager.getVal(PrefName.SmallView)) trendingBinding.trendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = (-108f).px bottomMargin = (-108f).px
} }
updateAvatar() updateAvatar()
binding.mangaNotificationCount.visibility = if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
binding.mangaNotificationCount.text = Anilist.unreadNotificationCount.toString() trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
binding.mangaSearchBar.hint = "MANGA" trendingBinding.searchBar.hint = "MANGA"
binding.mangaSearchBarText.setOnClickListener { trendingBinding.searchBarText.setOnClickListener {
ContextCompat.startActivity( ContextCompat.startActivity(
it.context, it.context,
Intent(it.context, SearchActivity::class.java).putExtra("type", "MANGA"), Intent(it.context, SearchActivity::class.java).putExtra("type", "MANGA"),
@@ -86,22 +91,23 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
) )
} }
binding.mangaUserAvatar.setSafeOnClickListener { trendingBinding.userAvatar.setSafeOnClickListener {
val dialogFragment = val dialogFragment =
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 -> trendingBinding.userAvatar.setOnLongClickListener { view ->
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
ContextCompat.startActivity( ContextCompat.startActivity(
view.context, view.context,
Intent(view.context, ProfileActivity::class.java) Intent(view.context, ProfileActivity::class.java)
.putExtra("userId", Anilist.userid),null .putExtra("userId", Anilist.userid), null
) )
false false
} }
binding.mangaSearchBar.setEndIconOnClickListener { trendingBinding.searchBar.setEndIconOnClickListener {
binding.mangaSearchBarText.performClick() trendingBinding.searchBarText.performClick()
} }
binding.mangaGenreImage.loadImage("https://s4.anilist.co/file/anilistcdn/media/manga/banner/105778-wk5qQ7zAaTGl.jpg") binding.mangaGenreImage.loadImage("https://s4.anilist.co/file/anilistcdn/media/manga/banner/105778-wk5qQ7zAaTGl.jpg")
@@ -125,8 +131,7 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
) )
} }
binding.mangaIncludeList.visibility = binding.mangaIncludeList.isVisible = Anilist.userid != null
if (Anilist.userid != null) View.VISIBLE else View.GONE
binding.mangaIncludeList.isChecked = PrefManager.getVal(PrefName.PopularMangaList) binding.mangaIncludeList.isChecked = PrefManager.getVal(PrefName.PopularMangaList)
@@ -148,63 +153,121 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
} }
fun updateTrending(adaptor: MediaAdaptor) { fun updateTrending(adaptor: MediaAdaptor) {
binding.mangaTrendingProgressBar.visibility = View.GONE trendingBinding.trendingProgressBar.visibility = View.GONE
binding.mangaTrendingViewPager.adapter = adaptor trendingBinding.trendingViewPager.adapter = adaptor
binding.mangaTrendingViewPager.offscreenPageLimit = 3 trendingBinding.trendingViewPager.offscreenPageLimit = 3
binding.mangaTrendingViewPager.getChildAt(0).overScrollMode = RecyclerView.OVER_SCROLL_NEVER trendingBinding.trendingViewPager.getChildAt(0).overScrollMode =
binding.mangaTrendingViewPager.setPageTransformer(MediaPageTransformer()) RecyclerView.OVER_SCROLL_NEVER
trendingBinding.trendingViewPager.setPageTransformer(MediaPageTransformer())
trendHandler = Handler(Looper.getMainLooper()) trendHandler = Handler(Looper.getMainLooper())
trendRun = Runnable { trendRun = Runnable {
binding.mangaTrendingViewPager.currentItem += 1 trendingBinding.trendingViewPager.currentItem += 1
} }
binding.mangaTrendingViewPager.registerOnPageChangeCallback( trendingBinding.trendingViewPager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() { object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
super.onPageSelected(position) super.onPageSelected(position)
trendHandler!!.removeCallbacks(trendRun) trendHandler?.removeCallbacks(trendRun)
trendHandler!!.postDelayed(trendRun, 4000) if (PrefManager.getVal(PrefName.TrendingScroller))
trendHandler!!.postDelayed(trendRun, 4000)
} }
} }
) )
binding.mangaTrendingViewPager.layoutAnimation = trendingBinding.trendingViewPager.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f) LayoutAnimationController(setSlideIn(), 0.25f)
binding.mangaTitleContainer.startAnimation(setSlideUp()) trendingBinding.titleContainer.startAnimation(setSlideUp())
binding.mangaListContainer.layoutAnimation = binding.mangaListContainer.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f) LayoutAnimationController(setSlideIn(), 0.25f)
}
fun updateTrendingManga(adaptor: MediaAdaptor) {
binding.apply {
init(
adaptor,
mangaTrendingMangaRecyclerView,
mangaTrendingMangaProgressBar,
mangaTrendingManga
)
}
}
fun updateTrendingManhwa(adaptor: MediaAdaptor) {
binding.apply {
init(
adaptor,
mangaTrendingManhwaRecyclerView,
mangaTrendingManhwaProgressBar,
mangaTrendingManhwa
)
}
} }
fun updateNovel(adaptor: MediaAdaptor) { fun updateNovel(adaptor: MediaAdaptor) {
binding.mangaNovelProgressBar.visibility = View.GONE binding.apply {
binding.mangaNovelRecyclerView.adapter = adaptor init(
binding.mangaNovelRecyclerView.layoutManager = adaptor,
mangaNovelRecyclerView,
mangaNovelProgressBar,
mangaNovel
)
}
}
fun updateTopRated(adaptor: MediaAdaptor) {
binding.apply {
init(
adaptor,
mangaTopRatedRecyclerView,
mangaTopRatedProgressBar,
mangaTopRated
)
}
}
fun updateMostFav(adaptor: MediaAdaptor) {
binding.apply {
init(
adaptor,
mangaMostFavRecyclerView,
mangaMostFavProgressBar,
mangaMostFav
)
mangaPopular.visibility = View.VISIBLE
mangaPopular.startAnimation(setSlideUp())
}
}
fun init(adaptor: MediaAdaptor, recyclerView: RecyclerView, progress: View, title: View) {
progress.visibility = View.GONE
recyclerView.adapter = adaptor
recyclerView.layoutManager =
LinearLayoutManager( LinearLayoutManager(
binding.mangaNovelRecyclerView.context, recyclerView.context,
LinearLayoutManager.HORIZONTAL, LinearLayoutManager.HORIZONTAL,
false false
) )
binding.mangaNovelRecyclerView.visibility = View.VISIBLE recyclerView.visibility = View.VISIBLE
title.visibility = View.VISIBLE
binding.mangaNovel.visibility = View.VISIBLE title.startAnimation(setSlideUp())
binding.mangaNovel.startAnimation(setSlideUp()) recyclerView.layoutAnimation =
binding.mangaNovelRecyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f) LayoutAnimationController(setSlideIn(), 0.25f)
binding.mangaPopular.visibility = View.VISIBLE
binding.mangaPopular.startAnimation(setSlideUp())
} }
fun updateAvatar() { fun updateAvatar() {
if (Anilist.avatar != null && ready.value == true) { if (Anilist.avatar != null && ready.value == true) {
binding.mangaUserAvatar.loadImage(Anilist.avatar) trendingBinding.userAvatar.loadImage(Anilist.avatar)
binding.mangaUserAvatar.imageTintList = null trendingBinding.userAvatar.imageTintList = null
} }
} }
fun updateNotificationCount() { fun updateNotificationCount() {
if (this::binding.isInitialized) { if (this::binding.isInitialized) {
binding.mangaNotificationCount.visibility = trendingBinding.notificationCount.visibility =
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
binding.mangaNotificationCount.text = Anilist.unreadNotificationCount.toString() trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
} }
} }

View File

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

View File

@@ -12,6 +12,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.EmptyAdapter import ani.dantotsu.EmptyAdapter
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.Refresh import ani.dantotsu.Refresh
@@ -32,7 +33,6 @@ class AuthorActivity : AppCompatActivity() {
private val model: OtherDetailsViewModel by viewModels() private val model: OtherDetailsViewModel by viewModels()
private var author: Author? = null private var author: Author? = null
private var loaded = false private var loaded = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -55,14 +55,15 @@ class AuthorActivity : AppCompatActivity() {
binding.studioClose.setOnClickListener { binding.studioClose.setOnClickListener {
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
} }
model.getAuthor().observe(this) { model.getAuthor().observe(this) {
if (it != null) { if (it != null) {
author = it author = it
loaded = true loaded = true
binding.studioProgressBar.visibility = View.GONE binding.studioProgressBar.visibility = View.GONE
binding.studioRecycler.visibility = View.VISIBLE binding.studioRecycler.visibility = View.VISIBLE
if (author!!.yearMedia.isNullOrEmpty()) {
binding.studioRecycler.visibility = View.GONE
}
val titlePosition = arrayListOf<Int>() val titlePosition = arrayListOf<Int>()
val concatAdapter = ConcatAdapter() val concatAdapter = ConcatAdapter()
val map = author!!.yearMedia ?: return@observe val map = author!!.yearMedia ?: return@observe
@@ -89,9 +90,19 @@ class AuthorActivity : AppCompatActivity() {
concatAdapter.addAdapter(MediaAdaptor(0, medias, this, true)) concatAdapter.addAdapter(MediaAdaptor(0, medias, this, true))
concatAdapter.addAdapter(EmptyAdapter(empty)) concatAdapter.addAdapter(EmptyAdapter(empty))
} }
binding.studioRecycler.adapter = concatAdapter binding.studioRecycler.adapter = concatAdapter
binding.studioRecycler.layoutManager = gridLayoutManager binding.studioRecycler.layoutManager = gridLayoutManager
binding.charactersRecycler.visibility = View.VISIBLE
binding.charactersText.visibility = View.VISIBLE
binding.charactersRecycler.adapter =
CharacterAdapter(author!!.character ?: arrayListOf())
binding.charactersRecycler.layoutManager =
LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
if (author!!.character.isNullOrEmpty()) {
binding.charactersRecycler.visibility = View.GONE
binding.charactersText.visibility = View.GONE
}
} }
} }
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) } val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) }

View File

@@ -1,6 +1,5 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.view.LayoutInflater import android.view.LayoutInflater
@@ -16,7 +15,7 @@ import ani.dantotsu.setAnimation
import java.io.Serializable import java.io.Serializable
class AuthorAdapter( class AuthorAdapter(
private val authorList: ArrayList<Author> private val authorList: ArrayList<Author>,
) : RecyclerView.Adapter<AuthorAdapter.AuthorViewHolder>() { ) : RecyclerView.Adapter<AuthorAdapter.AuthorViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AuthorViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AuthorViewHolder {
val binding = val binding =
@@ -24,8 +23,7 @@ class AuthorAdapter(
return AuthorViewHolder(binding) return AuthorViewHolder(binding)
} }
@SuppressLint("SetTextI18n") override fun onBindViewHolder(holder: AuthorViewHolder, position: Int) {
override fun onBindViewHolder(holder:AuthorViewHolder, position: Int) {
val binding = holder.binding val binding = holder.binding
setAnimation(binding.root.context, holder.binding.root) setAnimation(binding.root.context, holder.binding.root)
val author = authorList[position] val author = authorList[position]

View File

@@ -1,12 +1,10 @@
package ani.dantotsu.media package ani.dantotsu.media
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.ViewGroup
import android.view.Window import android.view.Window
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
@@ -16,8 +14,8 @@ 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.hideSystemBarsExtendView
import ani.dantotsu.media.user.ListViewPagerAdapter import ani.dantotsu.media.user.ListViewPagerAdapter
import ani.dantotsu.navBarHeight
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
@@ -34,7 +32,6 @@ class CalendarActivity : AppCompatActivity() {
private var selectedTabIdx = 1 private var selectedTabIdx = 1
private val model: OtherDetailsViewModel by viewModels() private val model: OtherDetailsViewModel by viewModels()
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -45,13 +42,6 @@ class CalendarActivity : AppCompatActivity() {
val typedValue = TypedValue() val typedValue = TypedValue()
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true) theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
val primaryColor = typedValue.data val primaryColor = typedValue.data
val typedValue2 = TypedValue()
theme.resolveAttribute(
com.google.android.material.R.attr.colorOnBackground,
typedValue2,
true
)
val titleTextColor = typedValue2.data
val typedValue3 = TypedValue() val typedValue3 = TypedValue()
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue3, true) theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue3, true)
val primaryTextColor = typedValue3.data val primaryTextColor = typedValue3.data
@@ -74,10 +64,7 @@ class CalendarActivity : AppCompatActivity() {
} else { } else {
binding.root.fitsSystemWindows = false binding.root.fitsSystemWindows = false
requestWindowFeature(Window.FEATURE_NO_TITLE) requestWindowFeature(Window.FEATURE_NO_TITLE)
window.setFlags( hideSystemBarsExtendView()
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight topMargin = statusBarHeight
} }

View File

@@ -14,5 +14,6 @@ data class Character(
var age: String? = null, var age: String? = null,
var gender: String? = null, var gender: String? = null,
var dateOfBirth: FuzzyDate? = null, var dateOfBirth: FuzzyDate? = null,
var roles: ArrayList<Media>? = null var roles: ArrayList<Media>? = null,
val voiceActor: ArrayList<Author>? = null,
) : Serializable ) : Serializable

View File

@@ -1,6 +1,5 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.view.LayoutInflater import android.view.LayoutInflater
@@ -24,12 +23,13 @@ class CharacterAdapter(
return CharacterViewHolder(binding) return CharacterViewHolder(binding)
} }
@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) setAnimation(binding.root.context, holder.binding.root)
val character = characterList[position] val character = characterList[position]
binding.itemCompactRelation.text = character.role + " " val whitespace = "${character.role} "
character.voiceActor
binding.itemCompactRelation.text = whitespace
binding.itemCompactImage.loadImage(character.image) binding.itemCompactImage.loadImage(character.image)
binding.itemCompactTitle.text = character.name binding.itemCompactTitle.text = character.name
} }

View File

@@ -8,6 +8,7 @@ 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.math.MathUtils.clamp import androidx.core.math.MathUtils.clamp
import androidx.core.view.isGone
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@@ -94,7 +95,8 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
} }
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
character.isFav = Anilist.query.isUserFav(AnilistMutations.FavType.CHARACTER, character.id) character.isFav =
Anilist.query.isUserFav(AnilistMutations.FavType.CHARACTER, character.id)
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
binding.characterFav.setImageResource( binding.characterFav.setImageResource(
@@ -152,7 +154,7 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
} }
override fun onResume() { override fun onResume() {
binding.characterProgress.visibility = if (!loaded) View.VISIBLE else View.GONE binding.characterProgress.isGone = loaded
super.onResume() super.onResume()
} }

View File

@@ -1,9 +1,10 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
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.currActivity import ani.dantotsu.currActivity
@@ -20,23 +21,36 @@ class CharacterDetailsAdapter(private val character: Character, private val acti
return GenreViewHolder(binding) return GenreViewHolder(binding)
} }
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) { override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
val binding = holder.binding val binding = holder.binding
val desc = val desc =
(if (character.age != "null") currActivity()!!.getString(R.string.age) + " " + character.age else "") + (if (character.age != "null") "${currActivity()!!.getString(R.string.age)} ${character.age}" else "") +
(if (character.dateOfBirth.toString() != "") currActivity()!!.getString(R.string.birthday) + " " + character.dateOfBirth.toString() else "") + (if (character.dateOfBirth.toString() != "")
(if (character.gender != "null") currActivity()!!.getString(R.string.gender) + " " + when (character.gender) { "${currActivity()!!.getString(R.string.birthday)} ${character.dateOfBirth.toString()}" else "") +
"Male" -> currActivity()!!.getString(R.string.male) (if (character.gender != "null")
"Female" -> currActivity()!!.getString(R.string.female) currActivity()!!.getString(R.string.gender) + " " + when (character.gender) {
else -> character.gender currActivity()!!.getString(R.string.male) -> currActivity()!!.getString(
} else "") + "\n" + character.description R.string.male
)
currActivity()!!.getString(R.string.female) -> currActivity()!!.getString(
R.string.female
)
else -> character.gender
} else "") + "\n" + character.description
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.replace("~!", "||").replace("!~", "||")) markWon.setMarkdown(binding.characterDesc, desc.replace("~!", "||").replace("!~", "||"))
binding.voiceActorRecycler.adapter = AuthorAdapter(character.voiceActor ?: arrayListOf())
binding.voiceActorRecycler.layoutManager = LinearLayoutManager(
activity, LinearLayoutManager.HORIZONTAL, false
)
if (binding.voiceActorRecycler.adapter!!.itemCount == 0) {
binding.voiceActorContainer.visibility = View.GONE
}
} }
override fun getItemCount(): Int = 1 override fun getItemCount(): Int = 1

View File

@@ -7,6 +7,7 @@ import ani.dantotsu.connections.anilist.api.MediaList
import ani.dantotsu.connections.anilist.api.MediaType import ani.dantotsu.connections.anilist.api.MediaType
import ani.dantotsu.media.anime.Anime import ani.dantotsu.media.anime.Anime
import ani.dantotsu.media.manga.Manga import ani.dantotsu.media.manga.Manga
import ani.dantotsu.profile.User
import java.io.Serializable import java.io.Serializable
import ani.dantotsu.connections.anilist.api.Media as ApiMedia import ani.dantotsu.connections.anilist.api.Media as ApiMedia
@@ -25,7 +26,7 @@ data class Media(
var cover: String? = null, var cover: String? = null,
var banner: String? = null, var banner: String? = null,
var relation: String? = null, var relation: String? = null,
var popularity: Int? = null, var favourites: Int? = null,
var isAdult: Boolean, var isAdult: Boolean,
var isFav: Boolean = false, var isFav: Boolean = false,
@@ -56,6 +57,9 @@ data class Media(
var trailer: String? = null, var trailer: String? = null,
var startDate: FuzzyDate? = null, var startDate: FuzzyDate? = null,
var endDate: FuzzyDate? = null, var endDate: FuzzyDate? = null,
var popularity: Int? = null,
var timeUntilAiring: Long? = null,
var characters: ArrayList<Character>? = null, var characters: ArrayList<Character>? = null,
var staff: ArrayList<Author>? = null, var staff: ArrayList<Author>? = null,
@@ -63,7 +67,7 @@ data class Media(
var sequel: Media? = null, var sequel: Media? = null,
var relations: ArrayList<Media>? = null, var relations: ArrayList<Media>? = null,
var recommendations: ArrayList<Media>? = null, var recommendations: ArrayList<Media>? = null,
var users: ArrayList<User>? = null,
var vrvId: String? = null, var vrvId: String? = null,
var crunchySlug: String? = null, var crunchySlug: String? = null,
@@ -83,7 +87,7 @@ data class Media(
name = apiMedia.title!!.english, name = apiMedia.title!!.english,
nameRomaji = apiMedia.title!!.romaji, nameRomaji = apiMedia.title!!.romaji,
userPreferredName = apiMedia.title!!.userPreferred, userPreferredName = apiMedia.title!!.userPreferred,
cover = apiMedia.coverImage?.large, cover = apiMedia.coverImage?.large ?: apiMedia.coverImage?.medium,
banner = apiMedia.bannerImage, banner = apiMedia.bannerImage,
status = apiMedia.status.toString(), status = apiMedia.status.toString(),
isFav = apiMedia.isFavourite!!, isFav = apiMedia.isFavourite!!,
@@ -95,6 +99,8 @@ data class Media(
meanScore = apiMedia.meanScore, meanScore = apiMedia.meanScore,
startDate = apiMedia.startDate, startDate = apiMedia.startDate,
endDate = apiMedia.endDate, endDate = apiMedia.endDate,
favourites = apiMedia.favourites,
timeUntilAiring = apiMedia.nextAiringEpisode?.timeUntilAiring?.let { it.toLong() * 1000 },
anime = if (apiMedia.type == MediaType.ANIME) Anime( anime = if (apiMedia.type == MediaType.ANIME) Anime(
totalEpisodes = apiMedia.episodes, totalEpisodes = apiMedia.episodes,
nextAiringEpisode = apiMedia.nextAiringEpisode?.episode?.minus(1) nextAiringEpisode = apiMedia.nextAiringEpisode?.episode?.minus(1)
@@ -109,7 +115,8 @@ 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() this.genres =
mediaList.media?.genres?.toMutableList() as? ArrayList<String>? ?: arrayListOf()
} }
constructor(mediaEdge: MediaEdge) : this(mediaEdge.node!!) { constructor(mediaEdge: MediaEdge) : this(mediaEdge.node!!) {

View File

@@ -1,8 +1,6 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
@@ -15,25 +13,25 @@ import android.widget.ImageView
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.app.ActivityOptionsCompat 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.ViewCompat
import androidx.core.view.isVisible
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
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.* import ani.dantotsu.R
import ani.dantotsu.blurImage
import ani.dantotsu.currActivity
import ani.dantotsu.databinding.ItemMediaCompactBinding 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.loadImage
import ani.dantotsu.setAnimation
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.request.RequestOptions
import com.flaviofaria.kenburnsview.RandomTransitionGenerator import com.flaviofaria.kenburnsview.RandomTransitionGenerator
import jp.wasabeef.glide.transformations.BlurTransformation
import java.io.Serializable import java.io.Serializable
@@ -85,7 +83,7 @@ class MediaAdaptor(
} }
@SuppressLint("SetTextI18n", "ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (type) { when (type) {
0 -> { 0 -> {
@@ -94,8 +92,8 @@ class MediaAdaptor(
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)
b.itemCompactOngoing.visibility = b.itemCompactOngoing.isVisible =
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE media.status == currActivity()!!.getString(R.string.status_releasing)
b.itemCompactTitle.text = media.userPreferredName b.itemCompactTitle.text = media.userPreferredName
b.itemCompactScore.text = b.itemCompactScore.text =
((if (media.userScore == 0) (media.meanScore ((if (media.userScore == 0) (media.meanScore
@@ -140,8 +138,8 @@ class MediaAdaptor(
if (media != null) { if (media != null) {
b.itemCompactImage.loadImage(media.cover) b.itemCompactImage.loadImage(media.cover)
blurImage(b.itemCompactBanner, media.banner ?: media.cover) blurImage(b.itemCompactBanner, media.banner ?: media.cover)
b.itemCompactOngoing.visibility = b.itemCompactOngoing.isVisible =
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE media.status == currActivity()!!.getString(R.string.status_releasing)
b.itemCompactTitle.text = media.userPreferredName b.itemCompactTitle.text = media.userPreferredName
b.itemCompactScore.text = b.itemCompactScore.text =
((if (media.userScore == 0) (media.meanScore ((if (media.userScore == 0) (media.meanScore
@@ -151,25 +149,30 @@ class MediaAdaptor(
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score) (if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
) )
if (media.anime != null) { if (media.anime != null) {
b.itemTotal.text = " " + if ((media.anime.totalEpisodes val itemTotal = " " + if ((media.anime.totalEpisodes
?: 0) != 1 ?: 0) != 1
) currActivity()!!.getString(R.string.episode_plural) ) currActivity()!!.getString(R.string.episode_plural) else currActivity()!!.getString(
else currActivity()!!.getString(R.string.episode_singular) R.string.episode_singular
)
b.itemTotal.text = itemTotal
b.itemCompactTotal.text = b.itemCompactTotal.text =
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes
?: "??").toString()) else (media.anime.totalEpisodes ?: "??").toString()) else (media.anime.totalEpisodes
?: "??").toString() ?: "??").toString()
} else if (media.manga != null) { } else if (media.manga != null) {
b.itemTotal.text = " " + if ((media.manga.totalChapters val itemTotal = " " + if ((media.manga.totalChapters
?: 0) != 1 ?: 0) != 1
) currActivity()!!.getString(R.string.chapter_plural) ) currActivity()!!.getString(R.string.chapter_plural) else currActivity()!!.getString(
else currActivity()!!.getString(R.string.chapter_singular) R.string.chapter_singular
)
b.itemTotal.text = itemTotal
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}" b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
} }
@SuppressLint("NotifyDataSetChanged")
if (position == mediaList!!.size - 2 && viewPager != null) viewPager.post { if (position == mediaList!!.size - 2 && viewPager != null) viewPager.post {
val start = mediaList.size
mediaList.addAll(mediaList) mediaList.addAll(mediaList)
notifyDataSetChanged() val end = mediaList.size - start
notifyItemRangeInserted(start, end)
} }
} }
} }
@@ -178,6 +181,7 @@ 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) val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
b.itemCompactImage.loadImage(media.cover) b.itemCompactImage.loadImage(media.cover)
if (bannerAnimations) if (bannerAnimations)
@@ -187,9 +191,12 @@ class MediaAdaptor(
AccelerateDecelerateInterpolator() AccelerateDecelerateInterpolator()
) )
) )
blurImage(b.itemCompactBanner, media.banner ?: media.cover) blurImage(
b.itemCompactOngoing.visibility = if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen,
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE media.banner ?: media.cover
)
b.itemCompactOngoing.isVisible =
media.status == currActivity()!!.getString(R.string.status_releasing)
b.itemCompactTitle.text = media.userPreferredName b.itemCompactTitle.text = media.userPreferredName
b.itemCompactScore.text = b.itemCompactScore.text =
((if (media.userScore == 0) (media.meanScore ((if (media.userScore == 0) (media.meanScore
@@ -236,9 +243,12 @@ class MediaAdaptor(
AccelerateDecelerateInterpolator() AccelerateDecelerateInterpolator()
) )
) )
blurImage(b.itemCompactBanner, media.banner ?: media.cover) blurImage(
b.itemCompactOngoing.visibility = if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen,
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE media.banner ?: media.cover
)
b.itemCompactOngoing.isVisible =
media.status == currActivity()!!.getString(R.string.status_releasing)
b.itemCompactTitle.text = media.userPreferredName b.itemCompactTitle.text = media.userPreferredName
b.itemCompactScore.text = b.itemCompactScore.text =
((if (media.userScore == 0) (media.meanScore ((if (media.userScore == 0) (media.meanScore

View File

@@ -2,9 +2,8 @@ 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.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.util.TypedValue import android.util.TypedValue
@@ -12,18 +11,19 @@ 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.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources 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.isVisible
import androidx.core.view.marginBottom import androidx.core.view.marginBottom
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updateMargins
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
@@ -54,6 +54,7 @@ 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 ani.dantotsu.util.LauncherWrapper
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 kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -62,20 +63,21 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import nl.joery.animatedbottombar.AnimatedBottomBar
import kotlin.math.abs import kotlin.math.abs
class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener { class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
lateinit var launcher: LauncherWrapper
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()
lateinit var tabLayout: TripleNavAdapter
var selected = 0 var selected = 0
lateinit var navBar: AnimatedBottomBar
var anime = true var anime = true
private var adult = false private var adult = false
@SuppressLint("SetTextI18n", "ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
var media: Media = intent.getSerialized("media") ?: mediaSingleton ?: emptyMedia() var media: Media = intent.getSerialized("media") ?: mediaSingleton ?: emptyMedia()
@@ -83,8 +85,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
if (id != -1) { if (id != -1) {
runBlocking { runBlocking {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
media = media = Anilist.query.getMedia(id, false) ?: emptyMedia()
Anilist.query.getMedia(id, false) ?: emptyMedia()
} }
} }
} }
@@ -93,6 +94,9 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
return return
} }
val contract = ActivityResultContracts.OpenDocumentTree()
launcher = LauncherWrapper(this, contract)
mediaSingleton = null mediaSingleton = null
ThemeManager(this).applyTheme(MediaSingleton.bitmap) ThemeManager(this).applyTheme(MediaSingleton.bitmap)
MediaSingleton.bitmap = null MediaSingleton.bitmap = null
@@ -100,26 +104,38 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
binding = ActivityMediaBinding.inflate(layoutInflater) binding = ActivityMediaBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
screenWidth = resources.displayMetrics.widthPixels.toFloat() screenWidth = resources.displayMetrics.widthPixels.toFloat()
navBar = binding.mediaBottomBar
val isVertical = resources.configuration.orientation // Ui init
//Ui init
initActivity(this) initActivity(this)
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight } binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
val oldMargin = binding.mediaViewPager.marginBottom val oldMargin = binding.mediaViewPager.marginBottom
AndroidBug5497Workaround.assistActivity(this) { AndroidBug5497Workaround.assistActivity(this) {
if (it) { if (it) {
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> { binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = 0 bottomMargin = 0
} }
binding.mediaTabContainer.visibility = View.GONE navBar.visibility = View.GONE
} else { } else {
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> { binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = oldMargin bottomMargin = oldMargin
} }
binding.mediaTabContainer.visibility = View.VISIBLE navBar.visibility = View.VISIBLE
} }
} }
val navBarRightMargin = if (resources.configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE
) navBarHeight else 0
val navBarBottomMargin = if (resources.configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE
) 0 else navBarHeight
navBar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
rightMargin = navBarRightMargin
bottomMargin = navBarBottomMargin
}
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 }
@@ -147,7 +163,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
val banner = val banner =
if (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 AnimatedBottomBar
viewPager.isUserInputEnabled = false viewPager.isUserInputEnabled = false
viewPager.setPageTransformer(ZoomOutPageTransformer()) viewPager.setPageTransformer(ZoomOutPageTransformer())
@@ -157,9 +172,10 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
binding.mediaCoverImage.loadImage(media.cover) binding.mediaCoverImage.loadImage(media.cover)
binding.mediaCoverImage.setOnLongClickListener { binding.mediaCoverImage.setOnLongClickListener {
val coverTitle = "${media.userPreferredName}[Cover]"
ImageViewDialog.newInstance( ImageViewDialog.newInstance(
this, this,
media.userPreferredName + "[Cover]", coverTitle,
media.cover media.cover
) )
} }
@@ -176,9 +192,10 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
} }
override fun onLongClick(event: MotionEvent) { override fun onLongClick(event: MotionEvent) {
val bannerTitle = "${media.userPreferredName}[Banner]"
ImageViewDialog.newInstance( ImageViewDialog.newInstance(
this@MediaDetailsActivity, this@MediaDetailsActivity,
media.userPreferredName + "[Banner]", bannerTitle,
media.banner ?: media.cover media.banner ?: media.cover
) )
banner.performClick() banner.performClick()
@@ -186,7 +203,8 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
}) })
banner.setOnTouchListener { _, motionEvent -> gestureDetector.onTouchEvent(motionEvent);true } banner.setOnTouchListener { _, motionEvent -> gestureDetector.onTouchEvent(motionEvent);true }
if (PrefManager.getVal(PrefName.Incognito)) { if (PrefManager.getVal(PrefName.Incognito)) {
binding.mediaTitle.text = " ${media.userPreferredName}" val mediaTitle = " ${media.userPreferredName}"
binding.mediaTitle.text = mediaTitle
binding.incognito.visibility = View.VISIBLE binding.incognito.visibility = View.VISIBLE
} else { } else {
binding.mediaTitle.text = media.userPreferredName binding.mediaTitle.text = media.userPreferredName
@@ -210,20 +228,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
R.drawable.ic_round_favorite_24 R.drawable.ic_round_favorite_24
) )
) )
val typedValue = TypedValue()
this.theme.resolveAttribute(
com.google.android.material.R.attr.colorSecondary,
typedValue,
true
)
val color = typedValue.data
val typedValue2 = TypedValue()
this.theme.resolveAttribute(
com.google.android.material.R.attr.colorSecondary,
typedValue2,
true
)
val color2 = typedValue.data
PopImageButton( PopImageButton(
scope, scope,
@@ -231,7 +235,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
R.drawable.ic_round_favorite_24, R.drawable.ic_round_favorite_24,
R.drawable.ic_round_favorite_border_24, R.drawable.ic_round_favorite_border_24,
R.color.bg_opp, R.color.bg_opp,
R.color.violet_400,//TODO: Change to colorSecondary R.color.violet_400,
media.isFav media.isFav
) { ) {
media.isFav = it media.isFav = it
@@ -246,13 +250,13 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
@SuppressLint("ResourceType") @SuppressLint("ResourceType")
fun total() { fun total() {
val text = SpannableStringBuilder().apply { val text = SpannableStringBuilder().apply {
val typedValue = TypedValue() val mediaTypedValue = TypedValue()
this@MediaDetailsActivity.theme.resolveAttribute( this@MediaDetailsActivity.theme.resolveAttribute(
com.google.android.material.R.attr.colorOnBackground, com.google.android.material.R.attr.colorOnBackground,
typedValue, mediaTypedValue,
true true
) )
val white = typedValue.data val white = mediaTypedValue.data
if (media.userStatus != null) { if (media.userStatus != null) {
append(if (media.anime != null) getString(R.string.watched_num) else getString(R.string.read_num)) append(if (media.anime != null) getString(R.string.watched_num) else getString(R.string.read_num))
val typedValue = TypedValue() val typedValue = TypedValue()
@@ -342,18 +346,16 @@ 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
if (media.anime != null) { if (media.anime != null) {
viewPager.adapter = viewPager.adapter =
ViewPagerAdapter(supportFragmentManager, lifecycle, SupportedMedia.ANIME, media, intent.getIntExtra("commentId", -1)) ViewPagerAdapter(
supportFragmentManager,
lifecycle,
SupportedMedia.ANIME,
media,
intent.getIntExtra("commentId", -1)
)
} else if (media.manga != null) { } else if (media.manga != null) {
viewPager.adapter = ViewPagerAdapter( viewPager.adapter = ViewPagerAdapter(
supportFragmentManager, supportFragmentManager,
@@ -365,31 +367,47 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
anime = false anime = false
} }
selected = media.selected!!.window selected = media.selected!!.window
binding.mediaTitle.translationX = -screenWidth binding.mediaTitle.translationX = -screenWidth
tabLayout.selectionListener = { selected, newId -> val infoTab = navBar.createTab(R.drawable.ic_round_info_24, R.string.info, R.id.info)
binding.commentInputLayout.visibility = if (selected == 2) View.VISIBLE else View.GONE val watchTab = if (anime) {
this.selected = selected navBar.createTab(R.drawable.ic_round_movie_filter_24, R.string.watch, R.id.watch)
selectFromID(newId) } else if (media.format == "NOVEL") {
viewPager.setCurrentItem(selected, false) navBar.createTab(R.drawable.ic_round_book_24, R.string.read, R.id.read)
val sel = model.loadSelected(media, isDownload) } else {
sel.window = selected navBar.createTab(R.drawable.ic_round_import_contacts_24, R.string.read, R.id.read)
model.saveSelected(media.id, sel)
} }
tabLayout.selectTab(selected) val commentTab =
selectFromID(tabLayout.selected) navBar.createTab(R.drawable.ic_round_comment_24, R.string.comments, R.id.comment)
viewPager.setCurrentItem(selected, false) navBar.addTab(infoTab)
navBar.addTab(watchTab)
navBar.addTab(commentTab)
if (model.continueMedia == null && media.cameFromContinue) { if (model.continueMedia == null && media.cameFromContinue) {
model.continueMedia = PrefManager.getVal(PrefName.ContinueMedia) model.continueMedia = PrefManager.getVal(PrefName.ContinueMedia)
selected = 1 selected = 1
} }
val frag = intent.getStringExtra("FRAGMENT_TO_LOAD") if (intent.getStringExtra("FRAGMENT_TO_LOAD") != null) selected = 2
if (frag != null) { if (viewPager.currentItem != selected) viewPager.post {
selected = 2 viewPager.setCurrentItem(selected, false)
} }
binding.commentInputLayout.isVisible = selected == 2
navBar.selectTabAt(selected)
navBar.setOnTabSelectListener(object : AnimatedBottomBar.OnTabSelectListener {
override fun onTabSelected(
lastIndex: Int,
lastTab: AnimatedBottomBar.Tab?,
newIndex: Int,
newTab: AnimatedBottomBar.Tab
) {
selected = newIndex
binding.commentInputLayout.isVisible = selected == 2
viewPager.setCurrentItem(selected, true)
val sel = model.loadSelected(media, isDownload)
sel.window = selected
model.saveSelected(media.id, sel)
}
})
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) } val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) }
live.observe(this) { live.observe(this) {
@@ -402,40 +420,21 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
} }
} }
private fun selectFromID(id: Int) { override fun onConfigurationChanged(newConfig: Configuration) {
when (id) { super.onConfigurationChanged(newConfig)
R.id.info -> { val rightMargin = if (resources.configuration.orientation ==
selected = 0 Configuration.ORIENTATION_LANDSCAPE
} ) navBarHeight else 0
val bottomMargin = if (resources.configuration.orientation ==
R.id.watch, R.id.read -> { Configuration.ORIENTATION_LANDSCAPE
selected = 1 ) 0 else navBarHeight
} val params: ViewGroup.MarginLayoutParams =
navBar.layoutParams as ViewGroup.MarginLayoutParams
R.id.comment -> { params.updateMargins(right = rightMargin, bottom = bottomMargin)
selected = 2
}
}
}
private fun idFromSelect(): Int {
if (anime) when (selected) {
0 -> return R.id.info
1 -> return R.id.watch
2 -> return R.id.comment
}
else when (selected) {
0 -> return R.id.info
1 -> return R.id.read
2 -> return R.id.comment
}
return R.id.info
} }
override fun onResume() { override fun onResume() {
if (this::tabLayout.isInitialized) { navBar.selectTabAt(selected)
tabLayout.selectTab(selected)
}
super.onResume() super.onResume()
} }
@@ -443,7 +442,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
ANIME, MANGA, NOVEL ANIME, MANGA, NOVEL
} }
//ViewPager // ViewPager
private class ViewPagerAdapter( private class ViewPagerAdapter(
fragmentManager: FragmentManager, fragmentManager: FragmentManager,
lifecycle: Lifecycle, lifecycle: Lifecycle,
@@ -462,6 +461,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
SupportedMedia.MANGA -> MangaReadFragment() SupportedMedia.MANGA -> MangaReadFragment()
SupportedMedia.NOVEL -> NovelReadFragment() SupportedMedia.NOVEL -> NovelReadFragment()
} }
2 -> { 2 -> {
val fragment = CommentsFragment() val fragment = CommentsFragment()
val bundle = Bundle() val bundle = Bundle()
@@ -489,13 +489,6 @@ 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 * (PrefManager.getVal(PrefName.AnimationSpeed) as Float)).toLong() val duration = (200 * (PrefManager.getVal(PrefName.AnimationSpeed) as Float)).toLong()
val typedValue = TypedValue()
this@MediaDetailsActivity.theme.resolveAttribute(
com.google.android.material.R.attr.colorSecondary,
typedValue,
true
)
val color = typedValue.data
if (percentage >= percent && !isCollapsed) { if (percentage >= percent && !isCollapsed) {
isCollapsed = true isCollapsed = true
ObjectAnimator.ofFloat(binding.mediaTitle, "translationX", 0f).setDuration(duration) ObjectAnimator.ofFloat(binding.mediaTitle, "translationX", 0f).setDuration(duration)

View File

@@ -9,7 +9,6 @@ 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.util.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
@@ -29,6 +28,7 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName 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 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
@@ -52,26 +52,23 @@ class MediaDetailsViewModel : ViewModel() {
it it
} }
if (isDownload) { if (isDownload) {
data.sourceIndex = if (media.anime != null) { data.sourceIndex = when {
AnimeSources.list.size - 1 media.anime != null -> {
} else if (media.format == "MANGA" || media.format == "ONE_SHOT") { AnimeSources.list.size - 1
MangaSources.list.size - 1 }
} else {
NovelSources.list.size - 1 media.format == "MANGA" || media.format == "ONE_SHOT" -> {
MangaSources.list.size - 1
}
else -> {
NovelSources.list.size - 1
}
} }
} }
return data return data
} }
fun loadSelectedStringLocation(sourceName: String): Int {
//find the location of the source in the list
var location = watchSources?.list?.indexOfFirst { it.name == sourceName } ?: 0
if (location == -1) {
location = 0
}
return location
}
var continueMedia: Boolean? = null var continueMedia: Boolean? = null
private var loading = false private var loading = false
@@ -152,10 +149,10 @@ class MediaDetailsViewModel : ViewModel() {
watchSources?.get(i)?.apply { watchSources?.get(i)?.apply {
if (!post && !allowsPreloading) return@apply if (!post && !allowsPreloading) return@apply
ep.sEpisode?.let { ep.sEpisode?.let {
loadByVideoServers(link, ep.extra, it) { loadByVideoServers(link, ep.extra, it) { extractor ->
if (it.videos.isNotEmpty()) { if (extractor.videos.isNotEmpty()) {
list.add(it) list.add(extractor)
ep.extractorCallback?.invoke(it) ep.extractorCallback?.invoke(extractor)
} }
} }
} }
@@ -291,7 +288,6 @@ class MediaDetailsViewModel : ViewModel() {
suspend fun loadMangaChapterImages( suspend fun loadMangaChapterImages(
chapter: MangaChapter, chapter: MangaChapter,
selected: Selected, selected: Selected,
series: String,
post: Boolean = true post: Boolean = true
): Boolean { ): Boolean {

View File

@@ -3,7 +3,6 @@ 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
@@ -16,16 +15,33 @@ import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
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.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.* import ani.dantotsu.R
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.copyToClipboard
import ani.dantotsu.currActivity
import ani.dantotsu.databinding.ActivityGenreBinding
import ani.dantotsu.databinding.FragmentMediaInfoBinding
import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.databinding.ItemQuelsBinding
import ani.dantotsu.databinding.ItemTitleChipgroupBinding
import ani.dantotsu.databinding.ItemTitleRecyclerBinding
import ani.dantotsu.databinding.ItemTitleSearchBinding
import ani.dantotsu.databinding.ItemTitleTextBinding
import ani.dantotsu.databinding.ItemTitleTrailerBinding
import ani.dantotsu.displayTimer
import ani.dantotsu.loadImage
import ani.dantotsu.navBarHeight
import ani.dantotsu.px
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
@@ -37,7 +53,6 @@ import java.io.Serializable
import java.net.URLEncoder import java.net.URLEncoder
@SuppressLint("SetTextI18n")
class MediaInfoFragment : Fragment() { class MediaInfoFragment : Fragment() {
private var _binding: FragmentMediaInfoBinding? = null private var _binding: FragmentMediaInfoBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
@@ -46,6 +61,8 @@ class MediaInfoFragment : Fragment() {
private var type = "ANIME" private var type = "ANIME"
private val genreModel: GenresViewModel by activityViewModels() private val genreModel: GenresViewModel by activityViewModels()
private val tripleTab = "\t\t\t"
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@@ -63,8 +80,8 @@ class MediaInfoFragment : Fragment() {
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) val offline: Boolean = PrefManager.getVal(PrefName.OfflineMode)
binding.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE binding.mediaInfoProgressBar.isGone = loaded
binding.mediaInfoContainer.visibility = if (loaded) View.VISIBLE else View.GONE binding.mediaInfoContainer.isVisible = loaded
binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight } binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight }
model.scrolledToTop.observe(viewLifecycleOwner) { model.scrolledToTop.observe(viewLifecycleOwner) {
@@ -75,17 +92,18 @@ class MediaInfoFragment : Fragment() {
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) val infoName = tripleTab + (media.name ?: media.nameRomaji)
binding.mediaInfoName.text = infoName
binding.mediaInfoName.setOnLongClickListener { binding.mediaInfoName.setOnLongClickListener {
copyToClipboard(media.name ?: media.nameRomaji) copyToClipboard(media.name ?: media.nameRomaji)
true true
} }
if (media.name != null) binding.mediaInfoNameRomajiContainer.visibility = if (media.name != null) binding.mediaInfoNameRomajiContainer.visibility =
View.VISIBLE View.VISIBLE
binding.mediaInfoNameRomaji.text = "\t\t\t" + media.nameRomaji val infoNameRomanji = tripleTab + media.nameRomaji
binding.mediaInfoNameRomaji.text = infoNameRomanji
binding.mediaInfoNameRomaji.setOnLongClickListener { binding.mediaInfoNameRomaji.setOnLongClickListener {
copyToClipboard(media.nameRomaji) copyToClipboard(media.nameRomaji)
true true
@@ -97,6 +115,8 @@ class MediaInfoFragment : Fragment() {
binding.mediaInfoSource.text = media.source binding.mediaInfoSource.text = media.source
binding.mediaInfoStart.text = media.startDate?.toString() ?: "??" binding.mediaInfoStart.text = media.startDate?.toString() ?: "??"
binding.mediaInfoEnd.text = media.endDate?.toString() ?: "??" binding.mediaInfoEnd.text = media.endDate?.toString() ?: "??"
binding.mediaInfoPopularity.text = media.popularity.toString()
binding.mediaInfoFavorites.text = media.favourites.toString()
if (media.anime != null) { if (media.anime != null) {
val episodeDuration = media.anime.episodeDuration val episodeDuration = media.anime.episodeDuration
@@ -125,8 +145,10 @@ class MediaInfoFragment : Fragment() {
} }
binding.mediaInfoDurationContainer.visibility = View.VISIBLE binding.mediaInfoDurationContainer.visibility = View.VISIBLE
binding.mediaInfoSeasonContainer.visibility = View.VISIBLE binding.mediaInfoSeasonContainer.visibility = View.VISIBLE
binding.mediaInfoSeason.text = val seasonInfo =
(media.anime.season ?: "??") + " " + (media.anime.seasonYear ?: "??") "${(media.anime.season ?: "??")} ${(media.anime.seasonYear ?: "??")}"
binding.mediaInfoSeason.text = seasonInfo
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
@@ -160,9 +182,12 @@ class MediaInfoFragment : Fragment() {
} }
} }
binding.mediaInfoTotalTitle.setText(R.string.total_eps) binding.mediaInfoTotalTitle.setText(R.string.total_eps)
binding.mediaInfoTotal.text = val infoTotal = if (media.anime.nextAiringEpisode != null)
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " | " + (media.anime.totalEpisodes "${media.anime.nextAiringEpisode} | ${media.anime.totalEpisodes ?: "~"}"
?: "~").toString()) else (media.anime.totalEpisodes ?: "~").toString() else
(media.anime.totalEpisodes ?: "~").toString()
binding.mediaInfoTotal.text = infoTotal
} else if (media.manga != null) { } else if (media.manga != null) {
type = "MANGA" type = "MANGA"
binding.mediaInfoTotalTitle.setText(R.string.total_chaps) binding.mediaInfoTotalTitle.setText(R.string.total_chaps)
@@ -189,8 +214,10 @@ class MediaInfoFragment : Fragment() {
(media.description ?: "null").replace("\\n", "<br>").replace("\\\"", "\""), (media.description ?: "null").replace("\\n", "<br>").replace("\\\"", "\""),
HtmlCompat.FROM_HTML_MODE_LEGACY HtmlCompat.FROM_HTML_MODE_LEGACY
) )
binding.mediaInfoDescription.text = val infoDesc =
"\t\t\t" + if (desc.toString() != "null") desc else getString(R.string.no_description_available) tripleTab + if (desc.toString() != "null") desc else getString(R.string.no_description_available)
binding.mediaInfoDescription.text = infoDesc
binding.mediaInfoDescription.setOnClickListener { binding.mediaInfoDescription.setOnClickListener {
if (binding.mediaInfoDescription.maxLines == 5) { if (binding.mediaInfoDescription.maxLines == 5) {
ObjectAnimator.ofInt(binding.mediaInfoDescription, "maxLines", 100) ObjectAnimator.ofInt(binding.mediaInfoDescription, "maxLines", 100)
@@ -200,8 +227,7 @@ class MediaInfoFragment : Fragment() {
.setDuration(400).start() .setDuration(400).start()
} }
} }
displayTimer(media, binding.mediaInfoContainer)
countDown(media, binding.mediaInfoContainer)
val parent = _binding?.mediaInfoContainer!! val parent = _binding?.mediaInfoContainer!!
val screenWidth = resources.displayMetrics.run { widthPixels / density } val screenWidth = resources.displayMetrics.run { widthPixels / density }
@@ -413,113 +439,155 @@ class MediaInfoFragment : Fragment() {
if (!media.relations.isNullOrEmpty() && !offline) { if (!media.relations.isNullOrEmpty() && !offline) {
if (media.sequel != null || media.prequel != null) { if (media.sequel != null || media.prequel != null) {
val bind = ItemQuelsBinding.inflate( ItemQuelsBinding.inflate(
LayoutInflater.from(context), LayoutInflater.from(context),
parent, parent,
false false
) ).apply {
if (media.sequel != null) { if (media.sequel != null) {
bind.mediaInfoSequel.visibility = View.VISIBLE mediaInfoSequel.visibility = View.VISIBLE
bind.mediaInfoSequelImage.loadImage( mediaInfoSequelImage.loadImage(
media.sequel!!.banner ?: media.sequel!!.cover media.sequel!!.banner ?: media.sequel!!.cover
)
bind.mediaInfoSequel.setSafeOnClickListener {
ContextCompat.startActivity(
requireContext(),
Intent(
requireContext(),
MediaDetailsActivity::class.java
).putExtra(
"media",
media.sequel as Serializable
), null
) )
} mediaInfoSequel.setSafeOnClickListener {
} ContextCompat.startActivity(
if (media.prequel != null) {
bind.mediaInfoPrequel.visibility = View.VISIBLE
bind.mediaInfoPrequelImage.loadImage(
media.prequel!!.banner ?: media.prequel!!.cover
)
bind.mediaInfoPrequel.setSafeOnClickListener {
ContextCompat.startActivity(
requireContext(),
Intent(
requireContext(), requireContext(),
MediaDetailsActivity::class.java Intent(
).putExtra( requireContext(),
"media", MediaDetailsActivity::class.java
media.prequel as Serializable ).putExtra(
), null "media",
) media.sequel as Serializable
), null
)
}
} }
if (media.prequel != null) {
mediaInfoPrequel.visibility = View.VISIBLE
mediaInfoPrequelImage.loadImage(
media.prequel!!.banner ?: media.prequel!!.cover
)
mediaInfoPrequel.setSafeOnClickListener {
ContextCompat.startActivity(
requireContext(),
Intent(
requireContext(),
MediaDetailsActivity::class.java
).putExtra(
"media",
media.prequel as Serializable
), null
)
}
}
parent.addView(root)
}
ItemTitleSearchBinding.inflate(
LayoutInflater.from(context),
parent,
false
).apply {
titleSearchImage.loadImage(media.banner ?: media.cover)
titleSearchText.text =
getString(R.string.search_title, media.mainName())
titleSearchCard.setSafeOnClickListener {
val query = Intent(requireContext(), SearchActivity::class.java)
.putExtra("type", "ANIME")
.putExtra("query", media.mainName())
.putExtra("search", true)
ContextCompat.startActivity(requireContext(), query, null)
}
parent.addView(root)
} }
parent.addView(bind.root)
} }
val bindi = ItemTitleRecyclerBinding.inflate( ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context), LayoutInflater.from(context),
parent, parent,
false false
) ).apply {
bindi.itemRecycler.adapter = itemRecycler.adapter =
MediaAdaptor(0, media.relations!!, requireActivity()) MediaAdaptor(0, media.relations!!, requireActivity())
bindi.itemRecycler.layoutManager = LinearLayoutManager( itemRecycler.layoutManager = LinearLayoutManager(
requireContext(), requireContext(),
LinearLayoutManager.HORIZONTAL, LinearLayoutManager.HORIZONTAL,
false false
) )
parent.addView(bindi.root) parent.addView(root)
}
} }
if (!media.characters.isNullOrEmpty() && !offline) { if (!media.characters.isNullOrEmpty() && !offline) {
val bind = ItemTitleRecyclerBinding.inflate( ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context), LayoutInflater.from(context),
parent, parent,
false false
) ).apply {
bind.itemTitle.setText(R.string.characters) itemTitle.setText(R.string.characters)
bind.itemRecycler.adapter = itemRecycler.adapter =
CharacterAdapter(media.characters!!) CharacterAdapter(media.characters!!)
bind.itemRecycler.layoutManager = LinearLayoutManager( itemRecycler.layoutManager = LinearLayoutManager(
requireContext(), requireContext(),
LinearLayoutManager.HORIZONTAL, LinearLayoutManager.HORIZONTAL,
false false
) )
parent.addView(bind.root) parent.addView(root)
}
} }
if (!media.staff.isNullOrEmpty() && !offline) { if (!media.staff.isNullOrEmpty() && !offline) {
val bind = ItemTitleRecyclerBinding.inflate( ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context), LayoutInflater.from(context),
parent, parent,
false false
) ).apply {
bind.itemTitle.setText(R.string.staff) itemTitle.setText(R.string.staff)
bind.itemRecycler.adapter = itemRecycler.adapter =
AuthorAdapter(media.staff!!) AuthorAdapter(media.staff!!)
bind.itemRecycler.layoutManager = LinearLayoutManager( itemRecycler.layoutManager = LinearLayoutManager(
requireContext(), requireContext(),
LinearLayoutManager.HORIZONTAL, LinearLayoutManager.HORIZONTAL,
false false
) )
parent.addView(bind.root) parent.addView(root)
}
} }
if (!media.recommendations.isNullOrEmpty() && !offline) { if (!media.recommendations.isNullOrEmpty() && !offline) {
val bind = ItemTitleRecyclerBinding.inflate( ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context), LayoutInflater.from(context),
parent, parent,
false false
) ).apply {
bind.itemTitle.setText(R.string.recommended) itemTitle.setText(R.string.recommended)
bind.itemRecycler.adapter = itemRecycler.adapter =
MediaAdaptor(0, media.recommendations!!, requireActivity()) MediaAdaptor(0, media.recommendations!!, requireActivity())
bind.itemRecycler.layoutManager = LinearLayoutManager( itemRecycler.layoutManager = LinearLayoutManager(
requireContext(), requireContext(),
LinearLayoutManager.HORIZONTAL, LinearLayoutManager.HORIZONTAL,
false
)
parent.addView(root)
}
}
if (!media.users.isNullOrEmpty() && !offline) {
ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context),
parent,
false false
) ).apply {
parent.addView(bind.root) itemTitle.setText(R.string.social)
itemRecycler.adapter =
MediaSocialAdapter(media.users!!)
itemRecycler.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
parent.addView(root)
}
} }
} }
} }
@@ -544,11 +612,12 @@ class MediaInfoFragment : Fragment() {
} }
} }
} }
super.onViewCreated(view, null) super.onViewCreated(view, null)
} }
override fun onResume() { override fun onResume() {
binding.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE binding.mediaInfoProgressBar.isGone = loaded
super.onResume() super.onResume()
} }

View File

@@ -1,6 +1,5 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.text.InputFilter.LengthFilter import android.text.InputFilter.LengthFilter
import android.view.Gravity import android.view.Gravity
@@ -11,11 +10,18 @@ import android.widget.ArrayAdapter
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import ani.dantotsu.* import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.DatePickerFragment
import ani.dantotsu.InputFilterMinMax
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.connections.anilist.Anilist 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 ani.dantotsu.navBarHeight
import ani.dantotsu.snackString
import ani.dantotsu.tryWith
import com.google.android.material.materialswitch.MaterialSwitch import com.google.android.material.materialswitch.MaterialSwitch
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -36,7 +42,6 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
return binding.root return binding.root
} }
@SuppressLint("SetTextI18n")
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 }
var media: Media? var media: Media?
@@ -168,9 +173,10 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
val init = val init =
if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString() if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString()
.toInt() else 0 .toInt() else 0
if (init < (total if (init < (total ?: 5000)) {
?: 5000) val progressText = "${init + 1}"
) binding.mediaListProgress.setText((init + 1).toString()) binding.mediaListProgress.setText(progressText)
}
if (init + 1 == (total ?: 5000)) { if (init + 1 == (total ?: 5000)) {
binding.mediaListStatus.setText(statusStrings[2], false) binding.mediaListStatus.setText(statusStrings[2], false)
onComplete() onComplete()

View File

@@ -1,6 +1,5 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.text.InputFilter.LengthFilter import android.text.InputFilter.LengthFilter
import android.view.Gravity import android.view.Gravity
@@ -10,11 +9,16 @@ import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import ani.dantotsu.* import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.InputFilterMinMax
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.mal.MAL import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding
import ani.dantotsu.navBarHeight
import ani.dantotsu.others.getSerialized import ani.dantotsu.others.getSerialized
import ani.dantotsu.snackString
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -54,7 +58,6 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
} }
@SuppressLint("SetTextI18n")
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
@@ -68,7 +71,7 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
MAL.query.deleteList(media.anime != null, media.idMAL) MAL.query.deleteList(media.anime != null, media.idMAL)
} catch (e: Exception) { } catch (e: Exception) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
snackString("Failed to delete because of... ${e.message}") snackString(getString(R.string.delete_fail_reason, e.message))
} }
return@withContext return@withContext
} }
@@ -154,7 +157,10 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
val init = val init =
if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString() if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString()
.toInt() else 0 .toInt() else 0
if (init < (total ?: 5000)) binding.mediaListProgress.setText((init + 1).toString()) if (init < (total ?: 5000)) {
val progressText = "${init + 1}"
binding.mediaListProgress.setText(progressText)
}
if (init + 1 == (total ?: 5000)) { if (init + 1 == (total ?: 5000)) {
binding.mediaListStatus.setText(statusStrings[2], false) binding.mediaListStatus.setText(statusStrings[2], false)
} }

View File

@@ -0,0 +1,146 @@
package ani.dantotsu.media
import java.util.Locale
import java.util.regex.Matcher
import java.util.regex.Pattern
object MediaNameAdapter {
private const val REGEX_ITEM = "[\\s:.\\-]*(\\d+\\.?\\d*)[\\s:.\\-]*"
private const val REGEX_PART_NUMBER = "(?<!part\\s)\\b(\\d+)\\b"
private const val REGEX_EPISODE =
"(episode|episodio|ep|e)${REGEX_ITEM}\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*"
private const val REGEX_SEASON = "(season|s)[\\s:.\\-]*(\\d+)[\\s:.\\-]*"
private const val REGEX_SUBDUB = "^(soft)?[\\s-]*(sub|dub|mixed)(bed|s)?\\s*$"
private const val REGEX_CHAPTER = "(chapter|chap|ch|c)${REGEX_ITEM}"
fun setSubDub(text: String, typeToSetTo: SubDubType): String? {
val subdubPattern: Pattern = Pattern.compile(REGEX_SUBDUB, 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(REGEX_SUBDUB, 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? {
val seasonPattern: Pattern = Pattern.compile(REGEX_SEASON, Pattern.CASE_INSENSITIVE)
val seasonMatcher: Matcher = seasonPattern.matcher(text)
return if (seasonMatcher.find()) {
seasonMatcher.group(2)?.toInt()
} else {
text.toIntOrNull()
}
}
fun findEpisodeNumber(text: String): Float? {
val episodePattern: Pattern = Pattern.compile(REGEX_EPISODE, 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(REGEX_PART_NUMBER, Pattern.CASE_INSENSITIVE)
val failedEpisodeNumberMatcher: Matcher =
failedEpisodeNumberPattern.matcher(text)
if (failedEpisodeNumberMatcher.find()) {
failedEpisodeNumberMatcher.group(1)?.toFloat()
} else {
null
}
}
} else {
text.toFloatOrNull()
}
}
fun removeEpisodeNumber(text: String): String {
val regexPattern = Regex(REGEX_EPISODE, 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(REGEX_EPISODE, RegexOption.IGNORE_CASE)
val removedNumber = text.replace(regexPattern, "")
return if (removedNumber.equals(text, true)) { // if nothing was removed
val failedEpisodeNumberPattern =
Regex(REGEX_PART_NUMBER, RegexOption.IGNORE_CASE)
failedEpisodeNumberPattern.replace(removedNumber) { mr ->
mr.value.replaceFirst(mr.groupValues[1], "")
}
} else {
removedNumber
}
}
fun findChapterNumber(text: String): Float? {
val pattern: Pattern = Pattern.compile(REGEX_CHAPTER, Pattern.CASE_INSENSITIVE)
val matcher: Matcher = pattern.matcher(text)
return if (matcher.find()) {
matcher.group(2)?.toFloat()
} else {
val failedChapterNumberPattern: Pattern =
Pattern.compile(REGEX_PART_NUMBER, Pattern.CASE_INSENSITIVE)
val failedChapterNumberMatcher: Matcher =
failedChapterNumberPattern.matcher(text)
if (failedChapterNumberMatcher.find()) {
failedChapterNumberMatcher.group(1)?.toFloat()
} else {
text.toFloatOrNull()
}
}
}
}

View File

@@ -0,0 +1,70 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.databinding.ItemFollowerGridBinding
import ani.dantotsu.loadImage
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.User
import ani.dantotsu.setAnimation
class MediaSocialAdapter(private val user: ArrayList<User>) :
RecyclerView.Adapter<MediaSocialAdapter.DeveloperViewHolder>() {
inner class DeveloperViewHolder(val binding: ItemFollowerGridBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeveloperViewHolder {
return DeveloperViewHolder(
ItemFollowerGridBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: DeveloperViewHolder, position: Int) {
holder.binding.apply {
val user = user[position]
val score = user.score?.div(10.0) ?: 0.0
setAnimation(root.context, root)
profileUserName.text = user.name
profileInfo.apply {
text = when (user.status) {
"CURRENT" -> "WATCHING"
else -> user.status ?: ""
}
visibility = View.VISIBLE
}
profileCompactUserProgress.text = user.progress.toString()
profileCompactScore.text = score.toString()
profileCompactTotal.text = " | ${user.totalEpisodes ?: "~"}"
profileUserAvatar.loadImage(user.pfp)
val scoreDrawable = if (score == 0.0) R.drawable.score else R.drawable.user_score
profileCompactScoreBG.apply {
visibility = View.VISIBLE
background = ContextCompat.getDrawable(root.context, scoreDrawable)
}
profileCompactProgressContainer.visibility = View.VISIBLE
profileUserAvatar.setOnClickListener {
val intent = Intent(root.context, ProfileActivity::class.java).apply {
putExtra("userId", user.id)
}
ContextCompat.startActivity(root.context, intent, null)
}
}
}
override fun getItemCount(): Int = user.size
}

View File

@@ -0,0 +1,56 @@
package ani.dantotsu.media
interface Type {
fun asText(): String
}
enum class MediaType : Type {
ANIME,
MANGA,
NOVEL;
override fun asText(): String {
return when (this) {
ANIME -> "Anime"
MANGA -> "Manga"
NOVEL -> "Novel"
}
}
companion object {
fun fromText(string: String): MediaType? {
return when (string) {
"Anime" -> ANIME
"Manga" -> MANGA
"Novel" -> NOVEL
else -> {
null
}
}
}
}
}
enum class AddonType : Type {
TORRENT,
DOWNLOAD;
override fun asText(): String {
return when (this) {
TORRENT -> "Torrent"
DOWNLOAD -> "Download"
}
}
companion object {
fun fromText(string: String): AddonType? {
return when (string) {
"Torrent" -> TORRENT
"Download" -> DOWNLOAD
else -> {
null
}
}
}
}
}

View File

@@ -30,7 +30,7 @@ class OtherDetailsViewModel : ViewModel() {
fun getCalendar(): LiveData<Map<String, MutableList<Media>>> = calendar fun getCalendar(): LiveData<Map<String, MutableList<Media>>> = calendar
suspend fun loadCalendar() { suspend fun loadCalendar() {
val curr = System.currentTimeMillis() / 1000 val curr = System.currentTimeMillis() / 1000
val res = Anilist.query.recentlyUpdated(false, curr - 86400, curr + (86400 * 6)) val res = Anilist.query.recentlyUpdated(curr - 86400, curr + (86400 * 6))
val df = DateFormat.getDateInstance(DateFormat.FULL) val df = DateFormat.getDateInstance(DateFormat.FULL)
val map = mutableMapOf<String, MutableList<Media>>() val map = mutableMapOf<String, MutableList<Media>>()
val idMap = mutableMapOf<String, MutableList<Int>>() val idMap = mutableMapOf<String, MutableList<Int>>()

View File

@@ -27,7 +27,7 @@ class ProgressAdapter(private val horizontal: Boolean = true, searched: Boolean)
return ProgressViewHolder(binding) return ProgressViewHolder(binding)
} }
@SuppressLint("SetTextI18n", "ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: ProgressViewHolder, position: Int) { override fun onBindViewHolder(holder: ProgressViewHolder, position: Int) {
val progressBar = holder.binding.root val progressBar = holder.binding.root
bar = progressBar bar = progressBar

View File

@@ -4,24 +4,30 @@ import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.View import android.view.View
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.view.isVisible
import androidx.core.view.updatePaddingRelative import androidx.core.view.updatePaddingRelative
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
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.recyclerview.widget.RecyclerView
import ani.dantotsu.*
import ani.dantotsu.connections.anilist.Anilist 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.initActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.px
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.statusBarHeight
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
import java.util.* import java.util.Timer
import java.util.TimerTask
class SearchActivity : AppCompatActivity() { class SearchActivity : AppCompatActivity() {
private lateinit var binding: ActivitySearchBinding private lateinit var binding: ActivitySearchBinding
@@ -64,11 +70,18 @@ class SearchActivity : AppCompatActivity() {
intent.getStringExtra("type") ?: "ANIME", intent.getStringExtra("type") ?: "ANIME",
isAdult = if (Anilist.adult) intent.getBooleanExtra("hentai", false) else false, isAdult = if (Anilist.adult) intent.getBooleanExtra("hentai", false) else false,
onList = listOnly, onList = listOnly,
search = intent.getStringExtra("query"),
genres = intent.getStringExtra("genre")?.let { mutableListOf(it) }, genres = intent.getStringExtra("genre")?.let { mutableListOf(it) },
tags = intent.getStringExtra("tag")?.let { mutableListOf(it) }, tags = intent.getStringExtra("tag")?.let { mutableListOf(it) },
sort = intent.getStringExtra("sortBy"), sort = intent.getStringExtra("sortBy"),
status = intent.getStringExtra("status"),
source = intent.getStringExtra("source"),
countryOfOrigin = intent.getStringExtra("country"),
season = intent.getStringExtra("season"), season = intent.getStringExtra("season"),
seasonYear = intent.getStringExtra("seasonYear")?.toIntOrNull(), seasonYear = if (intent.getStringExtra("type") == "ANIME") intent.getStringExtra("seasonYear")
?.toIntOrNull() else null,
startYear = if (intent.getStringExtra("type") == "MANGA") intent.getStringExtra("seasonYear")
?.toIntOrNull() else null,
results = mutableListOf(), results = mutableListOf(),
hasNextPage = false hasNextPage = false
) )
@@ -127,8 +140,12 @@ class SearchActivity : AppCompatActivity() {
excludedTags = it.excludedTags excludedTags = it.excludedTags
tags = it.tags tags = it.tags
season = it.season season = it.season
startYear = it.startYear
seasonYear = it.seasonYear seasonYear = it.seasonYear
status = it.status
source = it.source
format = it.format format = it.format
countryOfOrigin = it.countryOfOrigin
page = it.page page = it.page
hasNextPage = it.hasNextPage hasNextPage = it.hasNextPage
} }
@@ -137,7 +154,7 @@ class SearchActivity : AppCompatActivity() {
model.searchResults.results.addAll(it.results) model.searchResults.results.addAll(it.results)
mediaAdaptor.notifyItemRangeInserted(prev, it.results.size) mediaAdaptor.notifyItemRangeInserted(prev, it.results.size)
progressAdapter.bar?.visibility = if (it.hasNextPage) View.VISIBLE else View.GONE progressAdapter.bar?.isVisible = it.hasNextPage
} }
} }
@@ -151,7 +168,10 @@ class SearchActivity : AppCompatActivity() {
} else } else
headerAdaptor.requestFocus?.run() headerAdaptor.requestFocus?.run()
if (intent.getBooleanExtra("search", false)) search() if (intent.getBooleanExtra("search", false)) {
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED)
search()
}
} }
} }
} }

View File

@@ -13,9 +13,9 @@ import android.view.animation.AlphaAnimation
import android.view.animation.Animation 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 android.widget.PopupMenu
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources 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
@@ -28,7 +28,9 @@ import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.imagesearch.ImageSearchActivity import ani.dantotsu.others.imagesearch.ImageSearchActivity
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import com.google.android.material.checkbox.MaterialCheckBox.* import com.google.android.material.checkbox.MaterialCheckBox.STATE_CHECKED
import com.google.android.material.checkbox.MaterialCheckBox.STATE_INDETERMINATE
import com.google.android.material.checkbox.MaterialCheckBox.STATE_UNCHECKED
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -44,6 +46,20 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
private lateinit var searchHistoryAdapter: SearchHistoryAdapter private lateinit var searchHistoryAdapter: SearchHistoryAdapter
private lateinit var binding: ItemSearchHeaderBinding private lateinit var binding: ItemSearchHeaderBinding
private fun updateFilterTextViewDrawable() {
val filterDrawable = when (activity.result.sort) {
Anilist.sortBy[0] -> R.drawable.ic_round_area_chart_24
Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24
Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24
Anilist.sortBy[3] -> R.drawable.ic_round_new_releases_24
Anilist.sortBy[4] -> R.drawable.ic_round_filter_list_24
Anilist.sortBy[5] -> R.drawable.ic_round_filter_list_24_reverse
Anilist.sortBy[6] -> R.drawable.ic_round_assist_walker_24
else -> R.drawable.ic_round_filter_alt_24
}
binding.filterTextView.setCompoundDrawablesWithIntrinsicBounds(filterDrawable, 0, 0, 0)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder {
val binding = val binding =
ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false) ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
@@ -92,7 +108,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
binding.searchAdultCheck.isChecked = adult binding.searchAdultCheck.isChecked = adult
binding.searchList.isChecked = listOnly == true binding.searchList.isChecked = listOnly == true
binding.searchChipRecycler.adapter = SearchChipAdapter(activity).also { binding.searchChipRecycler.adapter = SearchChipAdapter(activity, this).also {
activity.updateChips = { it.update() } activity.updateChips = { it.update() }
} }
@@ -102,6 +118,65 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
binding.searchFilter.setOnClickListener { binding.searchFilter.setOnClickListener {
SearchFilterBottomDialog.newInstance().show(activity.supportFragmentManager, "dialog") SearchFilterBottomDialog.newInstance().show(activity.supportFragmentManager, "dialog")
} }
binding.searchFilter.setOnLongClickListener {
val popupMenu = PopupMenu(activity, binding.searchFilter)
popupMenu.menuInflater.inflate(R.menu.sortby_filter_menu, popupMenu.menu)
popupMenu.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.sort_by_score -> {
activity.result.sort = Anilist.sortBy[0]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_popular -> {
activity.result.sort = Anilist.sortBy[1]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_trending -> {
activity.result.sort = Anilist.sortBy[2]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_recent -> {
activity.result.sort = Anilist.sortBy[3]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_a_z -> {
activity.result.sort = Anilist.sortBy[4]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_z_a -> {
activity.result.sort = Anilist.sortBy[5]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_pure_pain -> {
activity.result.sort = Anilist.sortBy[6]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
}
true
}
popupMenu.show()
true
}
binding.searchByImage.setOnClickListener { binding.searchByImage.setOnClickListener {
activity.startActivity(Intent(activity, ImageSearchActivity::class.java)) activity.startActivity(Intent(activity, ImageSearchActivity::class.java))
} }
@@ -230,14 +305,12 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
private fun fadeInAnimation(): Animation { private fun fadeInAnimation(): Animation {
return AlphaAnimation(0f, 1f).apply { return AlphaAnimation(0f, 1f).apply {
duration = 150 duration = 150
fillAfter = true
} }
} }
private fun fadeOutAnimation(): Animation { private fun fadeOutAnimation(): Animation {
return AlphaAnimation(1f, 0f).apply { return AlphaAnimation(1f, 0f).apply {
duration = 150 duration = 150
fillAfter = true
} }
} }
@@ -256,7 +329,10 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
} }
class SearchChipAdapter(val activity: SearchActivity) : class SearchChipAdapter(
val activity: SearchActivity,
private val searchAdapter: SearchAdapter
) :
RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() { RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() {
private var chips = activity.result.toChipList() private var chips = activity.result.toChipList()
@@ -273,11 +349,12 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
override fun onBindViewHolder(holder: SearchChipViewHolder, position: Int) { override fun onBindViewHolder(holder: SearchChipViewHolder, position: Int) {
val chip = chips[position] val chip = chips[position]
holder.binding.root.apply { holder.binding.root.apply {
text = chip.text text = chip.text.replace("_", " ")
setOnClickListener { setOnClickListener {
activity.result.removeChip(chip) activity.result.removeChip(chip)
update() update()
activity.search() activity.search()
searchAdapter.updateFilterTextViewDrawable()
} }
} }
} }
@@ -286,6 +363,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
fun update() { fun update() {
chips = activity.result.toChipList() chips = activity.result.toChipList()
notifyDataSetChanged() notifyDataSetChanged()
searchAdapter.updateFilterTextViewDrawable()
} }
override fun getItemCount(): Int = chips.size override fun getItemCount(): Int = chips.size

View File

@@ -1,11 +1,15 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.animation.ObjectAnimator
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.View.GONE import android.view.View.GONE
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.AnimationUtils
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.PopupMenu
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@@ -17,6 +21,9 @@ 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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.Calendar import java.util.Calendar
class SearchFilterBottomDialog : BottomSheetDialogFragment() { class SearchFilterBottomDialog : BottomSheetDialogFragment() {
@@ -38,6 +45,54 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
private var exGenres = mutableListOf<String>() private var exGenres = mutableListOf<String>()
private var selectedTags = mutableListOf<String>() private var selectedTags = mutableListOf<String>()
private var exTags = mutableListOf<String>() private var exTags = mutableListOf<String>()
private fun updateChips() {
binding.searchFilterGenres.adapter?.notifyDataSetChanged()
binding.searchFilterTags.adapter?.notifyDataSetChanged()
}
private fun startBounceZoomAnimation(view: View? = null) {
val targetView = view ?: binding.sortByFilter
val bounceZoomAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.bounce_zoom)
targetView.startAnimation(bounceZoomAnimation)
}
private fun setSortByFilterImage() {
val filterDrawable = when (activity.result.sort) {
Anilist.sortBy[0] -> R.drawable.ic_round_area_chart_24
Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24
Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24
Anilist.sortBy[3] -> R.drawable.ic_round_new_releases_24
Anilist.sortBy[4] -> R.drawable.ic_round_filter_list_24
Anilist.sortBy[5] -> R.drawable.ic_round_filter_list_24_reverse
Anilist.sortBy[6] -> R.drawable.ic_round_assist_walker_24
else -> R.drawable.ic_round_filter_alt_24
}
binding.sortByFilter.setImageResource(filterDrawable)
}
private fun resetSearchFilter() {
activity.result.sort = null
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_alt_24)
startBounceZoomAnimation(binding.sortByFilter)
activity.result.countryOfOrigin = null
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_search_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
selectedGenres.clear()
exGenres.clear()
selectedTags.clear()
exTags.clear()
binding.searchStatus.setText("")
binding.searchSource.setText("")
binding.searchFormat.setText("")
binding.searchSeason.setText("")
binding.searchYear.setText("")
binding.searchStatus.clearFocus()
binding.searchFormat.clearFocus()
binding.searchSeason.clearFocus()
binding.searchYear.clearFocus()
updateChips()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@@ -47,14 +102,157 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
exGenres = activity.result.excludedGenres ?: mutableListOf() exGenres = activity.result.excludedGenres ?: mutableListOf()
selectedTags = activity.result.tags ?: mutableListOf() selectedTags = activity.result.tags ?: mutableListOf()
exTags = activity.result.excludedTags ?: mutableListOf() exTags = activity.result.excludedTags ?: mutableListOf()
setSortByFilterImage()
binding.resetSearchFilter.setOnClickListener {
val rotateAnimation =
ObjectAnimator.ofFloat(binding.resetSearchFilter, "rotation", 180f, 540f)
rotateAnimation.duration = 500
rotateAnimation.interpolator = AccelerateDecelerateInterpolator()
rotateAnimation.start()
resetSearchFilter()
}
binding.resetSearchFilter.setOnLongClickListener {
val rotateAnimation =
ObjectAnimator.ofFloat(binding.resetSearchFilter, "rotation", 180f, 540f)
rotateAnimation.duration = 500
rotateAnimation.interpolator = AccelerateDecelerateInterpolator()
rotateAnimation.start()
val bounceAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.bounce_zoom)
binding.resetSearchFilter.startAnimation(bounceAnimation)
binding.resetSearchFilter.postDelayed({
resetSearchFilter()
CoroutineScope(Dispatchers.Main).launch {
activity.result.apply {
status =
binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null }
source =
binding.searchSource.text.toString().replace(" ", "_").ifBlank { null }
format = binding.searchFormat.text.toString().ifBlank { null }
season = binding.searchSeason.text.toString().ifBlank { null }
startYear = binding.searchYear.text.toString().toIntOrNull()
seasonYear = binding.searchYear.text.toString().toIntOrNull()
sort = activity.result.sort
genres = selectedGenres
tags = selectedTags
excludedGenres = exGenres
excludedTags = exTags
}
activity.updateChips.invoke()
activity.search()
dismiss()
}
}, 500)
true
}
binding.sortByFilter.setOnClickListener {
val popupMenu = PopupMenu(requireContext(), it)
popupMenu.menuInflater.inflate(R.menu.sortby_filter_menu, popupMenu.menu)
popupMenu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.sort_by_score -> {
activity.result.sort = Anilist.sortBy[0]
binding.sortByFilter.setImageResource(R.drawable.ic_round_area_chart_24)
startBounceZoomAnimation()
}
R.id.sort_by_popular -> {
activity.result.sort = Anilist.sortBy[1]
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_peak_24)
startBounceZoomAnimation()
}
R.id.sort_by_trending -> {
activity.result.sort = Anilist.sortBy[2]
binding.sortByFilter.setImageResource(R.drawable.ic_round_star_graph_24)
startBounceZoomAnimation()
}
R.id.sort_by_recent -> {
activity.result.sort = Anilist.sortBy[3]
binding.sortByFilter.setImageResource(R.drawable.ic_round_new_releases_24)
startBounceZoomAnimation()
}
R.id.sort_by_a_z -> {
activity.result.sort = Anilist.sortBy[4]
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_list_24)
startBounceZoomAnimation()
}
R.id.sort_by_z_a -> {
activity.result.sort = Anilist.sortBy[5]
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_list_24_reverse)
startBounceZoomAnimation()
}
R.id.sort_by_pure_pain -> {
activity.result.sort = Anilist.sortBy[6]
binding.sortByFilter.setImageResource(R.drawable.ic_round_assist_walker_24)
startBounceZoomAnimation()
}
}
true
}
popupMenu.show()
}
binding.countryFilter.setOnClickListener {
val popupMenu = PopupMenu(requireContext(), it)
popupMenu.menuInflater.inflate(R.menu.country_filter_menu, popupMenu.menu)
popupMenu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.country_global -> {
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_search_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
R.id.country_china -> {
activity.result.countryOfOrigin = "CN"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_china_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
R.id.country_south_korea -> {
activity.result.countryOfOrigin = "KR"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_south_korea_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
R.id.country_japan -> {
activity.result.countryOfOrigin = "JP"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_japan_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
R.id.country_taiwan -> {
activity.result.countryOfOrigin = "TW"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_taiwan_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
}
true
}
popupMenu.show()
}
binding.searchFilterApply.setOnClickListener { binding.searchFilterApply.setOnClickListener {
activity.result.apply { activity.result.apply {
status = binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null }
source = binding.searchSource.text.toString().replace(" ", "_").ifBlank { null }
format = binding.searchFormat.text.toString().ifBlank { null } format = binding.searchFormat.text.toString().ifBlank { null }
sort = binding.searchSortBy.text.toString().ifBlank { null }
?.let { Anilist.sortBy[resources.getStringArray(R.array.sort_by).indexOf(it)] }
season = binding.searchSeason.text.toString().ifBlank { null } season = binding.searchSeason.text.toString().ifBlank { null }
seasonYear = binding.searchYear.text.toString().toIntOrNull() if (activity.result.type == "ANIME") {
seasonYear = binding.searchYear.text.toString().toIntOrNull()
} else {
startYear = binding.searchYear.text.toString().toIntOrNull()
}
sort = activity.result.sort
countryOfOrigin = activity.result.countryOfOrigin
genres = selectedGenres genres = selectedGenres
tags = selectedTags tags = selectedTags
excludedGenres = exGenres excludedGenres = exGenres
@@ -67,15 +265,23 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
binding.searchFilterCancel.setOnClickListener { binding.searchFilterCancel.setOnClickListener {
dismiss() dismiss()
} }
val format =
binding.searchSortBy.setText(activity.result.sort?.let { if (activity.result.type == "ANIME") Anilist.animeStatus else Anilist.mangaStatus
resources.getStringArray(R.array.sort_by)[Anilist.sortBy.indexOf(it)] binding.searchStatus.setText(activity.result.status?.replace("_", " "))
}) binding.searchStatus.setAdapter(
binding.searchSortBy.setAdapter(
ArrayAdapter( ArrayAdapter(
binding.root.context, binding.root.context,
R.layout.item_dropdown, R.layout.item_dropdown,
resources.getStringArray(R.array.sort_by) format
)
)
binding.searchSource.setText(activity.result.source?.replace("_", " "))
binding.searchSource.setAdapter(
ArrayAdapter(
binding.root.context,
R.layout.item_dropdown,
Anilist.source.toTypedArray()
) )
) )
@@ -84,11 +290,25 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
ArrayAdapter( ArrayAdapter(
binding.root.context, binding.root.context,
R.layout.item_dropdown, R.layout.item_dropdown,
(if (activity.result.type == "ANIME") Anilist.anime_formats else Anilist.manga_formats).toTypedArray() (if (activity.result.type == "ANIME") Anilist.animeFormats else Anilist.mangaFormats).toTypedArray()
) )
) )
if (activity.result.type == "MANGA") binding.searchSeasonYearCont.visibility = GONE if (activity.result.type == "ANIME") {
binding.searchYear.setText(activity.result.seasonYear?.toString())
} else {
binding.searchYear.setText(activity.result.startYear?.toString())
}
binding.searchYear.setAdapter(
ArrayAdapter(
binding.root.context,
R.layout.item_dropdown,
(1970 until Calendar.getInstance().get(Calendar.YEAR) + 2).map { it.toString() }
.reversed().toTypedArray()
)
)
if (activity.result.type == "MANGA") binding.searchSeasonCont.visibility = GONE
else { else {
binding.searchSeason.setText(activity.result.season) binding.searchSeason.setText(activity.result.season)
binding.searchSeason.setAdapter( binding.searchSeason.setAdapter(
@@ -98,16 +318,6 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
Anilist.seasons.toTypedArray() Anilist.seasons.toTypedArray()
) )
) )
binding.searchYear.setText(activity.result.seasonYear?.toString())
binding.searchYear.setAdapter(
ArrayAdapter(
binding.root.context,
R.layout.item_dropdown,
(1970 until Calendar.getInstance().get(Calendar.YEAR) + 2).map { it.toString() }
.reversed().toTypedArray()
)
)
} }
binding.searchFilterGenres.adapter = FilterChipAdapter(Anilist.genres ?: listOf()) { chip -> binding.searchFilterGenres.adapter = FilterChipAdapter(Anilist.genres ?: listOf()) { chip ->

View File

@@ -1,6 +1,5 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.annotation.SuppressLint
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@@ -22,7 +21,6 @@ abstract class SourceAdapter(
return SourceViewHolder(binding) return SourceViewHolder(binding)
} }
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: SourceViewHolder, position: Int) { override fun onBindViewHolder(holder: SourceViewHolder, position: Int) {
val binding = holder.binding val binding = holder.binding
val character = sources[position] val character = sources[position]

View File

@@ -65,7 +65,7 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
i = media!!.selected!!.sourceIndex i = media!!.selected!!.sourceIndex
val source = if (media!!.anime != null) { val source = if (media!!.anime != null) {
(if (!media!!.isAdult) AnimeSources else HAnimeSources)[i!!] (if (media!!.isAdult) HAnimeSources else AnimeSources)[i!!]
} else { } else {
anime = false anime = false
(if (media!!.isAdult) HMangaSources else MangaSources)[i!!] (if (media!!.isAdult) HMangaSources else MangaSources)[i!!]

View File

@@ -6,6 +6,7 @@ import android.view.ViewGroup
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.isGone
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@@ -114,7 +115,7 @@ class StudioActivity : AppCompatActivity() {
} }
override fun onResume() { override fun onResume() {
binding.studioProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE binding.studioProgressBar.isGone = loaded
super.onResume() super.onResume()
} }
} }

View File

@@ -5,19 +5,19 @@ import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.parsers.SubtitleType import ani.dantotsu.parsers.SubtitleType
import ani.dantotsu.snackString import ani.dantotsu.snackString
import com.anggrayudi.storage.file.openOutputStream
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 loadSubtitleType(context: Context, url: String): SubtitleType = suspend fun loadSubtitleType(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>()
@@ -51,21 +51,17 @@ class SubtitleDownloader {
downloadedType: DownloadedType downloadedType: DownloadedType
) { ) {
try { try {
val directory = DownloadsManager.getDirectory( val directory = DownloadsManager.getSubDirectory(
context, context,
downloadedType.type, downloadedType.type,
downloadedType.title, false,
downloadedType.chapter downloadedType.titleName,
) downloadedType.chapterName
if (!directory.exists()) { //just in case ) ?: throw Exception("Could not create directory")
directory.mkdirs() val type = loadSubtitleType(url)
} directory.findFile("subtitle.${type}")?.delete()
val type = loadSubtitleType(context, url) val subtitleFile = directory.createFile("*/*", "subtitle.${type}")
val subtiteFile = File(directory, "subtitle.${type}") ?: throw Exception("Could not create subtitle file")
if (subtiteFile.exists()) {
subtiteFile.delete()
}
subtiteFile.createNewFile()
val client = Injekt.get<NetworkHelper>().client val client = Injekt.get<NetworkHelper>().client
val request = Request.Builder().url(url).build() val request = Request.Builder().url(url).build()
@@ -77,7 +73,8 @@ class SubtitleDownloader {
} }
reponse.body.byteStream().use { input -> reponse.body.byteStream().use { input ->
subtiteFile.outputStream().use { output -> subtitleFile.openOutputStream(context, false).use { output ->
if (output == null) throw Exception("Could not open output stream")
input.copyTo(output) input.copyTo(output)
} }
} }

View File

@@ -1,136 +0,0 @@
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,127 +0,0 @@
package ani.dantotsu.media.anime
import java.util.Locale
import java.util.regex.Matcher
import java.util.regex.Pattern
class AnimeNameAdapter {
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? {
val seasonPattern: Pattern = Pattern.compile(seasonRegex, Pattern.CASE_INSENSITIVE)
val seasonMatcher: Matcher = seasonPattern.matcher(text)
return if (seasonMatcher.find()) {
seasonMatcher.group(2)?.toInt()
} else {
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

@@ -1,11 +1,9 @@
package ani.dantotsu.media.anime package ani.dantotsu.media.anime
import android.annotation.SuppressLint
import android.content.Intent 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.ImageButton import android.widget.ImageButton
@@ -13,22 +11,34 @@ import android.widget.LinearLayout
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.startActivity import androidx.core.content.ContextCompat.startActivity
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.* import ani.dantotsu.FileUrl
import ani.dantotsu.R
import ani.dantotsu.currActivity
import ani.dantotsu.databinding.DialogLayoutBinding 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.displayTimer
import ani.dantotsu.isOnline
import ani.dantotsu.loadImage
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.SourceSearchDialogFragment import ani.dantotsu.media.SourceSearchDialogFragment
import ani.dantotsu.openSettings
import ani.dantotsu.others.LanguageMapper import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.others.webview.CookieCatcher 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.px
import ani.dantotsu.settings.FAQActivity
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.toast
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.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_SUBSCRIPTION_CHECK import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_SUBSCRIPTION_CHECK
@@ -42,7 +52,7 @@ class AnimeWatchAdapter(
private val fragment: AnimeWatchFragment, private val fragment: AnimeWatchFragment,
private val watchSources: WatchSources private val watchSources: WatchSources
) : RecyclerView.Adapter<AnimeWatchAdapter.ViewHolder>() { ) : RecyclerView.Adapter<AnimeWatchAdapter.ViewHolder>() {
private var autoSelect = true
var subscribe: MediaDetailsActivity.PopImageButton? = null var subscribe: MediaDetailsActivity.PopImageButton? = null
private var _binding: ItemAnimeWatchBinding? = null private var _binding: ItemAnimeWatchBinding? = null
@@ -54,7 +64,6 @@ class AnimeWatchAdapter(
private var nestedDialog: AlertDialog? = null private var nestedDialog: AlertDialog? = null
@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
@@ -97,15 +106,12 @@ class AnimeWatchAdapter(
null null
) )
} }
val offline = if (!isOnline(binding.root.context) || PrefManager.getVal( val offline = !isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode)
PrefName.OfflineMode
)
) View.GONE else View.VISIBLE
binding.animeSourceNameContainer.visibility = offline binding.animeSourceNameContainer.isGone = offline
binding.animeSourceSettings.visibility = offline binding.animeSourceSettings.isGone = offline
binding.animeSourceSearch.visibility = offline binding.animeSourceSearch.isGone = offline
binding.animeSourceTitle.visibility = offline binding.animeSourceTitle.isGone = offline
//Source Selection //Source Selection
var source = var source =
@@ -117,8 +123,7 @@ class AnimeWatchAdapter(
this.selectDub = media.selected!!.preferDub this.selectDub = media.selected!!.preferDub
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.isVisible = isDubAvailableSeparately()
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
} }
} }
@@ -137,8 +142,7 @@ class AnimeWatchAdapter(
changing = true changing = true
binding.animeSourceDubbed.isChecked = selectDub binding.animeSourceDubbed.isChecked = selectDub
changing = false changing = false
binding.animeSourceDubbedCont.visibility = binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
source = i source = i
setLanguageList(0, i) setLanguageList(0, i)
} }
@@ -158,14 +162,12 @@ class AnimeWatchAdapter(
changing = true changing = true
binding.animeSourceDubbed.isChecked = selectDub binding.animeSourceDubbed.isChecked = selectDub
changing = false changing = false
binding.animeSourceDubbedCont.visibility = binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
if (isDubAvailableSeparately()) View.VISIBLE else View.GONE
setLanguageList(i, source) setLanguageList(i, source)
} }
subscribeButton(false) subscribeButton(false)
fragment.loadEpisodes(media.selected!!.sourceIndex, true) fragment.loadEpisodes(media.selected!!.sourceIndex, true)
} ?: run { } ?: run { }
}
} }
//settings //settings
@@ -223,9 +225,9 @@ class AnimeWatchAdapter(
else -> dialogBinding.animeSourceList else -> dialogBinding.animeSourceList
} }
when (style) { when (style) {
0 -> dialogBinding.layoutText.text = "List" 0 -> dialogBinding.layoutText.setText(R.string.list)
1 -> dialogBinding.layoutText.text = "Grid" 1 -> dialogBinding.layoutText.setText(R.string.grid)
2 -> dialogBinding.layoutText.text = "Compact" 2 -> dialogBinding.layoutText.setText(R.string.compact)
else -> dialogBinding.animeSourceList else -> dialogBinding.animeSourceList
} }
selected.alpha = 1f selected.alpha = 1f
@@ -237,24 +239,24 @@ class AnimeWatchAdapter(
dialogBinding.animeSourceList.setOnClickListener { dialogBinding.animeSourceList.setOnClickListener {
selected(it as ImageButton) selected(it as ImageButton)
style = 0 style = 0
dialogBinding.layoutText.text = "List" dialogBinding.layoutText.setText(R.string.list)
run = true run = true
} }
dialogBinding.animeSourceGrid.setOnClickListener { dialogBinding.animeSourceGrid.setOnClickListener {
selected(it as ImageButton) selected(it as ImageButton)
style = 1 style = 1
dialogBinding.layoutText.text = "Grid" dialogBinding.layoutText.setText(R.string.grid)
run = true run = true
} }
dialogBinding.animeSourceCompact.setOnClickListener { dialogBinding.animeSourceCompact.setOnClickListener {
selected(it as ImageButton) selected(it as ImageButton)
style = 2 style = 2
dialogBinding.layoutText.text = "Compact" dialogBinding.layoutText.setText(R.string.compact)
run = true run = true
} }
dialogBinding.animeWebviewContainer.setOnClickListener { dialogBinding.animeWebviewContainer.setOnClickListener {
if (!WebViewUtil.supportsWebView(fragment.requireContext())) { if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
toast("WebView not installed") toast(R.string.webview_not_installed)
} }
//start CookieCatcher activity //start CookieCatcher activity
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) { if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
@@ -307,7 +309,6 @@ class AnimeWatchAdapter(
} }
//Chips //Chips
@SuppressLint("SetTextI18n")
fun updateChips(limit: Int, names: Array<String>, arr: Array<Int>, selected: Int = 0) { fun updateChips(limit: Int, names: Array<String>, arr: Array<Int>, selected: Int = 0) {
val binding = _binding val binding = _binding
if (binding != null) { if (binding != null) {
@@ -329,7 +330,9 @@ class AnimeWatchAdapter(
0 0
) )
} }
chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
val chipText = "${names[limit * (position)]} - ${names[last - 1]}"
chip.text = chipText
chip.setTextColor( chip.setTextColor(
ContextCompat.getColorStateList( ContextCompat.getColorStateList(
fragment.requireContext(), fragment.requireContext(),
@@ -363,7 +366,6 @@ class AnimeWatchAdapter(
_binding?.animeSourceChipGroup?.removeAllViews() _binding?.animeSourceChipGroup?.removeAllViews()
} }
@SuppressLint("SetTextI18n")
fun handleEpisodes() { fun handleEpisodes() {
val binding = _binding val binding = _binding
if (binding != null) { if (binding != null) {
@@ -371,9 +373,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 = val appEp = PrefManager.getCustomVal<String?>(
PrefManager.getCustomVal<String?>("${media.id}_current_ep", "")?.toIntOrNull() "${media.id}_current_ep", ""
?: 1 )?.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)) {
@@ -403,21 +405,27 @@ class AnimeWatchAdapter(
} }
val ep = media.anime.episodes!![continueEp]!! val ep = media.anime.episodes!![continueEp]!!
val cleanedTitle = ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) } val cleanedTitle = ep.title?.let { MediaNameAdapter.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 ""}${"\n$cleanedTitle"}" currActivity()!!.getString(
R.string.continue_episode, ep.number, if (ep.filler)
currActivity()!!.getString(R.string.filler_tag)
else
"", 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 < PrefManager.getVal<Float>( if (
PrefName.WatchPercentage (binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams)
) .weight < PrefManager.getVal<Float>(PrefName.WatchPercentage)
) { ) {
binding.animeSourceContinue.performClick() binding.animeSourceContinue.performClick()
fragment.continueEp = false fragment.continueEp = false
@@ -428,13 +436,31 @@ class AnimeWatchAdapter(
} }
binding.animeSourceProgressBar.visibility = View.GONE binding.animeSourceProgressBar.visibility = View.GONE
if (media.anime.episodes!!.isNotEmpty()) {
binding.animeSourceNotFound.visibility = View.GONE val sourceFound = media.anime.episodes!!.isNotEmpty()
binding.faqbutton.visibility = View.GONE} binding.animeSourceNotFound.isGone = sourceFound
else { binding.faqbutton.isGone = sourceFound
binding.animeSourceNotFound.visibility = View.VISIBLE
binding.faqbutton.visibility = View.VISIBLE if (!sourceFound && PrefManager.getVal(PrefName.SearchSources) && autoSelect) {
if (binding.animeSource.adapter.count > media.selected!!.sourceIndex + 1) {
val nextIndex = media.selected!!.sourceIndex + 1
binding.animeSource.setText(
binding.animeSource.adapter
.getItem(nextIndex).toString(), false
)
fragment.onSourceChange(nextIndex).apply {
binding.animeSourceTitle.text = showUserText
showUserTextListener =
{ MainScope().launch { binding.animeSourceTitle.text = it } }
binding.animeSourceDubbed.isChecked = selectDub
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
setLanguageList(0, nextIndex)
}
subscribeButton(false)
fragment.loadEpisodes(nextIndex, false)
} }
}
binding.animeSource.setOnClickListener { autoSelect = false }
} else { } else {
binding.animeSourceContinue.visibility = View.GONE binding.animeSourceContinue.visibility = View.GONE
binding.animeSourceNotFound.visibility = View.GONE binding.animeSourceNotFound.visibility = View.GONE
@@ -480,8 +506,7 @@ class AnimeWatchAdapter(
inner class ViewHolder(val binding: ItemAnimeWatchBinding) : inner class ViewHolder(val binding: ItemAnimeWatchBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
init { init {
//Timer displayTimer(media, binding.animeSourceContainer)
countDown(media, binding.animeSourceContainer)
} }
} }
} }

View File

@@ -17,34 +17,45 @@ import androidx.annotation.OptIn
import androidx.cardview.widget.CardView import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils import androidx.core.math.MathUtils
import androidx.core.view.isGone
import androidx.core.view.isVisible
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.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.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.* import ani.dantotsu.FileUrl
import ani.dantotsu.R
import ani.dantotsu.databinding.FragmentAnimeWatchBinding import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.compareName
import ani.dantotsu.download.anime.AnimeDownloaderService import ani.dantotsu.download.anime.AnimeDownloaderService
import ani.dantotsu.download.video.ExoplayerDownloadService import ani.dantotsu.dp
import ani.dantotsu.isOnline
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.media.MediaNameAdapter
import ani.dantotsu.media.MediaType
import ani.dantotsu.navBarHeight
import ani.dantotsu.notifications.subscription.SubscriptionHelper
import ani.dantotsu.notifications.subscription.SubscriptionHelper.Companion.saveSubscription
import ani.dantotsu.others.LanguageMapper 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.setNavigationTheme
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.notifications.subscription.SubscriptionHelper import ani.dantotsu.snackString
import ani.dantotsu.notifications.subscription.SubscriptionHelper.Companion.saveSubscription import ani.dantotsu.util.StoragePermissions.Companion.accessAlertDialog
import ani.dantotsu.util.StoragePermissions.Companion.hasDirAccess
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
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
@@ -188,10 +199,16 @@ class AnimeWatchFragment : Fragment() {
ConcatAdapter(headerAdapter, episodeAdapter) ConcatAdapter(headerAdapter, episodeAdapter)
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
awaitAll( val offline =
async { model.loadKitsuEpisodes(media) }, !isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode)
async { model.loadFillerEpisodes(media) } if (offline) {
) media.selected!!.sourceIndex = model.watchSources!!.list.lastIndex
} else {
awaitAll(
async { model.loadKitsuEpisodes(media) },
async { model.loadFillerEpisodes(media) }
)
}
model.loadEpisodes(media, media.selected!!.sourceIndex) model.loadEpisodes(media, media.selected!!.sourceIndex)
} }
loaded = true loaded = true
@@ -216,7 +233,7 @@ class AnimeWatchFragment : Fragment() {
if (media.anime!!.kitsuEpisodes!!.containsKey(i)) { if (media.anime!!.kitsuEpisodes!!.containsKey(i)) {
episode.desc = episode.desc =
media.anime!!.kitsuEpisodes!![i]?.desc ?: episode.desc media.anime!!.kitsuEpisodes!![i]?.desc ?: episode.desc
episode.title = if (AnimeNameAdapter.removeEpisodeNumberCompletely( episode.title = if (MediaNameAdapter.removeEpisodeNumberCompletely(
episode.title ?: "" episode.title ?: ""
).isBlank() ).isBlank()
) media.anime!!.kitsuEpisodes!![i]?.title ) media.anime!!.kitsuEpisodes!![i]?.title
@@ -340,16 +357,12 @@ class AnimeWatchFragment : Fragment() {
val changeUIVisibility: (Boolean) -> Unit = { show -> val changeUIVisibility: (Boolean) -> Unit = { show ->
val activity = activity val activity = activity
if (activity is MediaDetailsActivity && isAdded) { if (activity is MediaDetailsActivity && isAdded) {
val visibility = if (show) View.VISIBLE else View.GONE activity.findViewById<AppBarLayout>(R.id.mediaAppBar).isVisible = show
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).visibility = visibility activity.findViewById<ViewPager2>(R.id.mediaViewPager).isVisible = show
activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility activity.findViewById<CardView>(R.id.mediaCover).isVisible = show
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility activity.findViewById<CardView>(R.id.mediaClose).isVisible = show
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility activity.navBar.isVisible = show
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).isGone = show
activity.tabLayout.setVisibility(visibility)
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
if (show) View.GONE else View.VISIBLE
} }
} }
var itemSelected = false var itemSelected = false
@@ -417,7 +430,29 @@ class AnimeWatchFragment : Fragment() {
} }
fun onAnimeEpisodeDownloadClick(i: String) { fun onAnimeEpisodeDownloadClick(i: String) {
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true) activity?.let {
if (!hasDirAccess(it)) {
(it as MediaDetailsActivity).accessAlertDialog(it.launcher) { success ->
if (success) {
model.onEpisodeClick(
media,
i,
requireActivity().supportFragmentManager,
isDownload = true
)
} else {
snackString(getString(R.string.download_permission_required))
}
}
} else {
model.onEpisodeClick(
media,
i,
requireActivity().supportFragmentManager,
isDownload = true
)
}
}
} }
fun onAnimeEpisodeStopDownloadClick(i: String) { fun onAnimeEpisodeStopDownloadClick(i: String) {
@@ -435,10 +470,11 @@ class AnimeWatchFragment : Fragment() {
DownloadedType( DownloadedType(
media.mainName(), media.mainName(),
i, i,
DownloadedType.Type.ANIME MediaType.ANIME
) )
) ) {
episodeAdapter.purgeDownload(i) episodeAdapter.purgeDownload(i)
}
} }
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
@@ -447,22 +483,13 @@ class AnimeWatchFragment : Fragment() {
DownloadedType( DownloadedType(
media.mainName(), media.mainName(),
i, i,
DownloadedType.Type.ANIME MediaType.ANIME
) )
) ) {
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i) val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
val id = PrefManager.getAnimeDownloadPreferences().getString( PrefManager.getAnimeDownloadPreferences().edit().remove(taskName).apply()
taskName, episodeAdapter.deleteDownload(i)
"" }
) ?: ""
PrefManager.getAnimeDownloadPreferences().edit().remove(taskName).apply()
DownloadService.sendRemoveDownload(
requireContext(),
ExoplayerDownloadService::class.java,
id,
true
)
episodeAdapter.deleteDownload(i)
} }
private val downloadStatusReceiver = object : BroadcastReceiver() { private val downloadStatusReceiver = object : BroadcastReceiver() {
@@ -526,8 +553,8 @@ class AnimeWatchFragment : Fragment() {
episodeAdapter.updateType(style ?: PrefManager.getVal(PrefName.AnimeDefaultView)) episodeAdapter.updateType(style ?: PrefManager.getVal(PrefName.AnimeDefaultView))
episodeAdapter.notifyItemRangeInserted(0, arr.size) episodeAdapter.notifyItemRangeInserted(0, arr.size)
for (download in downloadManager.animeDownloadedTypes) { for (download in downloadManager.animeDownloadedTypes) {
if (download.title == media.mainName()) { if (media.compareName(download.titleName)) {
episodeAdapter.stopDownload(download.chapter) episodeAdapter.stopDownload(download.chapterName)
} }
} }
} }

View File

@@ -1,6 +1,5 @@
package ani.dantotsu.media.anime package ani.dantotsu.media.anime
import android.annotation.SuppressLint
import android.app.AlertDialog import android.app.AlertDialog
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@@ -8,18 +7,21 @@ import android.view.ViewGroup
import android.view.animation.LinearInterpolator import android.view.animation.LinearInterpolator
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.core.view.isVisible
import androidx.lifecycle.coroutineScope import androidx.lifecycle.coroutineScope
import androidx.media3.common.util.UnstableApi 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.R
import ani.dantotsu.connections.updateProgress import ani.dantotsu.connections.updateProgress
import ani.dantotsu.currContext
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.DownloadsManager.Companion.getDirSize
import ani.dantotsu.download.video.Helper
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.MediaType
import ani.dantotsu.setAnimation
import ani.dantotsu.settings.saving.PrefManager 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
@@ -53,15 +55,7 @@ class EpisodeAdapter(
var arr: List<Episode> = arrayListOf(), var arr: List<Episode> = arrayListOf(),
var offlineMode: Boolean var offlineMode: Boolean
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
val context = fragment.requireContext()
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) {
@@ -97,11 +91,10 @@ class EpisodeAdapter(
return type return type
} }
@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 = if (!ep.title.isNullOrEmpty() && ep.title != "null") { val title = if (!ep.title.isNullOrEmpty() && ep.title != "null") {
ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) } ep.title?.let { MediaNameAdapter.removeEpisodeNumber(it) }
} else { } else {
ep.number ep.number
} ?: "" } ?: ""
@@ -125,8 +118,7 @@ class EpisodeAdapter(
binding.itemEpisodeFiller.visibility = View.GONE binding.itemEpisodeFiller.visibility = View.GONE
binding.itemEpisodeFillerView.visibility = View.GONE binding.itemEpisodeFillerView.visibility = View.GONE
} }
binding.itemEpisodeDesc.visibility = binding.itemEpisodeDesc.isVisible = !ep.desc.isNullOrBlank()
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) holder.bind(ep.number, ep.downloadProgress, ep.desc)
@@ -203,8 +195,7 @@ class EpisodeAdapter(
val binding = holder.binding val binding = holder.binding
setAnimation(fragment.requireContext(), holder.binding.root) setAnimation(fragment.requireContext(), holder.binding.root)
binding.itemEpisodeNumber.text = ep.number binding.itemEpisodeNumber.text = ep.number
binding.itemEpisodeFillerView.visibility = binding.itemEpisodeFillerView.isVisible = ep.filler
if (ep.filler) View.VISIBLE else View.GONE
if (media.userProgress != null) { if (media.userProgress != null) {
if ((ep.number.toFloatOrNull() ?: 9999f) <= media.userProgress!!.toFloat()) if ((ep.number.toFloatOrNull() ?: 9999f) <= media.userProgress!!.toFloat())
binding.itemEpisodeViewedCover.visibility = View.VISIBLE binding.itemEpisodeViewedCover.visibility = View.VISIBLE
@@ -248,17 +239,8 @@ class EpisodeAdapter(
// Find the position of the chapter and notify only that item // Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == episodeNumber } val position = arr.indexOfFirst { it.number == episodeNumber }
if (position != -1) { if (position != -1) {
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(
media.mainName(),
episodeNumber
)
val id = PrefManager.getAnimeDownloadPreferences().getString(
taskName,
""
) ?: ""
val size = try { val size = try {
val download = index.getDownload(id) bytesToHuman(getDirSize(context, MediaType.ANIME, media.mainName(), episodeNumber))
bytesToHuman(download?.bytesDownloaded ?: 0)
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
@@ -429,7 +411,7 @@ class EpisodeAdapter(
if (bytes < 0) return null if (bytes < 0) return null
val unit = 1000 val unit = 1000
if (bytes < unit) return "$bytes B" if (bytes < unit) return "$bytes B"
val exp = (Math.log(bytes.toDouble()) / ln(unit.toDouble())).toInt() val exp = (ln(bytes.toDouble()) / ln(unit.toDouble())).toInt()
val pre = ("KMGTPE")[exp - 1] val pre = ("KMGTPE")[exp - 1]
return String.format("%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre) return String.format("%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre)
} }

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,8 @@ package ani.dantotsu.media.anime
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.AlertDialog import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
@@ -12,29 +14,51 @@ 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 androidx.activity.result.ActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope 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.BottomSheetDialogFragment
import ani.dantotsu.R
import ani.dantotsu.addons.download.DownloadAddonManager
import ani.dantotsu.addons.torrent.TorrentAddonManager
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.copyToClipboard
import ani.dantotsu.currActivity
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.DownloadedType
import ani.dantotsu.download.video.Helper import ani.dantotsu.download.video.Helper
import ani.dantotsu.hideSystemBars
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.SubtitleDownloader
import ani.dantotsu.navBarHeight
import ani.dantotsu.others.Download.download import ani.dantotsu.others.Download.download
import ani.dantotsu.parsers.Subtitle import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.Video
import ani.dantotsu.parsers.VideoExtractor import ani.dantotsu.parsers.VideoExtractor
import ani.dantotsu.parsers.VideoType import ani.dantotsu.parsers.VideoType
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.toast
import ani.dantotsu.tryWith
import ani.dantotsu.util.Logger
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
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.text.DecimalFormat import java.text.DecimalFormat
@@ -211,10 +235,101 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
} }
private val externalPlayerResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result: ActivityResult ->
Logger.log(result.data.toString())
}
private fun exportMagnetIntent(episode: Episode, video: Video): Intent {
val amnis = "com.amnis"
return Intent(Intent.ACTION_VIEW).apply {
component = ComponentName(amnis, "$amnis.gui.player.PlayerActivity")
data = Uri.parse(video.file.url)
putExtra("title", "${media?.name} - ${episode.title}")
putExtra("position", 0)
putExtra(Intent.EXTRA_RETURN_RESULT, true)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
putExtra("secure_uri", true)
val headersArray = arrayOf<String>()
video.file.headers.forEach {
headersArray.plus(arrayOf(it.key, it.value))
}
putExtra("headers", headersArray)
}
}
@OptIn(DelicateCoroutinesApi::class)
@SuppressLint("UnsafeOptInUsageError") @SuppressLint("UnsafeOptInUsageError")
fun startExoplayer(media: Media) { fun startExoplayer(media: Media) {
prevEpisode = null prevEpisode = null
episode?.let { ep ->
val video = ep.extractors?.find {
it.server.name == ep.selectedExtractor
}?.videos?.getOrNull(ep.selectedVideo)
video?.file?.url?.let { url ->
if (url.startsWith("magnet:") || url.endsWith(".torrent")) {
val torrentExtension = Injekt.get<TorrentAddonManager>()
if (torrentExtension.isAvailable()) {
val activity = currActivity() ?: requireActivity()
launchIO {
val extension = torrentExtension.extension!!.extension
torrentExtension.torrentHash?.let {
extension.removeTorrent(it)
}
val index = if (url.contains("index=")) {
url.substringAfter("index=").toIntOrNull() ?: 0
} else 0
Logger.log("Sending: ${url}, ${video.quality}, $index")
val currentTorrent = extension.addTorrent(
url, video.quality.toString(), "", "", false
)
torrentExtension.torrentHash = currentTorrent.hash
video.file.url = extension.getLink(currentTorrent, index)
Logger.log("Received: ${video.file.url}")
if (launch == true) {
Intent(activity, ExoplayerView::class.java).apply {
ExoplayerView.media = media
ExoplayerView.initialized = true
startActivity(this)
}
} else {
model.setEpisode(
media.anime!!.episodes!![media.anime.selectedEpisode!!]!!,
"startExo no launch"
)
}
dismiss()
}
} else {
try {
externalPlayerResult.launch(exportMagnetIntent(ep, video))
} catch (e: ActivityNotFoundException) {
val amnis = "com.amnis"
try {
startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse("market://details?id=$amnis")
)
)
dismiss()
} catch (e: ActivityNotFoundException) {
startActivity(
Intent(
Intent.ACTION_VIEW,
Uri.parse("https://play.google.com/store/apps/details?id=$amnis")
)
)
}
}
}
return
}
}
}
dismiss() dismiss()
if (launch!! || model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)) { if (launch!! || model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)) {
stopAddingToList() stopAddingToList()
@@ -302,7 +417,6 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
) )
} }
@SuppressLint("SetTextI18n")
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]
@@ -311,6 +425,49 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
} else { } else {
binding.urlDownload.visibility = View.GONE binding.urlDownload.visibility = View.GONE
} }
val subtitles = extractor.subtitles
if (subtitles.isNotEmpty()) {
binding.urlSub.visibility = View.VISIBLE
} else {
binding.urlSub.visibility = View.GONE
}
binding.urlSub.setOnClickListener {
if (subtitles.isNotEmpty()) {
val subtitleNames = subtitles.map { it.language }
var subtitleToDownload: Subtitle? = null
val alertDialog = AlertDialog.Builder(context, R.style.MyPopup)
.setTitle("Download Subtitle")
.setSingleChoiceItems(
subtitleNames.toTypedArray(),
-1
) { _, which ->
subtitleToDownload = subtitles[which]
}
.setPositiveButton("Download") { dialog, _ ->
scope.launch {
if (subtitleToDownload != null) {
SubtitleDownloader.downloadSubtitle(
requireContext(),
subtitleToDownload!!.file.url,
DownloadedType(
media!!.mainName(),
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.number,
MediaType.ANIME
)
)
}
}
dialog.dismiss()
}
.setNegativeButton("Cancel") { dialog, _ ->
dialog.dismiss()
}
.show()
alertDialog.window?.setDimAmount(0.8f)
} else {
snackString("No Subtitles Available")
}
}
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
@@ -323,20 +480,52 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
media!!.userPreferredName media!!.userPreferredName
) )
} else { } else {
val downloadAddonManager: DownloadAddonManager = Injekt.get()
if (!downloadAddonManager.isAvailable()){
toast("Download Extension not available")
return@setSafeOnClickListener
}
val episode = media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!! val episode = media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!
val selectedVideo = val selectedVideo =
if (extractor.videos.size > episode.selectedVideo) extractor.videos[episode.selectedVideo] else null if (extractor.videos.size > episode.selectedVideo) extractor.videos[episode.selectedVideo] else null
val subtitles = extractor.subtitles
val subtitleNames = subtitles.map { it.language } val subtitleNames = subtitles.map { it.language }
var subtitleToDownload: Subtitle? = null var subtitleToDownload: Subtitle? = null
val activity = currActivity()?:requireActivity() val activity = currActivity() ?: requireActivity()
selectedVideo?.file?.url?.let { url ->
if (url.startsWith("magnet:") || url.endsWith(".torrent")) {
val torrentExtension = Injekt.get<TorrentAddonManager>()
if (!torrentExtension.isAvailable()) {
toast("Torrent Extension not available")
return@setSafeOnClickListener
}
runBlocking {
withContext(Dispatchers.IO) {
val extension = torrentExtension.extension!!.extension
torrentExtension.torrentHash?.let {
extension.removeTorrent(it)
}
val index = if (url.contains("index=")) {
url.substringAfter("index=").toIntOrNull() ?: 0
} else 0
Logger.log("Sending: ${url}, ${selectedVideo.quality}, $index")
val currentTorrent = extension.addTorrent(
url, selectedVideo.quality.toString(), "", "", false
)
torrentExtension.torrentHash = currentTorrent.hash
selectedVideo.file.url =
extension.getLink(currentTorrent, index)
Logger.log("Received: ${selectedVideo.file.url}")
}
}
}
}
if (subtitles.isNotEmpty()) { if (subtitles.isNotEmpty()) {
val alertDialog = AlertDialog.Builder(context, R.style.MyPopup) val alertDialog = AlertDialog.Builder(context, R.style.MyPopup)
.setTitle("Download Subtitle") .setTitle("Download Subtitle")
.setSingleChoiceItems( .setSingleChoiceItems(
subtitleNames.toTypedArray(), subtitleNames.toTypedArray(),
-1 -1
) { dialog, which -> ) { _, which ->
subtitleToDownload = subtitles[which] subtitleToDownload = subtitles[which]
} }
.setPositiveButton("Download") { _, _ -> .setPositiveButton("Download") { _, _ ->
@@ -401,12 +590,16 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
dismiss() dismiss()
} }
if (video.format == VideoType.CONTAINER) { if (video.format == VideoType.CONTAINER) {
binding.urlSize.visibility = if (video.size != null) View.VISIBLE else View.GONE binding.urlSize.isVisible = video.size != null
binding.urlSize.text = // if video size is null or 0, show "Unknown Size" else show the size in MB
// if video size is null or 0, show "Unknown Size" else show the size in MB val sizeText = getString(
(if (video.extraNote != null) " : " else "") + (if (video.size == 0.0) "Unknown Size" else (DecimalFormat( R.string.mb_size, "${if (video.extraNote != null) " : " else ""}${
"#.##" if (video.size == 0.0) getString(R.string.size_unknown) else DecimalFormat("#.##").format(
).format(video.size ?: 0).toString() + " MB")) video.size ?: 0
)
}"
)
binding.urlSize.text = sizeText
} }
binding.urlNote.visibility = View.VISIBLE binding.urlNote.visibility = View.VISIBLE
binding.urlNote.text = video.format.name binding.urlNote.text = video.format.name

View File

@@ -1,6 +1,5 @@
package ani.dantotsu.media.anime package ani.dantotsu.media.anime
import android.app.Activity
import android.graphics.Color.TRANSPARENT import android.graphics.Color.TRANSPARENT
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@@ -68,7 +67,11 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
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 = PrefManager.getNullableCustomVal("subLang_${mediaID}", null, String::class.java) 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)
} }
@@ -108,12 +111,15 @@ 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? = val selSubs: String? =
PrefManager.getNullableCustomVal("subLang_${mediaID}", null, String::class.java) 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)
} }
} }
val activity: Activity = requireActivity() as ExoplayerView
binding.root.setOnClickListener { binding.root.setOnClickListener {
episode.selectedSubtitle = position - 1 episode.selectedSubtitle = position - 1
model.setEpisode(episode, "Subtitle") model.setEpisode(episode, "Subtitle")

View File

@@ -0,0 +1,119 @@
package ani.dantotsu.media.anime
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.OptIn
import androidx.media3.common.C.TRACK_TYPE_AUDIO
import androidx.media3.common.C.TrackType
import androidx.media3.common.Tracks
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.R
import ani.dantotsu.databinding.BottomSheetSubtitlesBinding
import ani.dantotsu.databinding.ItemSubtitleTextBinding
import java.util.Locale
@OptIn(UnstableApi::class)
class TrackGroupDialogFragment(
instance: ExoplayerView, trackGroups: ArrayList<Tracks.Group>, type: @TrackType Int
) : BottomSheetDialogFragment() {
private var _binding: BottomSheetSubtitlesBinding? = null
private val binding get() = _binding!!
private var instance: ExoplayerView
private var trackGroups: ArrayList<Tracks.Group>
private var type: @TrackType Int
init {
this.instance = instance
this.trackGroups = trackGroups
this.type = type
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = BottomSheetSubtitlesBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (type == TRACK_TYPE_AUDIO) binding.selectionTitle.text = getString(R.string.audio_tracks)
binding.subtitlesRecycler.layoutManager = LinearLayoutManager(requireContext())
binding.subtitlesRecycler.adapter = TrackGroupAdapter()
}
inner class TrackGroupAdapter : RecyclerView.Adapter<TrackGroupAdapter.StreamViewHolder>() {
inner class StreamViewHolder(val binding: ItemSubtitleTextBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StreamViewHolder =
StreamViewHolder(
ItemSubtitleTextBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
@OptIn(UnstableApi::class)
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
val binding = holder.binding
trackGroups[position].let { trackGroup ->
when (val language = trackGroup.getTrackFormat(0).language?.lowercase()) {
null -> {
binding.subtitleTitle.text =
getString(R.string.unknown_track, "Track $position")
}
"none" -> {
binding.subtitleTitle.text = getString(R.string.disabled_track)
}
else -> {
val locale = if (language.contains("-")) {
val parts = language.split("-")
try {
Locale(parts[0], parts[1])
} catch (ignored: Exception) {
null
}
} else {
try {
Locale(language)
} catch (ignored: Exception) {
null
}
}
binding.subtitleTitle.text = locale?.let {
"[${it.language}] ${it.displayName}"
} ?: getString(R.string.unknown_track, language)
}
}
if (trackGroup.isSelected) {
val selected = "${binding.subtitleTitle.text}"
binding.subtitleTitle.text = selected
}
binding.root.setOnClickListener {
dismiss()
instance.onSetTrackGroupOverride(trackGroup, type)
}
}
}
override fun getItemCount(): Int = trackGroups.size
}
override fun onDestroy() {
_binding = null
super.onDestroy()
}
}

View File

@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.view.View import android.view.View
import android.widget.PopupMenu
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.R import ani.dantotsu.R
@@ -11,6 +12,7 @@ import ani.dantotsu.connections.comments.Comment
import ani.dantotsu.connections.comments.CommentsAPI import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.copyToClipboard import ani.dantotsu.copyToClipboard
import ani.dantotsu.databinding.ItemCommentsBinding import ani.dantotsu.databinding.ItemCommentsBinding
import ani.dantotsu.getAppString
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
import ani.dantotsu.others.ImageViewDialog import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.profile.ProfileActivity import ani.dantotsu.profile.ProfileActivity
@@ -28,17 +30,20 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale
import java.util.TimeZone import java.util.TimeZone
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.sqrt import kotlin.math.sqrt
class CommentItem(val comment: Comment, class CommentItem(
private val markwon: Markwon, val comment: Comment,
val parentSection: Section, private val markwon: Markwon,
private val commentsFragment: CommentsFragment, val parentSection: Section,
private val backgroundColor: Int, private val commentsFragment: CommentsFragment,
val commentDepth: Int private val backgroundColor: Int,
) : BindableItem<ItemCommentsBinding>() { val commentDepth: Int
) :
BindableItem<ItemCommentsBinding>() {
lateinit var binding: ItemCommentsBinding lateinit var binding: ItemCommentsBinding
val adapter = GroupieAdapter() val adapter = GroupieAdapter()
private var subCommentIds: MutableList<Int> = mutableListOf() private var subCommentIds: MutableList<Int> = mutableListOf()
@@ -52,18 +57,15 @@ class CommentItem(val comment: Comment,
adapter.add(repliesSection) adapter.add(repliesSection)
} }
@SuppressLint("SetTextI18n")
override fun bind(viewBinding: ItemCommentsBinding, position: Int) { override fun bind(viewBinding: ItemCommentsBinding, position: Int) {
binding = viewBinding binding = viewBinding
setAnimation(binding.root.context, binding.root) setAnimation(binding.root.context, binding.root)
viewBinding.commentRepliesList.layoutManager = LinearLayoutManager(commentsFragment.activity) viewBinding.commentRepliesList.layoutManager =
LinearLayoutManager(commentsFragment.activity)
viewBinding.commentRepliesList.adapter = adapter viewBinding.commentRepliesList.adapter = adapter
val isUserComment = CommentsAPI.userId == comment.userId val isUserComment = CommentsAPI.userId == comment.userId
val levelColor = getAvatarColor(comment.totalVotes, backgroundColor) val levelColor = getAvatarColor(comment.totalVotes, backgroundColor)
markwon.setMarkdown(viewBinding.commentText, comment.content) 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 viewBinding.commentEdit.visibility = if (isUserComment) View.VISIBLE else View.GONE
if (comment.tag == null) { if (comment.tag == null) {
viewBinding.commentUserTagLayout.visibility = View.GONE viewBinding.commentUserTagLayout.visibility = View.GONE
@@ -76,8 +78,15 @@ class CommentItem(val comment: Comment,
if ((comment.replyCount ?: 0) > 0) { if ((comment.replyCount ?: 0) > 0) {
viewBinding.commentTotalReplies.visibility = View.VISIBLE viewBinding.commentTotalReplies.visibility = View.VISIBLE
viewBinding.commentRepliesDivider.visibility = View.VISIBLE viewBinding.commentRepliesDivider.visibility = View.VISIBLE
viewBinding.commentTotalReplies.text = if(repliesVisible) "Hide Replies" else viewBinding.commentTotalReplies.context.run {
"View ${comment.replyCount} repl${if (comment.replyCount == 1) "y" else "ies"}" viewBinding.commentTotalReplies.text = if (repliesVisible)
getString(R.string.hide_replies)
else
if (comment.replyCount == 1)
getString(R.string.view_reply)
else
getString(R.string.view_replies_count, comment.replyCount)
}
} else { } else {
viewBinding.commentTotalReplies.visibility = View.GONE viewBinding.commentTotalReplies.visibility = View.GONE
viewBinding.commentRepliesDivider.visibility = View.GONE viewBinding.commentRepliesDivider.visibility = View.GONE
@@ -87,10 +96,15 @@ class CommentItem(val comment: Comment,
if (repliesVisible) { if (repliesVisible) {
repliesSection.clear() repliesSection.clear()
removeSubCommentIds() removeSubCommentIds()
viewBinding.commentTotalReplies.text = "View ${comment.replyCount} repl${if (comment.replyCount == 1) "y" else "ies"}" viewBinding.commentTotalReplies.context.run {
viewBinding.commentTotalReplies.text = if (comment.replyCount == 1)
getString(R.string.view_reply)
else
getString(R.string.view_replies_count, comment.replyCount)
}
repliesVisible = false repliesVisible = false
} else { } else {
viewBinding.commentTotalReplies.text = "Hide Replies" viewBinding.commentTotalReplies.setText(R.string.hide_replies)
repliesSection.clear() repliesSection.clear()
commentsFragment.viewReplyCallback(this) commentsFragment.viewReplyCallback(this)
repliesVisible = true repliesVisible = true
@@ -99,16 +113,20 @@ class CommentItem(val comment: Comment,
viewBinding.commentUserName.setOnClickListener { viewBinding.commentUserName.setOnClickListener {
ContextCompat.startActivity( ContextCompat.startActivity(
commentsFragment.activity, Intent(commentsFragment.activity, ProfileActivity::class.java) commentsFragment.activity,
Intent(commentsFragment.activity, ProfileActivity::class.java)
.putExtra("userId", comment.userId.toInt()) .putExtra("userId", comment.userId.toInt())
.putExtra("userLVL","[${levelColor.second}]"), null .putExtra("userLVL", "[${levelColor.second}]"),
null
) )
} }
viewBinding.commentUserAvatar.setOnClickListener { viewBinding.commentUserAvatar.setOnClickListener {
ContextCompat.startActivity( ContextCompat.startActivity(
commentsFragment.activity, Intent(commentsFragment.activity, ProfileActivity::class.java) commentsFragment.activity,
Intent(commentsFragment.activity, ProfileActivity::class.java)
.putExtra("userId", comment.userId.toInt()) .putExtra("userId", comment.userId.toInt())
.putExtra("userLVL","[${levelColor.second}]"), null .putExtra("userLVL", "[${levelColor.second}]"),
null
) )
} }
viewBinding.commentText.setOnLongClickListener { viewBinding.commentText.setOnLongClickListener {
@@ -127,39 +145,73 @@ class CommentItem(val comment: Comment,
} }
viewBinding.modBadge.visibility = if (comment.isMod == true) View.VISIBLE else View.GONE 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.adminBadge.visibility = if (comment.isAdmin == true) View.VISIBLE else View.GONE
viewBinding.commentDelete.setOnClickListener { viewBinding.commentInfo.setOnClickListener {
dialogBuilder("Delete Comment", "Are you sure you want to delete this comment?") { val popup = PopupMenu(commentsFragment.requireContext(), viewBinding.commentInfo)
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob()) popup.menuInflater.inflate(R.menu.profile_details_menu, popup.menu)
scope.launch { popup.menu.findItem(R.id.commentDelete)?.isVisible =
val success = CommentsAPI.deleteComment(comment.commentId) isUserComment || CommentsAPI.isAdmin || CommentsAPI.isMod
if (success) { popup.menu.findItem(R.id.commentBanUser)?.isVisible =
snackString("Comment Deleted") (CommentsAPI.isAdmin || CommentsAPI.isMod) && !isUserComment
parentSection.remove(this@CommentItem) popup.menu.findItem(R.id.commentReport)?.isVisible = !isUserComment
} popup.setOnMenuItemClickListener { item ->
} when (item.itemId) {
} R.id.commentReport -> {
} dialogBuilder(
viewBinding.commentBanUser.setOnClickListener { getAppString(R.string.report_comment),
dialogBuilder("Ban User", "Are you sure you want to ban this user?") { getAppString(R.string.report_comment_confirm)
val scope = CoroutineScope(Dispatchers.Main + SupervisorJob()) ) {
scope.launch { CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
val success = CommentsAPI.banUser(comment.userId) val success = CommentsAPI.reportComment(
if (success) { comment.commentId,
snackString("User Banned") comment.username,
} commentsFragment.mediaName,
} comment.userId
} )
} if (success) {
viewBinding.commentReport.setOnClickListener { snackString(R.string.comment_reported)
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) true
if (success) { }
snackString("Comment Reported")
R.id.commentDelete -> {
dialogBuilder(
getAppString(R.string.delete_comment),
getAppString(R.string.delete_comment_confirm)
) {
CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
val success = CommentsAPI.deleteComment(comment.commentId)
if (success) {
snackString(R.string.comment_deleted)
parentSection.remove(this@CommentItem)
}
}
}
true
}
R.id.commentBanUser -> {
dialogBuilder(
getAppString(R.string.ban_user),
getAppString(R.string.ban_user_confirm)
) {
CoroutineScope(Dispatchers.Main + SupervisorJob()).launch {
val success = CommentsAPI.banUser(comment.userId)
if (success) {
snackString(R.string.user_banned)
}
}
}
true
}
else -> {
false
} }
} }
} }
popup.show()
} }
//fill the icon if the user has liked the comment //fill the icon if the user has liked the comment
setVoteButtons(viewBinding) setVoteButtons(viewBinding)
@@ -195,7 +247,6 @@ class CommentItem(val comment: Comment,
comment.upvotes -= 1 comment.upvotes -= 1
} }
comment.downvotes += if (voteType == -1) 1 else -1 comment.downvotes += if (voteType == -1) 1 else -1
notifyChanged() notifyChanged()
} }
} }
@@ -210,7 +261,8 @@ class CommentItem(val comment: Comment,
} }
comment.profilePictureUrl?.let { viewBinding.commentUserAvatar.loadImage(it) } comment.profilePictureUrl?.let { viewBinding.commentUserAvatar.loadImage(it) }
viewBinding.commentUserName.text = comment.username viewBinding.commentUserName.text = comment.username
viewBinding.commentUserLevel.text = "[${levelColor.second}]" val userColor = "[${levelColor.second}]"
viewBinding.commentUserLevel.text = userColor
viewBinding.commentUserLevel.setTextColor(levelColor.first) viewBinding.commentUserLevel.setTextColor(levelColor.first)
viewBinding.commentUserTime.text = formatTimestamp(comment.timestamp) viewBinding.commentUserTime.text = formatTimestamp(comment.timestamp)
} }
@@ -228,12 +280,16 @@ class CommentItem(val comment: Comment,
} }
fun replying(isReplying: Boolean) { fun replying(isReplying: Boolean) {
binding.commentReply.text = if (isReplying) commentsFragment.activity.getString(R.string.cancel) else "Reply" binding.commentReply.text =
if (isReplying) commentsFragment.activity.getString(R.string.cancel) else "Reply"
this.isReplying = isReplying this.isReplying = isReplying
} }
fun editing(isEditing: Boolean) { fun editing(isEditing: Boolean) {
binding.commentEdit.text = if (isEditing) commentsFragment.activity.getString(R.string.cancel) else commentsFragment.activity.getString(R.string.edit) binding.commentEdit.text =
if (isEditing) commentsFragment.activity.getString(R.string.cancel) else commentsFragment.activity.getString(
R.string.edit
)
this.isEditing = isEditing this.isEditing = isEditing
} }
@@ -241,8 +297,9 @@ class CommentItem(val comment: Comment,
subCommentIds.add(id) subCommentIds.add(id)
} }
private fun removeSubCommentIds(){ private fun removeSubCommentIds() {
subCommentIds.forEach { id -> subCommentIds.forEach { id ->
@Suppress("UNCHECKED_CAST")
val parentComments = parentSection.groups as? List<CommentItem> ?: emptyList() val parentComments = parentSection.groups as? List<CommentItem> ?: emptyList()
val commentToRemove = parentComments.find { it.comment.commentId == id } val commentToRemove = parentComments.find { it.comment.commentId == id }
commentToRemove?.let { commentToRemove?.let {
@@ -260,11 +317,13 @@ class CommentItem(val comment: Comment,
viewBinding.commentUpVote.alpha = 1f viewBinding.commentUpVote.alpha = 1f
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24) viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
} }
-1 -> { -1 -> {
viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24) viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_active_24) viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_active_24)
viewBinding.commentDownVote.alpha = 1f viewBinding.commentDownVote.alpha = 1f
} }
else -> { else -> {
viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24) viewBinding.commentUpVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24) viewBinding.commentDownVote.setImageResource(R.drawable.ic_round_upvote_inactive_24)
@@ -272,9 +331,10 @@ class CommentItem(val comment: Comment,
} }
} }
@SuppressLint("SimpleDateFormat")
private fun formatTimestamp(timestamp: String): String { private fun formatTimestamp(timestamp: String): String {
return try { return try {
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT)
dateFormat.timeZone = TimeZone.getTimeZone("UTC") dateFormat.timeZone = TimeZone.getTimeZone("UTC")
val parsedDate = dateFormat.parse(timestamp) val parsedDate = dateFormat.parse(timestamp)
val currentDate = Date() val currentDate = Date()
@@ -297,8 +357,9 @@ class CommentItem(val comment: Comment,
} }
companion object { companion object {
@SuppressLint("SimpleDateFormat")
fun timestampToMillis(timestamp: String): Long { fun timestampToMillis(timestamp: String): Long {
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT)
dateFormat.timeZone = TimeZone.getTimeZone("UTC") dateFormat.timeZone = TimeZone.getTimeZone("UTC")
val parsedDate = dateFormat.parse(timestamp) val parsedDate = dateFormat.parse(timestamp)
return parsedDate?.time ?: 0 return parsedDate?.time ?: 0
@@ -307,7 +368,8 @@ class CommentItem(val comment: Comment,
private fun getAvatarColor(voteCount: Int, backgroundColor: Int): Pair<Int, Int> { 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 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] val colorString =
if (level > usernameColors.size - 1) usernameColors[usernameColors.size - 1] else usernameColors[level]
var color = Color.parseColor(colorString) var color = Color.parseColor(colorString)
val ratio = getContrastRatio(color, backgroundColor) val ratio = getContrastRatio(color, backgroundColor)
if (ratio < 4.5) { if (ratio < 4.5) {
@@ -325,16 +387,17 @@ class CommentItem(val comment: Comment,
* @param callback the callback to call when the user clicks yes * @param callback the callback to call when the user clicks yes
*/ */
private fun dialogBuilder(title: String, message: String, callback: () -> Unit) { private fun dialogBuilder(title: String, message: String, callback: () -> Unit) {
val alertDialog = android.app.AlertDialog.Builder(commentsFragment.activity, R.style.MyPopup) val alertDialog =
.setTitle(title) android.app.AlertDialog.Builder(commentsFragment.activity, R.style.MyPopup)
.setMessage(message) .setTitle(title)
.setPositiveButton("Yes") { dialog, _ -> .setMessage(message)
callback() .setPositiveButton("Yes") { dialog, _ ->
dialog.dismiss() callback()
} dialog.dismiss()
.setNegativeButton("No") { dialog, _ -> }
dialog.dismiss() .setNegativeButton("No") { dialog, _ ->
} dialog.dismiss()
}
val dialog = alertDialog.show() val dialog = alertDialog.show()
dialog?.window?.setDimAmount(0.8f) dialog?.window?.setDimAmount(0.8f)
} }

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