Compare commits

...

320 Commits

Author SHA1 Message Date
rebel onion
a316de3957 Update stable.md 2024-01-23 01:43:02 -06:00
rebel onion
c3f5a820e4 Merge pull request #155 from rebelonion/dev
Dev
2024-01-23 01:39:07 -06:00
rebelonion
daa5ec7bed extension updates toast 2024-01-23 01:36:38 -06:00
rebelonion
de91f1f3fa idk some fixes or smth 2024-01-23 01:23:47 -06:00
rebelonion
9c67a7e357 nice transition for offline mode 2024-01-22 22:51:51 -06:00
rebelonion
f70ce39fb7 some code cleanup 2024-01-22 22:39:01 -06:00
rebel onion
20ffe2273c Update README.md 2024-01-22 22:18:04 -06:00
aayush262
f333051073 Merge pull request #154 from aayush2622/dev
warning menu when deleting episode
2024-01-23 01:18:07 +05:30
Finnley Somdahl
a58f8fa76b random fix 2024-01-22 18:59:57 -06:00
aayush262
625c7d738b warning menu when deleting episode
auto hide desc if ep is downloaded(looked verybad when both was on)
2024-01-23 01:10:51 +05:30
aayush262
563e4f2cbe "Include list" switch changed (#153)
* "Include list" switch change

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

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

* incognito display in media

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

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

* fixed can uninstall after changing grid view

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

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

* added total ep released no

* padding fix

* fixed scroll to top coinciding with navbar

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

* fix

* Fix 1

* Update MainActivity.kt

* Update MainActivity.kt

---------

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

* Add files via upload

* Add files via upload

* use existing robust episode regex

* use existing robust episode regex

* use existing robust episode regex

* use existing robust episode regex

* allow external use of manga chapter regex as well

---------

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

* Removed UPI (rebel not indian)

* minor changes

* Shows number of manga/Ln downloaded

* fixed list name overlapping with notch

* wrong index selection in language fixed

* novel icon

* Emerald theme name changed to Ocean

* forgot to remove

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

* added more lang name

* changed alter dialog view

* sort language by names

* 3x grid for 360DP mobiles

* Default novel settings

* Oled for LN

* Lang full name

* Notification icon changed to dantotsu

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

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

* import fix

* alter dialog

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

Changed some strings

* More strings

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

* Update beta.yml

* Update strings.xml

* Update FAQActivity.kt

---------

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

* Downloaded manga page redesign(lol)

* quick fix

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

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

* undo
2023-12-19 16:12:15 -06:00
rebel onion
7ebb539bba Update beta.yml 2023-12-18 23:47:42 -06:00
rebel onion
d7c6d63d71 Update README.md 2023-12-18 23:42:11 -06:00
rebel onion
11d04ecb58 Merge pull request #86 from Sadwhy/main
Clean up switch
2023-12-18 17:17:36 -06:00
rebel onion
74328cf4cf Merge branch 'dev' into main 2023-12-18 17:17:24 -06:00
rebel onion
cfd59a6ba0 Update DisabledReports.kt 2023-12-18 17:15:57 -06:00
rebel onion
1779276154 Update beta.yml 2023-12-18 17:15:24 -06:00
rebel onion
dfc10d5520 Merge pull request #84 from aayush2622/dev
some changes
2023-12-14 18:06:11 -06:00
sadmansaif017@gmail.com
f090f6c630 Finalized 2023-12-13 14:49:53 +06:00
sadmansaif017@gmail.com
a13f98f6da Clean Switch 2023-12-13 14:48:37 +06:00
aayush262
cc98e2f307 correct??? 2023-12-12 15:30:31 +05:30
aayush262
5c4e9d7696 Better incognito (visually)
toggle for navbar in list
2023-12-11 21:39:23 +05:30
aayush262
b180625636 . 2023-12-11 02:45:21 +05:30
aayush262
31482674c0 yt icon 2023-12-10 23:47:06 +05:30
aayush262
c7bc6241dc some changes
better manga title/list name view
2023-12-10 23:42:56 +05:30
Sadwhy
86b74f022b Update build.gradle 2023-12-10 18:50:52 +06:00
Sadwhy
7336c73561 Update build.gradle 2023-12-10 18:49:22 +06:00
Sadwhy
528f70c6de Update beta.yml 2023-12-10 12:09:42 +06:00
Sadwhy
2c0d698ac9 Tracking denied 2023-12-10 11:29:58 +06:00
rebel onion
d404202371 linux no worky 2023-12-09 23:07:30 -06:00
rebel onion
ebeffa2135 fml 2023-12-09 22:59:08 -06:00
rebel onion
51015dc2f4 Update beta.yml 2023-12-09 22:56:37 -06:00
rebel onion
b840cdb695 Update beta.yml 2023-12-09 22:39:48 -06:00
rebel onion
e6cb10df19 Update beta.yml 2023-12-09 22:29:55 -06:00
rebel onion
1cd1b8af23 Update beta.yml 2023-12-09 22:20:40 -06:00
rebel onion
2b38869c41 signing? 2023-12-09 22:19:13 -06:00
Finnley Somdahl
6c310713d6 new color picker 2023-12-09 21:20:19 -06:00
Finnley Somdahl
0a2ecdd190 Update DisabledReports.kt 2023-12-09 14:40:58 -06:00
Sadwhy
3db4363100 Merge branch 'rebelonion:main' into main 2023-12-10 02:38:20 +06:00
Finnley Somdahl
713960e247 example png 2023-12-09 14:15:19 -06:00
Finnley Somdahl
b6be7075b0 widget outline 2023-12-09 14:09:24 -06:00
Finnley Somdahl
82bc215da5 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2023-12-09 14:04:32 -06:00
Finnley Somdahl
e8f3d5525d Update AndroidManifest.xml 2023-12-09 14:04:18 -06:00
rebel onion
d1cf8c4e10 Merge pull request #81 from rebelonion/main
update dev
2023-12-09 13:56:48 -06:00
Sadwhy
f19e112d0a Integrate GitHub runner and Discord for build updates (#79)
* Added a GitHub runner and uploads it to discord

* Update build.gradle

* Update build.gradle

* Update build.gradle

* Update build.gradle

* Add files via upload

* Add files via upload

* Add files via upload

* Delete app/src/debug/google-services.json

* Delete build.gradle

* target dev

* Add files via upload

---------

Co-authored-by: rebel onion <87634197+rebelonion@users.noreply.github.com>
2023-12-09 13:51:27 -06:00
Sadwhy
9eb29361dc Add files via upload 2023-12-10 01:49:06 +06:00
rebel onion
133959a34e target dev 2023-12-09 13:47:35 -06:00
Sadwhy
bd48ff05eb Delete build.gradle 2023-12-10 01:46:45 +06:00
rebel onion
1d2ce6ccaa Delete app/src/debug/google-services.json 2023-12-09 13:38:38 -06:00
rebel onion
3a3857e9eb Merge pull request #80 from rebelonion/dev
Dev
2023-12-09 13:36:31 -06:00
Finnley Somdahl
38c4440d45 google services 2023-12-09 13:35:51 -06:00
Sadwhy
85f03ece85 Add files via upload 2023-12-10 01:24:03 +06:00
Sadwhy
e2f02dc93c Add files via upload 2023-12-10 01:22:01 +06:00
Sadwhy
88c4d1f8a7 Add files via upload 2023-12-10 01:15:42 +06:00
Sadwhy
2c24a56446 Update build.gradle 2023-12-10 01:09:21 +06:00
Sadwhy
d11b370415 Update build.gradle 2023-12-10 01:04:43 +06:00
Sadwhy
f81c566f12 Update build.gradle 2023-12-10 00:38:59 +06:00
Sadwhy
9a4ed7ad54 Update build.gradle 2023-12-10 00:36:52 +06:00
Sadwhy
07793b11d6 Added a GitHub runner and uploads it to discord 2023-12-10 00:26:28 +06:00
Finnley Somdahl
aad3c3fed3 ic_delete reset rotation 2023-12-06 21:25:33 -06:00
Finnley Somdahl
f79bd9194a internal version 2023-12-06 21:23:09 -06:00
Finnley Somdahl
5ad68f2bd2 lower zoom 2023-12-06 21:22:54 -06:00
Finnley Somdahl
f463275a73 Custom AlertDialog fix 2023-12-06 21:22:45 -06:00
Finnley Somdahl
ac6b22f659 progressChapterIndex IndexOutOfBoundsException fix 2023-12-06 19:34:22 -06:00
Finnley Somdahl
ac9d3a2363 fix for Unable to destroy activity MediaDetailsActivity 2023-12-06 19:32:14 -06:00
Finnley Somdahl
38a27c45a1 empty check on mediaList 2023-12-06 19:27:34 -06:00
Finnley Somdahl
33bfbd65fb toast fix 2023-12-06 19:25:00 -06:00
Finnley Somdahl
ac98417355 out of bounds on 25 fix 2023-12-06 19:21:53 -06:00
Finnley Somdahl
876304065d fix for SettingsDialogFragment.<init> [] 2023-12-06 19:19:14 -06:00
Finnley Somdahl
9fc80d6397 beta version update 2023-12-06 00:44:08 -06:00
rebel onion
8797af0cbc Merge pull request #74 from rebelonion/dev
Dev
2023-12-06 00:35:31 -06:00
Finnley Somdahl
97a4cba680 v2 2023-12-06 00:31:59 -06:00
Finnley Somdahl
acc5069c83 why was this here? 2023-12-06 00:30:43 -06:00
Finnley Somdahl
fab978dba4 multi fix + etc 2023-12-05 23:43:34 -06:00
Finnley Somdahl
ad1734d640 only one dns 2023-12-05 22:40:27 -06:00
rebel onion
d687911c85 Create FUNDING.yml 2023-12-05 22:10:18 -06:00
rebelonion
1d4257b1b3 Merge pull request #73 from rebelonion/dev
Dev
2023-12-05 21:34:08 -06:00
Finnley Somdahl
55521ab9fc downloading from specific point 2023-12-05 21:20:00 -06:00
Finnley Somdahl
17e53a54af Aayush stuffs 2023-12-05 21:04:06 -06:00
Finnley Somdahl
dc1edc9a42 dimming 2023-12-05 20:51:22 -06:00
Finnley Somdahl
b8782b0507 multi download 2023-12-05 20:35:25 -06:00
Finnley Somdahl
0d422a57e7 styling fix + pos fix 2023-12-05 19:51:45 -06:00
Finnley Somdahl
1bbc98d350 downloads cleaner 2023-12-05 15:38:29 -06:00
Finnley Somdahl
7ae6831628 some downloading fixes 2023-12-05 15:38:01 -06:00
Finnley Somdahl
65e89398d9 orientation test fix 2023-12-05 05:41:37 -06:00
Finnley Somdahl
2b77b7578c reset watch position if nearly done with episode 2023-12-05 02:56:19 -06:00
Finnley Somdahl
e77ab2800a incognito 2023-12-05 02:39:59 -06:00
Finnley Somdahl
c1a0eeb361 fix manga chapter number tile display 2023-12-05 02:39:44 -06:00
Finnley Somdahl
393ab1e513 always round progress down 2023-12-05 01:11:56 -06:00
Finnley Somdahl
e26a6c647f random option 2023-12-05 01:05:36 -06:00
Finnley Somdahl
ea83b722a6 no presence when not signed in 2023-12-05 00:25:37 -06:00
Finnley Somdahl
34a3e9e5a3 change swipe sensitivity 2023-12-04 23:51:57 -06:00
rebelonion
7f92ac686d Merge pull request #69 from rebelonion/dev
Dev
2023-12-04 22:19:26 -06:00
Finnley Somdahl
b6c79dae40 Aayush's pr stuffs 2023-12-04 22:16:05 -06:00
Finnley Somdahl
8c957007ab rpc fix 2023-12-04 22:15:48 -06:00
Finnley Somdahl
c728eae2ba more searching 2023-12-04 00:29:33 -06:00
Finnley Somdahl
3ded6ba87a offline novel 2023-12-03 22:18:06 -06:00
Finnley Somdahl
111fb16266 webview version check 2023-12-02 21:05:06 -06:00
Finnley Somdahl
121be4bc6f signing fix 2023-12-02 18:34:40 -06:00
Finnley Somdahl
afa960c808 reformat 2023-12-01 01:22:15 -06:00
Finnley Somdahl
1df528c0dc Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2023-12-01 00:58:59 -06:00
Finnley Somdahl
f792296f78 offline manga fix 2023-12-01 00:58:58 -06:00
Wai What
d512929387 Small color fix for Saikou theme (#65)
* Update colors.xml

Updated Saikou theme

* Update colors.xml

Changed background color in accordance with Aayushi262

* Update colors.xml

* Update colors.xml

Fixed double space
2023-11-30 03:48:47 -06:00
Finnley Somdahl
c7bc1ffe9e Light novel support 2023-11-30 03:41:45 -06:00
Finnley Somdahl
32f918450a AMOLED + custom 2023-11-26 22:05:27 -06:00
Finnley Somdahl
f01377f0b1 OLED fix 2023-11-26 21:33:22 -06:00
Finnley Somdahl
c2a07278fc Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2023-11-26 21:16:02 -06:00
Finnley Somdahl
0a17bed243 hotfix for custom_theme 2023-11-26 21:15:54 -06:00
aayush262
c5ed8acfa3 small fixes (#61)
* quickfix

* android locale tuning

* toggle option to setting

* some customizations

* some fixes

* some fixes

* some fixes

* fixed header of search by image

---------

Co-authored-by: rebelonion <87634197+rebelonion@users.noreply.github.com>
2023-11-26 21:14:51 -06:00
Finnley Somdahl
e5f2bb6566 new theme options 2023-11-26 21:00:46 -06:00
Finnley Somdahl
b4093b0c47 extension fix 2023-11-26 16:38:13 -06:00
Finnley Somdahl
d0fd62abf2 Allow AMOLED with Monet 2023-11-26 15:39:41 -06:00
Finnley Somdahl
4d0c3e5849 set page search to index at one 2023-11-26 03:13:16 -06:00
Finnley Somdahl
d131562f34 cleanup 2023-11-26 02:46:36 -06:00
Finnley Somdahl
cf2d9ad654 rpc fix 2023-11-26 02:36:27 -06:00
aayush262
af326c8258 some customizations (#59)
* quickfix

* android locale tuning

* toggle option to setting

* some customizations

---------

Co-authored-by: rebelonion <87634197+rebelonion@users.noreply.github.com>
2023-11-24 01:28:44 -06:00
Finnley Somdahl
ba351df331 odd extension search error fix 2023-11-23 00:12:09 -06:00
rebelonion
d4c2df37ae dev update (#57)
* quickfix

* android locale tuning

* toggle option to setting
2023-11-22 23:43:18 -06:00
rebelonion
79d1c44e63 Merge branch 'dev' into main 2023-11-22 23:43:06 -06:00
Finnley Somdahl
38faedb4b5 rpc fix and api 34 fix 2023-11-22 23:38:22 -06:00
Finnley Somdahl
39b0f28127 move user agent to popup 2023-11-22 16:01:41 -06:00
Finnley Somdahl
a1913ed968 new icon color fixed sorting issue [12:28 AM] some nice gui stuff for downloading [12:28 AM] yomiroll preferences bug [12:28 AM] background no longer stuck on black 2023-11-22 00:32:26 -06:00
Finnley Somdahl
f7917df907 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2023-11-21 20:34:52 -06:00
Finnley Somdahl
84c58fbe6c downloading button cleanup 2023-11-21 20:34:34 -06:00
aayush262
75895d851f Language add to extensions (#52)
* got rid of both companion object

* minor changes

* It Now show lang on extensions

* fixed

* quickfix

* android locale tuning

* toggle option to setting

* some fixes
added 2 fonts
removed Scanlators for anime

---------

Co-authored-by: rebelonion <87634197+rebelonion@users.noreply.github.com>
2023-11-21 20:33:17 -06:00
Finnley Somdahl
6d05a42168 toggle option to setting 2023-11-21 04:13:05 -06:00
Finnley Somdahl
594fa4daa9 android locale tuning 2023-11-21 04:08:45 -06:00
Finnley Somdahl
8d9254140d quickfix 2023-11-21 03:01:11 -06:00
rebelonion
533aa9f56e Merge pull request #54 from rebelonion/dev
Dev
2023-11-21 02:52:46 -06:00
rebelonion
c310bea0e9 Merge branch 'main' into dev 2023-11-21 02:52:31 -06:00
Finnley Somdahl
813f7a0992 Update AniyomiAdapter.kt 2023-11-21 02:49:52 -06:00
Finnley Somdahl
4c0f56d3e3 Squashed commit of the following:
commit 187262a266
Author: Finnley Somdahl <87634197+rebelonion@users.noreply.github.com>
Date:   Mon Nov 20 23:38:24 2023 -0600

    work
2023-11-21 02:46:56 -06:00
Finnley Somdahl
1f44d32f35 :bocchi_overload_animated: 2023-11-21 02:38:18 -06:00
Finnley Somdahl
d937f447ef work(ing) 2023-11-20 23:51:59 -06:00
Finnley Somdahl
187262a266 work 2023-11-20 23:38:24 -06:00
Finnley Somdahl
f40ebc9d09 on second thought 2023-11-20 19:53:09 -06:00
Finnley Somdahl
3998d88297 oh no 2023-11-20 19:45:33 -06:00
Finnley Somdahl
4db301ca7a more offline stuff/bugfixes 2023-11-20 00:39:14 -06:00
Finnley Somdahl
3dfcc9fc31 Merge branch 'main' of https://github.com/rebelonion/Dantotsu 2023-11-19 20:49:12 -06:00
Asvin Ragunathan
df63586c02 Updated Support For Android 14 (API 34) (WIP) (#47)
* Updated Support For Android 14 (API 34)

* Updated To Not Require Greater than API 26

* Fixed Github Stash Issue With Commit
2023-11-19 20:48:59 -06:00
Finnley Somdahl
d7372d4dbb move CoroutineScopes for clarity 2023-11-19 20:47:43 -06:00
aayush262
f4266d0da3 got rid of both companion object (#50) 2023-11-18 00:33:09 -06:00
aayush262
736b06bdbe Added a option to toggle fast forward / Added NSFW extension toggle to extension settings (#48)
* Remove 18+ extension if Anilist 18+ is off
 ~requested by @arif

* Translation filter for extension(WIP)

* Added a option to toggle fast forward
suggested by arif

* Added NFSW toggle to extension settings
now it will be more easy rather then going to anilist to toggle it
 ~suggested by arif

* Forgot to undo this

* changed icons in extension setting

* get rid of companion object (todo)

* get rid of companion object (todo)

---------

Co-authored-by: rebelonion <87634197+rebelonion@users.noreply.github.com>
2023-11-17 15:10:58 -06:00
aayush262
5543d29317 Better extension page(maybe) (#45)
* Fixed Mono icon not loading/Extension page customization

* Better extension page(maybe)
2023-11-14 19:41:07 -06:00
aayush262
2fc351f57a Fixed Mono icon not loading/Extension page customization (#44) 2023-11-12 23:08:14 -06:00
Wai What
eee1242964 Added Saikou theme (#40)
* Update colors.xml

* Update themes.xml

* Update themes.xml

* Update ThemeManager.kt

* Update ThemeManager.kt

* Update ThemeManager.kt

* Update ThemeManager.kt

* Update DevelopersDialogFragment.kt

* Update activity_main.xml

* Update item_anime_page.xml

* Update item_manga_page.xml

* Update fragment_login.xml

* Update activity_media.xml

* Update activity_media.xml

* Update item_anime_page.xml

* Update item_manga_page.xml

* Update themes.xml

* Update themes.xml

* Update exo_player_control_view.xml

* Update activity_author.xml

* Update activity_studio.xml

* Update activity_manga_reader.xml

* Update activity_novel_reader.xml

* Update activity_media.xml

Fix

* Update tab_layout_icon.xml

* Update activity_media.xml

* Update activity_media.xml

* Update tab_layout_icon.xml

Changed selected layout icon from primary to secondary

* Update activity_list.xml

* Update ListActivity.kt

Unbound listTabLayout, listAppBar and listTitle because it stopped color reallocation

* Update CalendarActivity.kt

Unbound listTabLayout, listAppBar and listTitle because it stopped color reallocation

* Update button_switch_track.xml

* Update CalendarActivity.kt

Undo

* Update ListActivity.kt

Undo

* Update CalendarActivity.kt

* Update ListActivity.kt

* Update ListActivity.kt

* Update CalendarActivity.kt (Saikou theme complete!)

I'll just need to check for bugs and request to merge

* Update ThemeManager.kt

Took Sakiou theme out of beta

* Update tab_layout_icon.xml

Changes to media tabs (less accurate to Saikou but selected menu is more vibrant and supports other themes better)

* Update activity_media.xml

Changes to media tabs (less accurate to Saikou but selected menu is more vibrant and supports other themes better)

* Update activity_media.xml

Changes to media tabs (less accurate to Saikou but selected menu is more vibrant and supports other themes better)

* Update control_background_40dp.xml

* Update build.gradle

Changed version number
2023-11-12 12:55:16 -06:00
rebelonion
a58e9a523a Update README.md 2023-11-07 11:00:37 -06:00
Finnley Somdahl
cd3aad1c33 double error and crash fix 2023-11-05 02:40:40 -06:00
Finnley Somdahl
5a482d8307 Merge branch 'main' of https://github.com/rebelonion/Dantotsu 2023-11-05 02:17:57 -06:00
Finnley Somdahl
91d869005c basic offline manga fragment 2023-11-05 02:17:49 -06:00
rebelonion
05e73269d3 Merge pull request #35 from aayush2622/main
Changed "skip loading extension icons" icon
2023-11-05 01:33:10 -06:00
aayush262
3dac48ced8 Changed "skip loading extension icons" icon 2023-11-05 12:45:38 +05:30
rebelonion
8c5726ab8a Merge pull request #31 from elucubro/zero-mb-displaying
when video size is 0mb, display unknown size instead
2023-11-04 13:39:58 -05:00
rebelonion
076516be23 Merge pull request #27 from aayush2622/main
Added some screenshots
2023-11-04 02:13:08 -05:00
elucubro
1059a3c17e when video size is 0mb, display unknown size instead 2023-11-04 15:12:09 +08:00
Finnley Somdahl
c75df942f2 beta updater fix 2023-11-04 02:05:21 -05:00
rebelonion
cfe7be5cdb Merge pull request #30 from elucubro/double-tap-zoom
making double tap zoom lower in manga reader
2023-11-04 02:04:23 -05:00
Finnley Somdahl
390fc18c4c queueing system for manga downloads 2023-11-04 01:40:40 -05:00
elucubro
4c82c56828 making double tap zoom lower in manga reader 2023-11-04 13:13:23 +08:00
aayush262
da5c480ba7 added website link 2023-11-03 20:07:36 +05:30
Finnley Somdahl
20acd71b1a parent acb0225699
author Finnley Somdahl <87634197+rebelonion@users.noreply.github.com> 1698992132 -0500
committer Finnley Somdahl <87634197+rebelonion@users.noreply.github.com> 1698992691 -0500

manga downloading base

Update README.md

Update README.md

Update README.md

Update README.md

Update README.md
2023-11-03 01:29:24 -05:00
aayush262
231c9c5b98 fixed typo 2023-11-02 21:34:00 +05:30
aayush262
4cfdcdb23c Added some screenshots 2023-11-01 21:46:43 +05:30
Finnley Somdahl
acb0225699 Merge pull request #24 from aayush2622/main
little customizations on readme page
2023-10-31 12:03:30 -05:00
aayush262
ebffaaa742 a lil better README.md 2023-10-31 22:21:28 +05:30
aayush262
1760064555 Update README.md 2023-10-31 17:58:41 +05:30
aayush262
44a6db3fc2 Update README.md 2023-10-31 17:52:27 +05:30
aayush262
aab25d157e Update README.md 2023-10-31 17:51:19 +05:30
aayush262
8a4be86ddc Update README.md 2023-10-31 17:50:00 +05:30
aayush262
31baf729be Update README.md 2023-10-31 17:48:29 +05:30
aayush262
b98e3dc780 Update README.md 2023-10-31 17:41:50 +05:30
aayush262
878d58679e Update README.md 2023-10-31 17:34:50 +05:30
aayush262
f500ba6cf0 Update README.md 2023-10-31 17:25:30 +05:30
aayush262
d124736556 Update README.md 2023-10-31 17:24:15 +05:30
aayush262
1a825e2509 Update README.md 2023-10-31 17:19:59 +05:30
Finnley Somdahl
6e14c2221d Merge branch 'main' of https://github.com/rebelonion/Dantotsu 2023-10-31 01:03:00 -05:00
Finnley Somdahl
5b6e351a56 extension fixes 2023-10-31 01:02:54 -05:00
Finnley Somdahl
c310708401 Merge pull request #23 from Sadwhy/patch-1
Update README.md
2023-10-30 22:03:01 -05:00
Sadwhy
f0093b903a Update README.md
Just made some titles uppercase. Consistency
2023-10-31 09:00:47 +06:00
Finnley Somdahl
d33568f0ad icon and logging 2023-10-29 22:37:28 -05:00
Finnley Somdahl
26f9f40042 ui tweaks 2023-10-29 20:33:13 -05:00
Finnley Somdahl
7545870f38 ui tweaks 2023-10-29 20:06:16 -05:00
Finnley Somdahl
3368a1bc8d extension settings 2023-10-29 19:45:11 -05:00
Finnley Somdahl
9c0ef7a788 bugfixes and themes 2023-10-27 00:47:44 -05:00
Finnley Somdahl
960c2b4113 Update stable.md 2023-10-26 02:11:43 -05:00
Finnley Somdahl
1eb85d4419 new themes 2023-10-26 02:08:35 -05:00
Finnley Somdahl
20bea76e6c subtitle, image, and extension page fixes 2023-10-26 00:40:57 -05:00
Finnley Somdahl
866bd3b3a9 theme cleanup 2023-10-25 15:55:29 -05:00
Finnley Somdahl
3567b8dced hotfix 2023-10-25 01:22:40 -05:00
Finnley Somdahl
d109914537 Update stable.md 2023-10-25 00:36:37 -05:00
Finnley Somdahl
da4d55a9a8 final fixes before update 2023-10-25 00:35:09 -05:00
Finnley Somdahl
63526c6ed3 themes and various bugs 2023-10-24 23:38:46 -05:00
445 changed files with 24407 additions and 5225 deletions

13
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
# These are supported funding model platforms
github: [rebelonion]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: ['https://www.buymeacoffee.com/rebelonion']

67
.github/workflows/beta.yml vendored Normal file
View File

@@ -0,0 +1,67 @@
name: Build APK and Notify Discord
on:
push:
branches:
- dev
paths-ignore:
- '**/README.md'
jobs:
build:
runs-on: ubuntu-latest
env:
CI: true
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Set variables
run: |
VER=$(grep -E -o "versionName \".*\"" app/build.gradle | sed -e 's/versionName //g' | tr -d '"')
SHA=${{ github.sha }}
VERSION="$VER.${SHA:0:7}"
echo "Version $VERSION"
echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: Setup JDK 17
uses: actions/setup-java@v3
with:
distribution: 'temurin'
java-version: 17
cache: gradle
- name: Decode Keystore File
run: echo "${{ secrets.KEYSTORE_FILE }}" | base64 -d > $GITHUB_WORKSPACE/key.keystore
- name: List files in the directory
run: ls -l
- name: Make gradlew executable
run: chmod +x ./gradlew
- name: Build with Gradle
run: ./gradlew assembleDebug -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
uses: actions/upload-artifact@v3.0.0
with:
name: Dantotsu
path: "app/build/outputs/apk/debug/app-debug.apk"
- name: Upload APK to Discord
shell: bash
run: |
contentbody=$( jq -Rsa . <<< "${{ github.event.head_commit.message }}" )
curl -F "payload_json={\"content\":\" Debug-Build: <@719439449423085569> **${{ env.VERSION }}**\n\n${contentbody:1:-1}\"}" -F "dantotsu_debug=@app/build/outputs/apk/debug/app-debug.apk" ${{ secrets.DISCORD_WEBHOOK }}
- name: Delete Old Pre-Releases
id: delete-pre-releases
uses: sgpublic/delete-release-action@master
with:
pre-release-drop: true
pre-release-keep-count: 3
pre-release-drop-tag: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

6
.gitignore vendored
View File

@@ -23,8 +23,8 @@ output.json
*.jks *.jks
*.keystore *.keystore
# Google Services (e.g. APIs or Firebase)
google-services.json
# Android Profiling # Android Profiling
*.hprof *.hprof
#other
scripts/

110
README.md
View File

@@ -1,109 +1,41 @@
# **Dantotsu** (🚧 ALPHA 🚧)
> ⚠️ **WARNING**: This project is in alpha stage. Things may not work as expected.
<p align="center"> <p align="center">
<a href="https://discord.gg/4HPZ5nAWwM"><img src="https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white"></a> <img src="https://pbxt.replicate.delivery/2PX94viD6lJSDVayQrGyDH7CGu7IjQ6e8HEtOGDeelefXRdOC/out.png" alt="Dantotsu Banner" width=100% >
</p>
<p align="center">
<img src="https://img.shields.io/badge/platforms-android-blueviolet?style=for-the-badge"/>
<a href="https://github.com/rebelonion/Dantotsu/releases"><img src="https://img.shields.io/github/downloads/rebelonion/Dantotsu/total?color=%233DDC84&logo=android&logoColor=%23fff&style=for-the-badge"></a> <a href="https://github.com/rebelonion/Dantotsu/releases"><img src="https://img.shields.io/github/downloads/rebelonion/Dantotsu/total?color=%233DDC84&logo=android&logoColor=%23fff&style=for-the-badge"></a>
<a href="https://www.codefactor.io/repository/github/rebelonion/dantotsu"><img src="https://www.codefactor.io/repository/github/rebelonion/dantotsu/badge?color=%233DDC84&logo=android&logoColor=%23fff&style=for-the-badge" alt="CodeFactor" /></a>
<a href="https://discord.gg/4HPZ5nAWwM"><img src="https://img.shields.io/discord/358599430502481920.svg?style=for-the-badge&logo=discord&colorB=7289DA"></a>
</p> </p>
Dantotsu is crafted from the ashes of Saikou and based on simplistic yet state-of-the-art elegance. It is an <a href="https://anilist.co/">Anilist</a> only client, which also lets you stream-download Anime & Manga through extensions.
<br><br>
<i>Dantotsu (断トツ; Dan-totsu) literally means the best of the best in Japanese. Well, we would like to say this is the best open source app for anime and manga on Android, but hey, try it out yourself & judge!
</i>
<br>
<br>
<a href="https://www.buymeacoffee.com/rebelonion"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=rebelonion&button_colour=FFDD00&font_colour=000000&font_family=Poppins&outline_colour=000000&coffee_colour=ffffff" /></a>
<br>
### 🌟STAR THIS REPOSITORY TO SUPPORT THE DEVELOPER AND ENCOURAGE THE DEVELOPMENT OF THE APPLICATION! # **Dantotsu** 🌟
> **Warning** Dantotsu is an [Anilist](https://anilist.co/) only client.
>
> Please do not attempt to upload Dantotsu or any of it's forks on Playstore or any other Android appstores on the internet. Doing so, may infringe their terms and conditions. This may result to legal action or immediate take-down of the app.
## Extension Status > **Dantotsu (断トツ; Dan-totsu)** literally means "the best of the best" in Japanese. Try it out for yourself and be the judge!
| Type | Status | <a href="https://www.buymeacoffee.com/rebelonion"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=rebelonion&button_colour=FFDD00&font_colour=030201&font_family=Poppins&outline_colour=000000&coffee_colour=ffffff" /></a>
| ---------------- | ------- |
| Anime Extensions | Working |
| Manga Extensions | "Working" |
| Light Novel Extensions | Not Working |
### 🚀 STAR THIS REPOSITORY TO SUPPORT THE DEVELOPER AND ENCOURAGE THE DEVELOPMENT OF THE APPLICATION!
## WANT TO CONTRIBUTE? 🤝
## APP FEATURES All contributions are welcome, from code to documentation to graphics to design suggestions to bug reports. Please use GitHub to its fullest; contribute Pull Requests, contribute tutorials or other content - whatever you have to offer, we can use!
- Easy and functional way to both, watch anime and read manga, ad-free. You can come hang out with our awesome community, request new features, and report any bugs or issues at our Discord server too. 📣
- A completely open source app with a nice UI & Animations :) ### OFFICIAL DISCORD SERVER 🚀
- Aniyomi extension support built right into the app.
- Synchronize anime and manga real-time with AniList and MyAnimeList. Easily categorise anime and manga based on your current status. (Powered by AniList)
- Find all shows using thoroughly and frequently updated list of all trending, popular and ongoing anime based on scores.
- View extensive details about anime shows, movies and manga titles. It also features ability to countdown to the next episode of airing anime. (Powered by AniList & MyAnimeList)
- Get notified when new episodes/chapters are released!
* **Available Anime sources:-**
NONE BUILT IN!
add your own extensions in the settings menu (Dantotsu has no affiliation with any of the extensions)
* **Available Manga sources:-**
NONE BUILT IN!
add your own extensions in the settings menu (Dantotsu has no affiliation with any of the extensions)
## Planned Stuff
- get app out of alpha
- Accent Color Change (RIP Hot Pink Supremacy.)
## Rejected Stuff (still rejected)
- Sources of any language except English
- News Section in the App
- Comment Section
## WANT TO CONTRIBUTE?
- All contributions are welcome, from code to documentation to graphics to design suggestions to bug reports. Please use GitHub to its fullest; contribute Pull Requests, contribute tutorials or other content- whatever you have to offer, we can use it!
- You can come hang out with our awesome community and request new features and report any bugs or issue at our discord server too.
### Official Discord Server
<p align="center"> <p align="center">
<a href="https://discord.gg/4HPZ5nAWwM"><img src="https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white"></a> <a href="https://discord.gg/4HPZ5nAWwM">
<img src="https://invidget.switchblade.xyz/4HPZ5nAWwM">
</a>
</p> </p>
## VISITORS
### VISIT FOR MORE INFORMATION:- <img src="https://count.getloli.com/get/@:rebeloniondantotsu" alt=":rebeloniondantotsu" />
no website yet :( ## LICENSE 📜
## DISCLAIMER
* Dantotsu by itself only provides an anime and manga tracker and does not provide any anime or manga streaming or downloading capabilities.
* Dantotsu or any of its developer/staff don't host any of the content found inside Dantotsu. Any and all images and anime/manga information found in the app are taken from various public APIs (AniList, MyAnimeList, Kitsu).
* Furthermore, all of the anime/manga links found in Dantotsu are taken from various 3rd party plugins and have no affiliation with Dantotsu or its staff.
* Dantotsu or it's owners aren't liable for any misuse of any of the contents found inside or outside of the app and cannot be held accountable for the distribution of any of the contents found inside the app.
* By using Dantotsu, you comply to the fact that the developer of the app is not responsible for any of the contents found in the app. You also agree to the fact that you may not use Dantotsu to download or stream any copyrighted content.
* If the internet infringement issues are involved, please contact the source website. The developer does not assume any legal responsibility.
## License
Dantotsu is licensed under the [GNU General Public License v3.0](LICENSE.md) Dantotsu is licensed under the [GNU General Public License v3.0](LICENSE.md)

View File

@@ -21,19 +21,20 @@ android {
minSdk 23 minSdk 23
targetSdk 34 targetSdk 34
versionCode ((System.currentTimeMillis() / 60000).toInteger()) versionCode ((System.currentTimeMillis() / 60000).toInteger())
versionName "0.1.2" versionName "2.0.0-beta01-iv1"
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
} }
buildTypes { buildTypes {
debug { debug {
//applicationIdSuffix ".beta" applicationIdSuffix ".beta"
debuggable true manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_beta", icon_placeholder_round: "@mipmap/ic_launcher_beta_round"]
versionNameSuffix "." + gitCommitHash debuggable System.getenv("CI") == null
} }
release { release {
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher", icon_placeholder_round: "@mipmap/ic_launcher_round"]
debuggable false debuggable false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-gson.pro', 'proguard-rules.pro'
} }
} }
buildFeatures { buildFeatures {
@@ -53,18 +54,20 @@ android {
dependencies { dependencies {
// Core // Core
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.browser:browser:1.6.0' implementation 'androidx.browser:browser:1.7.0'
implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.fragment:fragment-ktx:1.6.1' implementation 'androidx.fragment:fragment-ktx:1.6.2'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.multidex:multidex:2.0.1'
implementation "androidx.work:work-runtime-ktx:2.8.1" implementation "androidx.work:work-runtime-ktx:2.9.0"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.google.code.gson:gson:2.10'
implementation 'com.github.Blatzar:NiceHttp:0.4.3' implementation 'com.github.Blatzar:NiceHttp:0.4.4'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.webkit:webkit:1.9.0'
// Glide // Glide
ext.glide_version = '4.16.0' ext.glide_version = '4.16.0'
@@ -76,26 +79,31 @@ dependencies {
// FireBase // FireBase
implementation platform('com.google.firebase:firebase-bom:32.2.3') implementation platform('com.google.firebase:firebase-bom:32.2.3')
implementation 'com.google.firebase:firebase-analytics-ktx:21.3.0' implementation 'com.google.firebase:firebase-analytics-ktx:21.5.0'
implementation 'com.google.firebase:firebase-crashlytics-ktx:18.4.3' implementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.0'
// Exoplayer // Exoplayer
ext.exo_version = '1.1.1' ext.exo_version = '1.2.0'
implementation "androidx.media3:media3-exoplayer:$exo_version" implementation "androidx.media3:media3-exoplayer:$exo_version"
implementation "androidx.media3:media3-ui:$exo_version" implementation "androidx.media3:media3-ui:$exo_version"
implementation "androidx.media3:media3-exoplayer-hls:$exo_version" implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
implementation "androidx.media3:media3-exoplayer-dash:$exo_version" implementation "androidx.media3:media3-exoplayer-dash:$exo_version"
implementation "androidx.media3:media3-datasource-okhttp:$exo_version" implementation "androidx.media3:media3-datasource-okhttp:$exo_version"
implementation "androidx.media3:media3-session:$exo_version" implementation "androidx.media3:media3-session:$exo_version"
//media3 casting
implementation "androidx.media3:media3-cast:$exo_version"
implementation "androidx.mediarouter:mediarouter:1.6.0"
// UI // UI
implementation 'com.google.android.material:material:1.10.0' implementation 'com.google.android.material:material:1.11.0'
implementation 'nl.joery.animatedbottombar:library:1.1.0' implementation 'nl.joery.animatedbottombar:library:1.1.0'
implementation 'io.noties.markwon:core:4.6.2' implementation 'io.noties.markwon:core:4.6.2'
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 'com.github.eltos:simpledialogfragments:v3.7'
// string matching // string matching
implementation 'me.xdrop:fuzzywuzzy:1.4.0' implementation 'me.xdrop:fuzzywuzzy:1.4.0'
@@ -110,13 +118,14 @@ dependencies {
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11' implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11'
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11' implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps' implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'
implementation 'com.squareup.okio:okio:3.3.0' implementation 'com.squareup.okio:okio:3.7.0'
implementation 'ch.acra:acra-http:5.9.7' implementation 'ch.acra:acra-http:5.11.3'
implementation 'org.jsoup:jsoup:1.15.4' implementation 'org.jsoup:jsoup:1.15.4'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.5.0' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.2'
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0' implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
implementation 'com.github.tachiyomiorg:unifile:17bec43' implementation 'com.github.tachiyomiorg:unifile:17bec43'
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1' implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'app.cash.quickjs:quickjs-android:0.9.2'
} }

67
app/google-services.json Normal file
View File

@@ -0,0 +1,67 @@
{
"project_info": {
"project_number": "1039200814590",
"project_id": "dantotsu-1e50f",
"storage_bucket": "dantotsu-1e50f.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:1039200814590:android:c372b8c1b92b825f1aacaf",
"android_client_info": {
"package_name": "ani.Dantotsu"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyCiXo_q4S2ofA5oCztsoLnlDqJi3GtTJjY"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:1039200814590:android:40e14720ee97917e1aacaf",
"android_client_info": {
"package_name": "ani.dantotsu.beta"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyCiXo_q4S2ofA5oCztsoLnlDqJi3GtTJjY"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:1039200814590:android:40e14720ee97917e1aacaf",
"android_client_info": {
"package_name": "ani.dantotsu"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyCiXo_q4S2ofA5oCztsoLnlDqJi3GtTJjY"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

View File

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

View File

@@ -10,6 +10,8 @@
android:required="false" /> android:required="false" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"
tools:ignore="LeanbackUsesWifi" />
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
@@ -17,23 +19,22 @@
<uses-permission <uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" /> android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" <uses-permission
android:maxSdkVersion="32" /> android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" /> <!-- For background jobs -->
<!-- For background jobs -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> <!-- For managing extensions -->
<!-- For managing extensions -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" /> <!-- To view extension packages in API 30+ -->
<!-- To view extension packages in API 30+ --> <uses-permission
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" /> tools:ignore="QueryAllPackagesPermission" />
<uses-permission
<uses-permission android:name="android.permission.READ_APP_SPECIFIC_LOCALES" android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
tools:ignore="ProtectedPermissions" /> tools:ignore="ProtectedPermissions" />
<queries> <queries>
@@ -46,22 +47,37 @@
<application <application
android:name=".App" android:name=".App"
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:banner="@mipmap/ic_banner_foreground"
android:icon="${icon_placeholder}"
android:label="@string/app_name" android:label="@string/app_name"
android:largeHeap="true" android:largeHeap="true"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="${icon_placeholder_round}"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Dantotsu" android:theme="@style/Theme.Dantotsu"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:ignore="AllowBackup" tools:ignore="AllowBackup">
android:banner="@drawable/ic_banner_foreground"> <receiver
android:name=".widgets.CurrentlyAiringWidget"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/currently_airing_widget_info" />
</receiver>
<receiver android:name=".subcriptions.NotificationClickReceiver" />
<activity <activity
android:name="ani.dantotsu.media.novel.novelreader.NovelReaderActivity" android:name=".media.novel.novelreader.NovelReaderActivity"
android:configChanges="orientation|screenSize" android:configChanges="orientation|screenSize"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/epub+zip" /> <data android:mimeType="application/epub+zip" />
@@ -69,13 +85,11 @@
<data android:mimeType="application/vnd.amazon.ebook" /> <data android:mimeType="application/vnd.amazon.ebook" />
<data android:mimeType="application/fb2+zip" /> <data android:mimeType="application/fb2+zip" />
<data android:mimeType="application/vnd.comicbook+zip" /> <data android:mimeType="application/vnd.comicbook+zip" />
<data android:pathPattern=".*\\.epub" /> <data android:pathPattern=".*\\.epub" />
<data android:pathPattern=".*\\.mobi" /> <data android:pathPattern=".*\\.mobi" />
<data android:pathPattern=".*\\.kf8" /> <data android:pathPattern=".*\\.kf8" />
<data android:pathPattern=".*\\.fb2" /> <data android:pathPattern=".*\\.fb2" />
<data android:pathPattern=".*\\.cbz" /> <data android:pathPattern=".*\\.cbz" />
<data android:scheme="content" /> <data android:scheme="content" />
<data android:scheme="file" /> <data android:scheme="file" />
</intent-filter> </intent-filter>
@@ -101,9 +115,9 @@
<activity <activity
android:name=".media.CalendarActivity" android:name=".media.CalendarActivity"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
<activity android:name="ani.dantotsu.media.user.ListActivity" /> <activity android:name=".media.user.ListActivity" />
<activity <activity
android:name="ani.dantotsu.media.manga.mangareader.MangaReaderActivity" android:name=".media.manga.mangareader.MangaReaderActivity"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
android:label="@string/manga" android:label="@string/manga"
@@ -116,7 +130,7 @@
<activity android:name=".media.CharacterDetailsActivity" /> <activity android:name=".media.CharacterDetailsActivity" />
<activity android:name=".home.NoInternet" /> <activity android:name=".home.NoInternet" />
<activity <activity
android:name="ani.dantotsu.media.anime.ExoplayerView" android:name=".media.anime.ExoplayerView"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
@@ -125,7 +139,7 @@
android:supportsPictureInPicture="true" android:supportsPictureInPicture="true"
tools:targetApi="n" /> tools:targetApi="n" />
<activity <activity
android:name="ani.dantotsu.connections.anilist.Login" android:name=".connections.anilist.Login"
android:configChanges="orientation|screenSize|layoutDirection" android:configChanges="orientation|screenSize|layoutDirection"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
@@ -142,7 +156,7 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name="ani.dantotsu.connections.mal.Login" android:name=".connections.mal.Login"
android:configChanges="orientation|screenSize|layoutDirection" android:configChanges="orientation|screenSize|layoutDirection"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
@@ -158,8 +172,8 @@
android:scheme="dantotsu" /> android:scheme="dantotsu" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
<activity android:name="ani.dantotsu.connections.discord.Login" android:name=".connections.discord.Login"
android:configChanges="orientation|screenSize|layoutDirection" android:configChanges="orientation|screenSize|layoutDirection"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
@@ -176,9 +190,26 @@
<data android:host="discord.dantotsu.com" /> <data android:host="discord.dantotsu.com" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name="ani.dantotsu.connections.anilist.UrlMedia" android:name=".others.webview.CookieCatcher"
android:configChanges="orientation|screenSize|layoutDirection"
android:excludeFromRecents="true"
android:exported="true"
android:launchMode="singleTask">
<intent-filter android:label="Discord Login for Dantotsu">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="dantotsu" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="discord.dantotsu.com" />
</intent-filter>
</activity>
<activity
android:name=".connections.anilist.UrlMedia"
android:configChanges="orientation|screenSize|layoutDirection" android:configChanges="orientation|screenSize|layoutDirection"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
@@ -214,22 +245,23 @@
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.Main" /> <action android:name="android.intent.action.Main" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" /> <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity" android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar" android:exported="false"
android:exported="false" /> android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity <activity
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallActivity" android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar" android:exported="false"
android:exported="false" /> android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<receiver <receiver
android:name=".subcriptions.AlarmReceiver" android:name=".subcriptions.AlarmReceiver"
@@ -255,18 +287,49 @@
android:resource="@xml/provider_paths" /> android:resource="@xml/provider_paths" />
</provider> </provider>
<service android:name=".download.video.MyDownloadService" <service
android:exported="false"> android:name=".widgets.CurrentlyAiringRemoteViewsService"
android:permission="android.permission.BIND_REMOTEVIEWS"
android:exported="true" />
<service
android:name=".download.video.ExoplayerDownloadService"
android:exported="false"
android:foregroundServiceType="dataSync">
<intent-filter> <intent-filter>
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART" /> <action android:name="androidx.media3.exoplayer.downloadService.action.RESTART" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
</intent-filter> </intent-filter>
</service> </service>
<service android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService" <service
android:exported="false" /> 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:foregroundServiceType="dataSync" />
<service
android:name=".download.manga.MangaDownloaderService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service
android:name=".download.novel.NovelDownloaderService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service android:name=".download.anime.AnimeDownloaderService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service
android:name=".connections.discord.DiscordService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service android:name="androidx.media3.exoplayer.scheduler.PlatformScheduler$PlatformSchedulerService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true"/>
<service android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService" <meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:exported="false" /> android:value="androidx.media3.cast.DefaultCastOptionsProvider"/>
</application> </application>
</manifest> </manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -8,19 +8,36 @@ import androidx.multidex.MultiDex
import androidx.multidex.MultiDexApplication import androidx.multidex.MultiDexApplication
import ani.dantotsu.aniyomi.anime.custom.AppModule import ani.dantotsu.aniyomi.anime.custom.AppModule
import ani.dantotsu.aniyomi.anime.custom.PreferenceModule import ani.dantotsu.aniyomi.anime.custom.PreferenceModule
import eu.kanade.tachiyomi.data.notification.Notifications
import tachiyomi.core.util.system.logcat
import ani.dantotsu.others.DisabledReports import ani.dantotsu.others.DisabledReports
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.parsers.NovelSources
import ani.dantotsu.parsers.novel.NovelExtensionManager
import ani.dantotsu.settings.SettingsActivity
import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColors
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 eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import logcat.AndroidLogcatLogger import logcat.AndroidLogcatLogger
import logcat.LogPriority import logcat.LogPriority
import logcat.LogcatLogger import logcat.LogcatLogger
import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
class App : MultiDexApplication() { class App : MultiDexApplication() {
private lateinit var animeExtensionManager: AnimeExtensionManager
private lateinit var mangaExtensionManager: MangaExtensionManager
private lateinit var novelExtensionManager: NovelExtensionManager
override fun attachBaseContext(base: Context?) { override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base) super.attachBaseContext(base)
MultiDex.install(this) MultiDex.install(this)
@@ -38,21 +55,64 @@ class App : MultiDexApplication() {
val useMaterialYou = sharedPreferences.getBoolean("use_material_you", false) val useMaterialYou = sharedPreferences.getBoolean("use_material_you", false)
if (useMaterialYou) { if (useMaterialYou) {
DynamicColors.applyToActivitiesIfAvailable(this) DynamicColors.applyToActivitiesIfAvailable(this)
//TODO: HarmonizedColors
} }
registerActivityLifecycleCallbacks(mFTActivityLifecycleCallbacks) registerActivityLifecycleCallbacks(mFTActivityLifecycleCallbacks)
Firebase.crashlytics.setCrashlyticsCollectionEnabled(!DisabledReports) Firebase.crashlytics.setCrashlyticsCollectionEnabled(!DisabledReports)
initializeNetwork(baseContext) getSharedPreferences(
getString(R.string.preference_file_key),
Context.MODE_PRIVATE
).getBoolean("shared_user_id", true).let {
if (!it) return@let
val dUsername = getSharedPreferences(
getString(R.string.preference_file_key),
Context.MODE_PRIVATE
).getString("discord_username", null)
val aUsername = getSharedPreferences(
getString(R.string.preference_file_key),
Context.MODE_PRIVATE
).getString("anilist_username", null)
if (dUsername != null || aUsername != null) {
Firebase.crashlytics.setUserId("$dUsername - $aUsername")
}
}
FirebaseCrashlytics.getInstance().setCustomKey("device Info", SettingsActivity.getDeviceInfo())
Injekt.importModule(AppModule(this)) Injekt.importModule(AppModule(this))
Injekt.importModule(PreferenceModule(this)) Injekt.importModule(PreferenceModule(this))
initializeNetwork(baseContext)
setupNotificationChannels() setupNotificationChannels()
if (!LogcatLogger.isInstalled) { if (!LogcatLogger.isInstalled) {
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE)) LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
} }
animeExtensionManager = Injekt.get()
mangaExtensionManager = Injekt.get()
novelExtensionManager = Injekt.get()
val animeScope = CoroutineScope(Dispatchers.Default)
animeScope.launch {
animeExtensionManager.findAvailableExtensions()
logger("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
AnimeSources.init(animeExtensionManager.installedExtensionsFlow, this@App)
} }
val mangaScope = CoroutineScope(Dispatchers.Default)
mangaScope.launch {
mangaExtensionManager.findAvailableExtensions()
logger("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
MangaSources.init(mangaExtensionManager.installedExtensionsFlow, this@App)
}
val novelScope = CoroutineScope(Dispatchers.Default)
novelScope.launch {
novelExtensionManager.findAvailableExtensions()
logger("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
NovelSources.init(novelExtensionManager.installedExtensionsFlow)
}
}
private fun setupNotificationChannels() { private fun setupNotificationChannels() {
try { try {

View File

@@ -4,6 +4,8 @@ import android.animation.ObjectAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.DatePickerDialog import android.app.DatePickerDialog
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
@@ -13,6 +15,7 @@ import android.content.res.Configuration
import android.content.res.Resources.getSystem import android.content.res.Resources.getSystem
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.media.MediaScannerConnection import android.media.MediaScannerConnection
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkCapabilities.* import android.net.NetworkCapabilities.*
@@ -23,11 +26,13 @@ import android.telephony.TelephonyManager
import android.text.InputFilter import android.text.InputFilter
import android.text.Spanned import android.text.Spanned
import android.util.AttributeSet import android.util.AttributeSet
import android.util.TypedValue
import android.view.* import android.view.*
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.animation.* import android.view.animation.*
import android.widget.* import android.widget.*
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.NotificationCompat
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
@@ -44,6 +49,8 @@ import ani.dantotsu.databinding.ItemCountDownBinding
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.parsers.ShowResponse import ani.dantotsu.parsers.ShowResponse
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.subcriptions.NotificationClickReceiver
import ani.dantotsu.themes.ThemeManager
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade
@@ -53,6 +60,8 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.internal.ViewUtils import com.google.android.material.internal.ViewUtils
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.firebase.crashlytics.FirebaseCrashlytics
import eu.kanade.tachiyomi.data.notification.Notifications
import kotlinx.coroutines.* import kotlinx.coroutines.*
import nl.joery.animatedbottombar.AnimatedBottomBar import nl.joery.animatedbottombar.AnimatedBottomBar
import java.io.* import java.io.*
@@ -124,6 +133,13 @@ fun <T> loadData(fileName: String, context: Context? = null, toast: Boolean = tr
} }
} catch (e: Exception) { } catch (e: Exception) {
if (toast) snackString(a?.getString(R.string.error_loading_data, fileName)) if (toast) snackString(a?.getString(R.string.error_loading_data, fileName))
//try to delete the file
try {
a?.deleteFile(fileName)
} catch (e: Exception) {
FirebaseCrashlytics.getInstance().log("Failed to delete file $fileName")
FirebaseCrashlytics.getInstance().recordException(e)
}
e.printStackTrace() e.printStackTrace()
} }
return null return null
@@ -132,7 +148,8 @@ fun <T> loadData(fileName: String, context: Context? = null, toast: Boolean = tr
fun initActivity(a: Activity) { fun initActivity(a: Activity) {
val window = a.window val window = a.window
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
val uiSettings = loadData<UserInterfaceSettings>("ui_settings", toast = false) ?: UserInterfaceSettings().apply { val uiSettings = loadData<UserInterfaceSettings>("ui_settings", toast = false)
?: UserInterfaceSettings().apply {
saveData("ui_settings", this) saveData("ui_settings", this)
} }
uiSettings.darkMode.apply { uiSettings.darkMode.apply {
@@ -146,7 +163,8 @@ fun initActivity(a: Activity) {
} }
if (uiSettings.immersiveMode) { if (uiSettings.immersiveMode) {
if (navBarHeight == 0) { if (navBarHeight == 0) {
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))?.apply { ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
?.apply {
navBarHeight = this.getInsets(WindowInsetsCompat.Type.systemBars()).bottom navBarHeight = this.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
} }
} }
@@ -160,7 +178,8 @@ fun initActivity(a: Activity) {
} }
} else } else
if (statusBarHeight == 0) { if (statusBarHeight == 0) {
val windowInsets = ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content)) val windowInsets =
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
if (windowInsets != null) { if (windowInsets != null) {
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
statusBarHeight = insets.top statusBarHeight = insets.top
@@ -188,10 +207,17 @@ fun Activity.hideStatusBar() {
open class BottomSheetDialogFragment : BottomSheetDialogFragment() { open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
val window = dialog?.window
val decorView: View = window?.decorView ?: return
decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
if (this.resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) { if (this.resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) {
val behavior = BottomSheetBehavior.from(requireView().parent as View) val behavior = BottomSheetBehavior.from(requireView().parent as View)
behavior.state = BottomSheetBehavior.STATE_EXPANDED behavior.state = BottomSheetBehavior.STATE_EXPANDED
} }
val typedValue = TypedValue()
val theme = requireContext().theme
theme.resolveAttribute(com.google.android.material.R.attr.colorOnSurfaceInverse, typedValue, true)
window.navigationBarColor = typedValue.data
} }
override fun show(manager: FragmentManager, tag: String?) { override fun show(manager: FragmentManager, tag: String?) {
@@ -202,9 +228,9 @@ open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
} }
fun isOnline(context: Context): Boolean { fun isOnline(context: Context): Boolean {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
return tryWith { return tryWith {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
return@tryWith if (cap != null) { return@tryWith if (cap != null) {
when { when {
@@ -220,7 +246,6 @@ fun isOnline(context: Context): Boolean {
else -> false else -> false
} }
} else false } else false
} else true
} ?: false } ?: false
} }
@@ -238,7 +263,8 @@ fun startMainActivity(activity: Activity, bundle: Bundle? = null) {
} }
class DatePickerFragment(activity: Activity, var date: FuzzyDate = FuzzyDate().getToday()) : DialogFragment(), class DatePickerFragment(activity: Activity, var date: FuzzyDate = FuzzyDate().getToday()) :
DialogFragment(),
DatePickerDialog.OnDateSetListener { DatePickerDialog.OnDateSetListener {
var dialog: DatePickerDialog var dialog: DatePickerDialog
@@ -263,9 +289,20 @@ class DatePickerFragment(activity: Activity, var date: FuzzyDate = FuzzyDate().g
} }
} }
class InputFilterMinMax(private val min: Double, private val max: Double, private val status: AutoCompleteTextView? = null) : class InputFilterMinMax(
private val min: Double,
private val max: Double,
private val status: AutoCompleteTextView? = null
) :
InputFilter { InputFilter {
override fun filter(source: CharSequence, start: Int, end: Int, dest: Spanned, dstart: Int, dend: Int): CharSequence? { override fun filter(
source: CharSequence,
start: Int,
end: Int,
dest: Spanned,
dstart: Int,
dend: Int
): CharSequence? {
try { try {
val input = (dest.toString() + source.toString()).toDouble() val input = (dest.toString() + source.toString()).toDouble()
if (isInRange(min, max, input)) return null if (isInRange(min, max, input)) return null
@@ -288,11 +325,20 @@ class InputFilterMinMax(private val min: Double, private val max: Double, privat
} }
class ZoomOutPageTransformer(private val uiSettings: UserInterfaceSettings) : ViewPager2.PageTransformer { class ZoomOutPageTransformer(private val uiSettings: UserInterfaceSettings) :
ViewPager2.PageTransformer {
override fun transformPage(view: View, position: Float) { override fun transformPage(view: View, position: Float) {
if (position == 0.0f && uiSettings.layoutAnimations) { if (position == 0.0f && uiSettings.layoutAnimations) {
setAnimation(view.context, view, uiSettings, 300, floatArrayOf(1.3f, 1f, 1.3f, 1f), 0.5f to 0f) setAnimation(
ObjectAnimator.ofFloat(view, "alpha", 0f, 1.0f).setDuration((200 * uiSettings.animationSpeed).toLong()).start() view.context,
view,
uiSettings,
300,
floatArrayOf(1.3f, 1f, 1.3f, 1f),
0.5f to 0f
)
ObjectAnimator.ofFloat(view, "alpha", 0f, 1.0f)
.setDuration((200 * uiSettings.animationSpeed).toLong()).start()
} }
} }
} }
@@ -327,7 +373,11 @@ class FadingEdgeRecyclerView : RecyclerView {
constructor(context: Context) : super(context) constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
override fun isPaddingOffsetRequired(): Boolean { override fun isPaddingOffsetRequired(): Boolean {
return !clipToPadding return !clipToPadding
@@ -419,15 +469,30 @@ fun String.findBetween(a: String, b: String): String? {
fun ImageView.loadImage(url: String?, size: Int = 0) { fun ImageView.loadImage(url: String?, size: Int = 0) {
if (!url.isNullOrEmpty()) { if (!url.isNullOrEmpty()) {
val localFile = File(url)
if (localFile.exists()) {
loadLocalImage(localFile, size)
} else {
loadImage(FileUrl(url), size) loadImage(FileUrl(url), size)
} }
} }
}
fun ImageView.loadImage(file: FileUrl?, size: Int = 0) { fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
if (file?.url?.isNotEmpty() == true) { if (file?.url?.isNotEmpty() == true) {
tryWith { tryWith {
val glideUrl = GlideUrl(file.url) { file.headers } val glideUrl = GlideUrl(file.url) { file.headers }
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size).into(this) Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
.into(this)
}
}
}
fun ImageView.loadLocalImage(file: File?, size: Int = 0) {
if (file?.exists() == true) {
tryWith {
Glide.with(this.context).load(file).transition(withCrossFade()).override(size)
.into(this)
} }
} }
} }
@@ -485,7 +550,12 @@ abstract class GesturesListener : GestureDetector.SimpleOnGestureListener() {
return super.onDoubleTap(e) return super.onDoubleTap(e)
} }
override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean { override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
onScrollYClick(distanceY) onScrollYClick(distanceY)
onScrollXClick(distanceX) onScrollXClick(distanceX)
return super.onScroll(e1, e2, distanceX, distanceY) return super.onScroll(e1, e2, distanceX, distanceY)
@@ -551,7 +621,7 @@ fun saveImageToDownloads(title: String, bitmap: Bitmap, context: Context) {
"$APPLICATION_ID.provider", "$APPLICATION_ID.provider",
saveImage( saveImage(
bitmap, bitmap,
Environment.getExternalStorageDirectory().absolutePath + "/" + Environment.DIRECTORY_DOWNLOADS, Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath,
title title
) ?: return ) ?: return
) )
@@ -574,13 +644,16 @@ fun shareImage(title: String, bitmap: Bitmap, context: Context) {
fun saveImage(image: Bitmap, path: String, imageFileName: String): File? { fun saveImage(image: Bitmap, path: String, imageFileName: String): File? {
val imageFile = File(path, "$imageFileName.png") val imageFile = File(path, "$imageFileName.png")
return tryWith { return try {
val fOut: OutputStream = FileOutputStream(imageFile) val fOut: OutputStream = FileOutputStream(imageFile)
image.compress(Bitmap.CompressFormat.PNG, 0, fOut) image.compress(Bitmap.CompressFormat.PNG, 0, fOut)
fOut.close() fOut.close()
scanFile(imageFile.absolutePath, currContext()!!) scanFile(imageFile.absolutePath, currContext()!!)
toast(String.format(currContext()!!.getString(R.string.saved_to_path, path))) toast(String.format(currContext()!!.getString(R.string.saved_to_path, path)))
imageFile imageFile
} catch (e: Exception) {
snackString("Failed to save image: ${e.localizedMessage}")
null
} }
} }
@@ -623,13 +696,19 @@ fun copyToClipboard(string: String, toast: Boolean = true) {
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun countDown(media: Media, view: ViewGroup) { fun countDown(media: Media, view: ViewGroup) {
if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null && (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 7.toLong()) { if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null && (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 28.toLong()) {
val v = ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false) val v = ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
view.addView(v.root, 0) view.addView(v.root, 0)
v.mediaCountdownText.text = v.mediaCountdownText.text =
currActivity()?.getString(R.string.episode_release_countdown, media.anime.nextAiringEpisode!! + 1) currActivity()?.getString(
R.string.episode_release_countdown,
media.anime.nextAiringEpisode!! + 1
)
object : CountDownTimer((media.anime.nextAiringEpisodeTime!! + 10000) * 1000 - System.currentTimeMillis(), 1000) { object : CountDownTimer(
(media.anime.nextAiringEpisodeTime!! + 10000) * 1000 - System.currentTimeMillis(),
1000
) {
override fun onTick(millisUntilFinished: Long) { override fun onTick(millisUntilFinished: Long) {
val a = millisUntilFinished / 1000 val a = millisUntilFinished / 1000
v.mediaCountdown.text = currActivity()?.getString( v.mediaCountdown.text = currActivity()?.getString(
@@ -720,16 +799,22 @@ fun toast(string: String?) {
if (string != null) { if (string != null) {
logger(string) logger(string)
MainScope().launch { MainScope().launch {
Toast.makeText(currActivity()?.application ?: return@launch, string, Toast.LENGTH_SHORT).show() Toast.makeText(currActivity()?.application ?: return@launch, string, Toast.LENGTH_SHORT)
.show()
} }
} }
} }
fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null) { fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null) {
try { //I have no idea why this sometimes crashes for some people...
if (s != null) { if (s != null) {
(activity ?: currActivity())?.apply { (activity ?: currActivity())?.apply {
runOnUiThread { runOnUiThread {
val snackBar = Snackbar.make(window.decorView.findViewById(android.R.id.content), s, Snackbar.LENGTH_LONG) val snackBar = Snackbar.make(
window.decorView.findViewById(android.R.id.content),
s,
Snackbar.LENGTH_SHORT
)
snackBar.view.apply { snackBar.view.apply {
updateLayoutParams<FrameLayout.LayoutParams> { updateLayoutParams<FrameLayout.LayoutParams> {
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM) gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)
@@ -752,9 +837,14 @@ fun snackString(s: String?, activity: Activity? = null, clipboard: String? = nul
} }
logger(s) logger(s)
} }
} catch (e: Exception) {
logger(e.stackTraceToString())
FirebaseCrashlytics.getInstance().recordException(e)
}
} }
open class NoPaddingArrayAdapter<T>(context: Context, layoutId: Int, items: List<T>) : ArrayAdapter<T>(context, layoutId, items) { open class NoPaddingArrayAdapter<T>(context: Context, layoutId: Int, items: List<T>) :
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 {
val view = super.getView(position, convertView, parent) val view = super.getView(position, convertView, parent)
view.setPadding(0, view.paddingTop, view.paddingRight, view.paddingBottom) view.setPadding(0, view.paddingTop, view.paddingRight, view.paddingBottom)
@@ -775,12 +865,17 @@ class SpinnerNoSwipe : androidx.appcompat.widget.AppCompatSpinner {
setup() setup()
} }
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
setup() setup()
} }
private fun setup() { private fun setup() {
mGestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() { mGestureDetector =
GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
override fun onSingleTapUp(e: MotionEvent): Boolean { override fun onSingleTapUp(e: MotionEvent): Boolean {
return performClick() return performClick()
} }
@@ -828,7 +923,11 @@ fun getCurrentBrightnessValue(context: Context): Float {
} }
fun getCur(): Float { fun getCur(): Float {
return Settings.System.getInt(context.contentResolver, Settings.System.SCREEN_BRIGHTNESS, 127).toFloat() return Settings.System.getInt(
context.contentResolver,
Settings.System.SCREEN_BRIGHTNESS,
127
).toFloat()
} }
return brightnessConverter(getCur() / getMax(), true) return brightnessConverter(getCur() / getMax(), true)
@@ -859,6 +958,33 @@ fun checkCountry(context: Context): Boolean {
} }
} }
const val INCOGNITO_CHANNEL_ID = 26
@SuppressLint("LaunchActivityFromNotification")
fun incognitoNotification(context: Context) {
val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val incognito = context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
.getBoolean("incognito", false)
if (incognito) {
val intent = Intent(context, NotificationClickReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context, 0, intent,
PendingIntent.FLAG_IMMUTABLE
)
val builder = NotificationCompat.Builder(context, Notifications.CHANNEL_INCOGNITO_MODE)
.setSmallIcon(R.drawable.ic_incognito_24)
.setContentTitle("Incognito Mode")
.setContentText("Disable Incognito Mode")
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(pendingIntent)
.setOngoing(true)
notificationManager.notify(INCOGNITO_CHANNEL_ID, builder.build())
} else {
notificationManager.cancel(INCOGNITO_CHANNEL_ID)
}
}
suspend fun View.pop() { suspend fun View.pop() {
currActivity()?.runOnUiThread { currActivity()?.runOnUiThread {
ObjectAnimator.ofFloat(this@pop, "scaleX", 1f, 1.25f).setDuration(120).start() ObjectAnimator.ofFloat(this@pop, "scaleX", 1f, 1.25f).setDuration(120).start()

View File

@@ -1,24 +1,28 @@
package ani.dantotsu package ani.dantotsu
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
import android.graphics.drawable.GradientDrawable
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle 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.Log
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.AnticipateInterpolator import android.view.animation.AnticipateInterpolator
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.activity.addCallback import androidx.activity.addCallback
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.animation.doOnEnd import androidx.core.animation.doOnEnd
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.doOnAttach import androidx.core.view.doOnAttach
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
@@ -26,12 +30,14 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.Download
import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.adapter.FragmentStateAdapter
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistHomeViewModel import ani.dantotsu.connections.anilist.AnilistHomeViewModel
import ani.dantotsu.databinding.ActivityMainBinding import ani.dantotsu.databinding.ActivityMainBinding
import ani.dantotsu.databinding.SplashScreenBinding import ani.dantotsu.databinding.SplashScreenBinding
import ani.dantotsu.download.video.Helper
import ani.dantotsu.home.AnimeFragment import ani.dantotsu.home.AnimeFragment
import ani.dantotsu.home.HomeFragment import ani.dantotsu.home.HomeFragment
import ani.dantotsu.home.LoginFragment import ani.dantotsu.home.LoginFragment
@@ -39,52 +45,109 @@ import ani.dantotsu.home.MangaFragment
import ani.dantotsu.home.NoInternet import ani.dantotsu.home.NoInternet
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.others.CustomBottomDialog import ani.dantotsu.others.CustomBottomDialog
import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.others.LangSet
import ani.dantotsu.parsers.MangaSources import ani.dantotsu.others.SharedPreferenceBooleanLiveData
import ani.dantotsu.parsers.novel.NovelExtensionManager
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
import ani.dantotsu.themes.ThemeManager
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin import io.noties.markwon.SoftBreakAddsNewLinePlugin
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import nl.joery.animatedbottombar.AnimatedBottomBar import nl.joery.animatedbottombar.AnimatedBottomBar
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.Serializable import java.io.Serializable
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private lateinit var incognitoLiveData: SharedPreferenceBooleanLiveData
private val scope = lifecycleScope private val scope = lifecycleScope
private var load = false private var load = false
private var uiSettings = UserInterfaceSettings() private var uiSettings = UserInterfaceSettings()
private val animeExtensionManager: AnimeExtensionManager by injectLazy()
private val mangaExtensionManager: MangaExtensionManager by injectLazy()
@SuppressLint("InternalInsetResource", "DiscouragedApi")
@OptIn(UnstableApi::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
//get FRAGMENT_CLASS_NAME from intent
val FRAGMENT_CLASS_NAME = intent.getStringExtra("FRAGMENT_CLASS_NAME")
binding = ActivityMainBinding.inflate(layoutInflater) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
val animeScope = CoroutineScope(Dispatchers.Default) val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
animeScope.launch { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
animeExtensionManager.findAvailableExtensions()
logger("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}") val backgroundDrawable = _bottomBar.background as GradientDrawable
AnimeSources.init(animeExtensionManager.installedExtensionsFlow) val currentColor = backgroundDrawable.color?.defaultColor ?: 0
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xF9000000.toInt()
backgroundDrawable.setColor(semiTransparentColor)
_bottomBar.background = backgroundDrawable
} }
val mangaScope = CoroutineScope(Dispatchers.Default) val sharedPreferences = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
mangaScope.launch { val colorOverflow = sharedPreferences.getBoolean("colorOverflow", false)
mangaExtensionManager.findAvailableExtensions() if (!colorOverflow) {
logger("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}") _bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
MangaSources.init(mangaExtensionManager.installedExtensionsFlow)
} }
val offset = try {
val statusBarHeightId = resources.getIdentifier("status_bar_height", "dimen", "android")
resources.getDimensionPixelSize(statusBarHeightId)
} catch (e: Exception) {
statusBarHeight
}
val layoutParams = binding.incognito.layoutParams as ViewGroup.MarginLayoutParams
layoutParams.topMargin = 11 * offset / 12
binding.incognito.layoutParams = layoutParams
incognitoLiveData = SharedPreferenceBooleanLiveData(
sharedPreferences,
"incognito",
false
)
incognitoLiveData.observe(this) {
if (it) {
val slideDownAnim = ObjectAnimator.ofFloat(
binding.incognito,
View.TRANSLATION_Y,
-(binding.incognito.height.toFloat() + statusBarHeight),
0f
)
slideDownAnim.duration = 200
slideDownAnim.start()
binding.incognito.visibility = View.VISIBLE
} else {
val slideUpAnim = ObjectAnimator.ofFloat(
binding.incognito,
View.TRANSLATION_Y,
0f,
-(binding.incognito.height.toFloat() + statusBarHeight)
)
slideUpAnim.duration = 200
slideUpAnim.start()
//wait for animation to finish
Handler(Looper.getMainLooper()).postDelayed(
{ binding.incognito.visibility = View.GONE },
200
)
}
}
incognitoNotification(this)
var doubleBackToExitPressedOnce = false var doubleBackToExitPressedOnce = false
onBackPressedDispatcher.addCallback(this) { onBackPressedDispatcher.addCallback(this) {
if (doubleBackToExitPressedOnce) { if (doubleBackToExitPressedOnce) {
@@ -98,17 +161,25 @@ 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 {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
val splash = SplashScreenBinding.inflate(layoutInflater) val splash = SplashScreenBinding.inflate(layoutInflater)
binding.root.addView(splash.root) binding.root.addView(splash.root)
(splash.splashImage.drawable as Animatable).start() (splash.splashImage.drawable as Animatable).start()
// Wait for 2 seconds (2000 milliseconds) delay(1200)
delay(2000)
// Now perform the animation
ObjectAnimator.ofFloat( ObjectAnimator.ofFloat(
splash.root, splash.root,
View.TRANSLATION_Y, View.TRANSLATION_Y,
@@ -121,32 +192,65 @@ class MainActivity : AppCompatActivity() {
start() start()
} }
} }
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
splashScreen.setOnExitAnimationListener { splashScreenView ->
ObjectAnimator.ofFloat(
splashScreenView,
View.TRANSLATION_Y,
0f,
-splashScreenView.height.toFloat()
).apply {
interpolator = AnticipateInterpolator()
duration = 200L
doOnEnd { splashScreenView.remove() }
start()
}
}
}
binding.root.doOnAttach { binding.root.doOnAttach {
initActivity(this) initActivity(this)
uiSettings = loadData("ui_settings") ?: uiSettings uiSettings = loadData("ui_settings") ?: uiSettings
selectedOption = uiSettings.defaultStartUpTab selectedOption = if (FRAGMENT_CLASS_NAME != null) {
binding.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { when (FRAGMENT_CLASS_NAME) {
AnimeFragment::class.java.name -> 0
HomeFragment::class.java.name -> 1
MangaFragment::class.java.name -> 2
else -> 1
}
} else {
uiSettings.defaultStartUpTab
}
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight bottomMargin = navBarHeight
}
}
}
}
val offlineMode = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
.getBoolean("offlineMode", false)
if (!isOnline(this)) { if (!isOnline(this)) {
snackString(this@MainActivity.getString(R.string.no_internet_connection)) snackString(this@MainActivity.getString(R.string.no_internet_connection))
startActivity(Intent(this, NoInternet::class.java)) startActivity(Intent(this, NoInternet::class.java))
} else {
if (offlineMode) {
snackString(this@MainActivity.getString(R.string.no_internet_connection))
startActivity(Intent(this, NoInternet::class.java))
} else { } else {
val model: AnilistHomeViewModel by viewModels() val model: AnilistHomeViewModel by viewModels()
model.genres.observe(this) { model.genres.observe(this) { it ->
if (it != null) { if (it != null) {
if (it) { if (it) {
val navbar = binding.navbar val navbar = binding.includedNavbar.navbar
bottomBar = navbar bottomBar = navbar
navbar.visibility = View.VISIBLE navbar.visibility = View.VISIBLE
binding.mainProgressBar.visibility = View.GONE binding.mainProgressBar.visibility = View.GONE
val mainViewPager = binding.viewpager val mainViewPager = binding.viewpager
mainViewPager.isUserInputEnabled = false mainViewPager.isUserInputEnabled = false
mainViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle) mainViewPager.adapter =
ViewPagerAdapter(supportFragmentManager, lifecycle)
mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings)) mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
navbar.setOnTabSelectListener(object : navbar.setOnTabSelectListener(object :
AnimatedBottomBar.OnTabSelectListener { AnimatedBottomBar.OnTabSelectListener {
@@ -162,7 +266,12 @@ class MainActivity : AppCompatActivity() {
} }
}) })
navbar.selectTabAt(selectedOption) navbar.selectTabAt(selectedOption)
mainViewPager.post { mainViewPager.setCurrentItem(selectedOption, false) } mainViewPager.post {
mainViewPager.setCurrentItem(
selectedOption,
false
)
}
} else { } else {
binding.mainProgressBar.visibility = View.GONE binding.mainProgressBar.visibility = View.GONE
} }
@@ -225,8 +334,28 @@ class MainActivity : AppCompatActivity() {
} }
} }
} }
} }
//TODO: Remove this
GlobalScope.launch(Dispatchers.IO) {
val index = Helper.downloadManager(this@MainActivity).downloadIndex
val downloadCursor = index.getDownloads()
while (downloadCursor.moveToNext()) {
val download = downloadCursor.download
Log.e("Downloader", download.request.uri.toString())
Log.e("Downloader", download.request.id.toString())
Log.e("Downloader", download.request.mimeType.toString())
Log.e("Downloader", download.request.data.size.toString())
Log.e("Downloader", download.bytesDownloaded.toString())
Log.e("Downloader", download.state.toString())
Log.e("Downloader", download.failureReason.toString())
if (download.state == Download.STATE_FAILED) { //simple cleanup
Helper.downloadManager(this@MainActivity).removeDownload(download.request.id)
}
}
}
}
//ViewPager //ViewPager
private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) : private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :

View File

@@ -8,58 +8,51 @@ import ani.dantotsu.others.webview.WebViewBottomDialog
import com.lagradost.nicehttp.Requests import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.ResponseParser import com.lagradost.nicehttp.ResponseParser
import com.lagradost.nicehttp.addGenericDns import com.lagradost.nicehttp.addGenericDns
import kotlinx.coroutines.* import eu.kanade.tachiyomi.network.NetworkHelper
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.serializer import kotlinx.serialization.serializer
import okhttp3.Cache
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import java.io.File import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.PrintWriter import java.io.PrintWriter
import java.io.Serializable import java.io.Serializable
import java.io.StringWriter import java.io.StringWriter
import java.util.concurrent.* import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KFunction import kotlin.reflect.KFunction
val defaultHeaders = mapOf( lateinit var defaultHeaders: Map<String, String>
"User-Agent" to
"Mozilla/5.0 (Linux; Android %s; %s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Mobile Safari/537.36"
.format(Build.VERSION.RELEASE, Build.MODEL)
)
lateinit var cache: Cache
lateinit var okHttpClient: OkHttpClient lateinit var okHttpClient: OkHttpClient
lateinit var client: Requests lateinit var client: Requests
fun initializeNetwork(context: Context) { fun initializeNetwork(context: Context) {
val dns = loadData<Int>("settings_dns")
cache = Cache( val networkHelper = Injekt.get<NetworkHelper>()
File(context.cacheDir, "http_cache"),
5 * 1024L * 1024L // 5 MiB defaultHeaders = mapOf(
"User-Agent" to
Injekt.get<NetworkHelper>().defaultUserAgentProvider()
.format(Build.VERSION.RELEASE, Build.MODEL)
) )
okHttpClient = OkHttpClient.Builder()
.followRedirects(true) okHttpClient = networkHelper.client
.followSslRedirects(true)
.cache(cache)
.apply {
when (dns) {
1 -> addGoogleDns()
2 -> addCloudFlareDns()
3 -> addAdGuardDns()
}
}
.build()
client = Requests( client = Requests(
okHttpClient, networkHelper.client,
defaultHeaders, defaultHeaders,
defaultCacheTime = 6, defaultCacheTime = 6,
defaultCacheTimeUnit = TimeUnit.HOURS, defaultCacheTimeUnit = TimeUnit.HOURS,
responseParser = Mapper responseParser = Mapper
) )
} }
object Mapper : ResponseParser { object Mapper : ResponseParser {
@@ -122,7 +115,11 @@ fun <T> tryWith(post: Boolean = false, snackbar: Boolean = true, call: () -> T):
} }
} }
suspend fun <T> tryWithSuspend(post: Boolean = false, snackbar: Boolean = true, call: suspend () -> T): T? { suspend fun <T> tryWithSuspend(
post: Boolean = false,
snackbar: Boolean = true,
call: suspend () -> T
): T? {
return try { return try {
call.invoke() call.invoke()
} catch (e: Throwable) { } catch (e: Throwable) {
@@ -209,7 +206,8 @@ suspend fun webViewInterface(webViewDialog: WebViewBottomDialog): Map<String, St
map = it map = it
latch.countDown() latch.countDown()
} }
val fragmentManager = (currContext() as FragmentActivity?)?.supportFragmentManager ?: return null val fragmentManager =
(currContext() as FragmentActivity?)?.supportFragmentManager ?: return null
webViewDialog.show(fragmentManager, "web-view") webViewDialog.show(fragmentManager, "web-view")
delay(0) delay(0)
latch.await(2, TimeUnit.MINUTES) latch.await(2, TimeUnit.MINUTES)

View File

@@ -3,16 +3,26 @@ package ani.dantotsu.aniyomi.anime.custom
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import androidx.annotation.OptIn
import androidx.core.content.ContextCompat
import androidx.media3.common.util.UnstableApi
import androidx.media3.database.StandaloneDatabaseProvider
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.media.manga.MangaCache import ani.dantotsu.media.manga.MangaCache
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager import ani.dantotsu.parsers.novel.NovelExtensionManager
import tachiyomi.core.preference.PreferenceStore
import eu.kanade.domain.base.BasePreferences import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkPreferences import eu.kanade.tachiyomi.network.NetworkPreferences
import eu.kanade.tachiyomi.source.anime.AndroidAnimeSourceManager
import eu.kanade.tachiyomi.source.manga.AndroidMangaSourceManager
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.domain.source.anime.service.AnimeSourceManager
import tachiyomi.domain.source.manga.service.MangaSourceManager
import uy.kohesive.injekt.api.InjektModule import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addSingleton import uy.kohesive.injekt.api.addSingleton
@@ -20,14 +30,20 @@ import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class AppModule(val app: Application) : InjektModule { class AppModule(val app: Application) : InjektModule {
@OptIn(UnstableApi::class)
override fun InjektRegistrar.registerInjectables() { override fun InjektRegistrar.registerInjectables() {
addSingleton(app) addSingleton(app)
addSingletonFactory { DownloadsManager(app) }
addSingletonFactory { NetworkHelper(app, get()) } addSingletonFactory { NetworkHelper(app, get()) }
addSingletonFactory { AnimeExtensionManager(app) } addSingletonFactory { AnimeExtensionManager(app) }
addSingletonFactory { MangaExtensionManager(app) } addSingletonFactory { MangaExtensionManager(app) }
addSingletonFactory { NovelExtensionManager(app) }
addSingletonFactory<AnimeSourceManager> { AndroidAnimeSourceManager(app, get()) }
addSingletonFactory<MangaSourceManager> { AndroidMangaSourceManager(app, get()) }
val sharedPreferences = app.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE) val sharedPreferences = app.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
addSingleton(sharedPreferences) addSingleton(sharedPreferences)
@@ -39,7 +55,14 @@ class AppModule(val app: Application) : InjektModule {
} }
} }
addSingletonFactory { StandaloneDatabaseProvider(app) }
addSingletonFactory { MangaCache() } addSingletonFactory { MangaCache() }
ContextCompat.getMainExecutor(app).execute {
get<AnimeSourceManager>()
get<MangaSourceManager>()
}
} }
} }

View File

@@ -10,13 +10,15 @@ import ani.dantotsu.toast
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.roundToInt
fun updateProgress(media: Media, number: String) { fun updateProgress(media: Media, number: String) {
val incognito = currContext()?.getSharedPreferences("Dantotsu", 0)
?.getBoolean("incognito", false) ?: false
if (!incognito) {
if (Anilist.userid != null) { if (Anilist.userid != null) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val a = number.toFloatOrNull()?.roundToInt() val a = number.toFloatOrNull()?.toInt()
if (a != media.userProgress) { if ((a ?: 0) > (media.userProgress ?: 0)) {
Anilist.mutation.editList( Anilist.mutation.editList(
media.id, media.id,
a, a,
@@ -36,4 +38,7 @@ fun updateProgress(media: Media, number: String) {
} else { } else {
toast(currContext()?.getString(R.string.login_anilist_account)) toast(currContext()?.getString(R.string.login_anilist_account))
} }
} else {
toast("Sneaky sneaky :3")
}
} }

View File

@@ -10,7 +10,7 @@ import ani.dantotsu.currContext
import ani.dantotsu.openLinkInBrowser import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.tryWithSuspend import ani.dantotsu.tryWithSuspend
import java.io.File import java.io.File
import java.util.* import java.util.Calendar
object Anilist { object Anilist {
val query: AnilistQueries = AnilistQueries() val query: AnilistQueries = AnilistQueries()
@@ -29,7 +29,12 @@ object Anilist {
var tags: Map<Boolean, List<String>>? = null var tags: Map<Boolean, List<String>>? = null
val sortBy = listOf( val sortBy = listOf(
"SCORE_DESC","POPULARITY_DESC","TRENDING_DESC","TITLE_ENGLISH","TITLE_ENGLISH_DESC","SCORE" "SCORE_DESC",
"POPULARITY_DESC",
"TRENDING_DESC",
"TITLE_ENGLISH",
"TITLE_ENGLISH_DESC",
"SCORE"
) )
val seasons = listOf( val seasons = listOf(
@@ -132,7 +137,12 @@ object Anilist {
if (token != null || force) { if (token != null || force) {
if (token != null && useToken) headers["Authorization"] = "Bearer $token" if (token != null && useToken) headers["Authorization"] = "Bearer $token"
val json = client.post("https://graphql.anilist.co/", headers, data = data, cacheTime = cache ?: 10) val json = client.post(
"https://graphql.anilist.co/",
headers,
data = data,
cacheTime = cache ?: 10
)
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) println("Response : ${json.text}") if (show) println("Response : ${json.text}")
json.parsed() json.parsed()

View File

@@ -1,15 +1,17 @@
package ani.dantotsu.connections.anilist package ani.dantotsu.connections.anilist
import android.app.Activity import android.app.Activity
import android.content.Context
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.checkGenreTime
import ani.dantotsu.checkId
import ani.dantotsu.connections.anilist.Anilist.authorRoles import ani.dantotsu.connections.anilist.Anilist.authorRoles
import ani.dantotsu.connections.anilist.Anilist.executeQuery import ani.dantotsu.connections.anilist.Anilist.executeQuery
import ani.dantotsu.connections.anilist.api.FuzzyDate import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.anilist.api.Page import ani.dantotsu.connections.anilist.api.Page
import ani.dantotsu.connections.anilist.api.Query import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.checkGenreTime
import ani.dantotsu.checkId
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.isOnline
import ani.dantotsu.loadData import ani.dantotsu.loadData
import ani.dantotsu.logError import ani.dantotsu.logError
import ani.dantotsu.media.Author import ani.dantotsu.media.Author
@@ -33,6 +35,13 @@ class AnilistQueries {
}.also { println("time : $it") } }.also { println("time : $it") }
val user = response?.data?.user ?: return false val user = response?.data?.user ?: return false
currContext()?.let {
it.getSharedPreferences(it.getString(R.string.preference_file_key), Context.MODE_PRIVATE)
.edit()
.putString("anilist_username", user.name)
.apply()
}
Anilist.userid = user.id Anilist.userid = user.id
Anilist.username = user.name Anilist.username = user.name
Anilist.bg = user.bannerImage Anilist.bg = user.bannerImage
@@ -114,8 +123,12 @@ class AnilistQueries {
image = i.node?.image?.medium, image = i.node?.image?.medium,
banner = media.banner ?: media.cover, banner = media.banner ?: media.cover,
role = when (i.role.toString()) { role = when (i.role.toString()) {
"MAIN" -> currContext()?.getString(R.string.main_role) ?: "MAIN" "MAIN" -> currContext()?.getString(R.string.main_role)
"SUPPORTING" -> currContext()?.getString(R.string.supporting_role) ?: "SUPPORTING" ?: "MAIN"
"SUPPORTING" -> currContext()?.getString(R.string.supporting_role)
?: "SUPPORTING"
else -> i.role.toString() else -> i.role.toString()
} }
) )
@@ -129,11 +142,16 @@ class AnilistQueries {
val m = Media(mediaEdge) val m = Media(mediaEdge)
media.relations?.add(m) media.relations?.add(m)
if (m.relation == "SEQUEL") { if (m.relation == "SEQUEL") {
media.sequel = if ((media.sequel?.popularity ?: 0) < (m.popularity ?: 0)) m else media.sequel media.sequel =
if ((media.sequel?.popularity ?: 0) < (m.popularity
?: 0)
) m else media.sequel
} else if (m.relation == "PREQUEL") { } else if (m.relation == "PREQUEL") {
media.prequel = media.prequel =
if ((media.prequel?.popularity ?: 0) < (m.popularity ?: 0)) m else media.prequel if ((media.prequel?.popularity ?: 0) < (m.popularity
?: 0)
) m else media.prequel
} }
} }
media.relations?.sortByDescending { it.popularity } media.relations?.sortByDescending { it.popularity }
@@ -199,17 +217,19 @@ class AnilistQueries {
) )
} }
media.anime.nextAiringEpisodeTime = fetchedMedia.nextAiringEpisode?.airingAt?.toLong() media.anime.nextAiringEpisodeTime =
fetchedMedia.nextAiringEpisode?.airingAt?.toLong()
fetchedMedia.externalLinks?.forEach { i -> fetchedMedia.externalLinks?.forEach { i ->
when (i.site.lowercase()) { when (i.site.lowercase()) {
"youtube" -> media.anime.youtube = i.url "youtube" -> media.anime.youtube = i.url
"crunchyroll" -> media.crunchySlug = i.url?.split("/")?.getOrNull(3) "crunchyroll" -> media.crunchySlug =
i.url?.split("/")?.getOrNull(3)
"vrv" -> media.vrvId = i.url?.split("/")?.getOrNull(4) "vrv" -> media.vrvId = i.url?.split("/")?.getOrNull(4)
} }
} }
} } else if (media.manga != null) {
else if (media.manga != null) {
fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let { fetchedMedia.staff?.edges?.find { authorRoles.contains(it.role?.trim()) }?.node?.let {
media.manga.author = Author( media.manga.author = Author(
it.id.toString(), it.id.toString(),
@@ -228,9 +248,11 @@ class AnilistQueries {
else snackString(currContext()?.getString(R.string.what_did_you_open)) else snackString(currContext()?.getString(R.string.what_did_you_open))
} }
} else { } else {
if (currContext()?.let { isOnline(it) } == true) {
snackString(currContext()?.getString(R.string.error_getting_data)) snackString(currContext()?.getString(R.string.error_getting_data))
} }
} }
}
val mal = async { val mal = async {
if (media.idMAL != null) { if (media.idMAL != null) {
MalScraper.loadMedia(media) MalScraper.loadMedia(media)
@@ -361,7 +383,11 @@ class AnilistQueries {
return default return default
} }
suspend fun getMediaLists(anime: Boolean, userId: Int, sortOrder: String? = null): MutableMap<String, ArrayList<Media>> { suspend fun getMediaLists(
anime: Boolean,
userId: Int,
sortOrder: String? = null
): MutableMap<String, ArrayList<Media>> {
val response = val response =
executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: $userId, type: ${if (anime) "ANIME" else "MANGA"}) { lists { name isCustomList entries { status progress private score(format:POINT_100) updatedAt media { id idMal isAdult type status chapters episodes nextAiringEpisode {episode} bannerImage meanScore isFavourite format coverImage{large} startDate{year month day} title {english romaji userPreferred } } } } user { id mediaListOptions { rowOrder animeList { sectionOrder } mangaList { sectionOrder } } } } }""") executeQuery<Query.MediaListCollection>("""{ MediaListCollection(userId: $userId, type: ${if (anime) "ANIME" else "MANGA"}) { lists { name isCustomList entries { status progress private score(format:POINT_100) updatedAt media { id idMal isAdult type status chapters episodes nextAiringEpisode {episode} bannerImage meanScore isFavourite format coverImage{large} startDate{year month day} title {english romaji userPreferred } } } } user { id mediaListOptions { rowOrder animeList { sectionOrder } mangaList { sectionOrder } } } } }""")
val sorted = mutableMapOf<String, ArrayList<Media>>() val sorted = mutableMapOf<String, ArrayList<Media>>()
@@ -395,11 +421,19 @@ class AnilistQueries {
sorted["Favourites"]?.sortWith(compareBy { it.userFavOrder }) sorted["Favourites"]?.sortWith(compareBy { it.userFavOrder })
sorted["All"] = all sorted["All"] = all
val listsort = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
val sort = sortOrder ?: options?.rowOrder ?.getString("sort_order", "score")
val sort = listsort ?: sortOrder ?: options?.rowOrder
for (i in sorted.keys) { for (i in sorted.keys) {
when (sort) { when (sort) {
"score" -> sorted[i]?.sortWith { b, a -> compareValuesBy(a, b, { it.userScore }, { it.meanScore }) } "score" -> sorted[i]?.sortWith { b, a ->
compareValuesBy(
a,
b,
{ it.userScore },
{ it.meanScore })
}
"title" -> sorted[i]?.sortWith(compareBy { it.userPreferredName }) "title" -> sorted[i]?.sortWith(compareBy { it.userPreferredName })
"updatedAt" -> sorted[i]?.sortWith(compareByDescending { it.userUpdatedAt }) "updatedAt" -> sorted[i]?.sortWith(compareByDescending { it.userUpdatedAt })
"release" -> sorted[i]?.sortWith(compareByDescending { it.startDate }) "release" -> sorted[i]?.sortWith(compareByDescending { it.startDate })
@@ -564,13 +598,31 @@ query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult:
${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)
""","excludedGenres":[${excludedGenres.joinToString { "\"${it.replace("Not ", "")}\"" }}]""" ""","excludedGenres":[${
excludedGenres.joinToString {
"\"${
it.replace(
"Not ",
""
)
}\""
}
}]"""
else "" else ""
} }
${if (tags?.isNotEmpty() == true) ""","tags":[${tags.joinToString { "\"$it\"" }}]""" else ""} ${if (tags?.isNotEmpty() == true) ""","tags":[${tags.joinToString { "\"$it\"" }}]""" else ""}
${ ${
if (excludedTags?.isNotEmpty() == true) if (excludedTags?.isNotEmpty() == true)
""","excludedTags":[${excludedTags.joinToString { "\"${it.replace("Not ", "")}\"" }}]""" ""","excludedTags":[${
excludedTags.joinToString {
"\"${
it.replace(
"Not ",
""
)
}\""
}
}]"""
else "" else ""
} }
}""".replace("\n", " ").replace(""" """, "") }""".replace("\n", " ").replace(""" """, "")
@@ -822,7 +874,8 @@ Page(page:$page,perPage:50) {
var page = 0 var page = 0
while (hasNextPage) { while (hasNextPage) {
page++ page++
hasNextPage = executeQuery<Query.Studio>(query(page), force = true)?.data?.studio?.media?.let { hasNextPage =
executeQuery<Query.Studio>(query(page), force = true)?.data?.studio?.media?.let {
it.edges?.forEach { i -> it.edges?.forEach { i ->
i.node?.apply { i.node?.apply {
val status = status.toString() val status = status.toString()
@@ -896,7 +949,10 @@ Page(page:$page,perPage:50) {
while (hasNextPage) { while (hasNextPage) {
page++ page++
hasNextPage = executeQuery<Query.Author>(query(page), force = true)?.data?.author?.staffMedia?.let { hasNextPage = executeQuery<Query.Author>(
query(page),
force = true
)?.data?.author?.staffMedia?.let {
it.edges?.forEach { i -> it.edges?.forEach { i ->
i.node?.apply { i.node?.apply {
val status = status.toString() val status = status.toString()

View File

@@ -7,8 +7,8 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.discord.Discord import ani.dantotsu.connections.discord.Discord
import ani.dantotsu.loadData
import ani.dantotsu.connections.mal.MAL import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.loadData
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.others.AppUpdater import ani.dantotsu.others.AppUpdater
import ani.dantotsu.snackString import ani.dantotsu.snackString
@@ -19,9 +19,16 @@ import kotlinx.coroutines.launch
suspend fun getUserId(context: Context, block: () -> Unit) { suspend fun getUserId(context: Context, block: () -> Unit) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
if (Discord.userid == null && Discord.token != null) { val sharedPref = context.getSharedPreferences(
if (!Discord.getUserData()) context.getString(R.string.preference_file_key),
snackString(context.getString(R.string.error_loading_discord_user_data)) Context.MODE_PRIVATE
)
val token = sharedPref.getString("discord_token", null)
val userid = sharedPref.getString("discord_id", null)
if (userid == null && token != null) {
/*if (!Discord.getUserData())
snackString(context.getString(R.string.error_loading_discord_user_data))*/
//TODO: Discord.getUserData()
} }
} }
@@ -42,35 +49,53 @@ suspend fun getUserId(context: Context, block: () -> Unit) {
} }
class AnilistHomeViewModel : ViewModel() { class AnilistHomeViewModel : ViewModel() {
private val listImages: MutableLiveData<ArrayList<String?>> = MutableLiveData<ArrayList<String?>>(arrayListOf()) private val listImages: MutableLiveData<ArrayList<String?>> =
MutableLiveData<ArrayList<String?>>(arrayListOf())
fun getListImages(): LiveData<ArrayList<String?>> = listImages fun getListImages(): LiveData<ArrayList<String?>> = listImages
suspend fun setListImages() = listImages.postValue(Anilist.query.getBannerImages()) suspend fun setListImages() = listImages.postValue(Anilist.query.getBannerImages())
private val animeContinue: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null) private val animeContinue: MutableLiveData<ArrayList<Media>> =
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")) suspend fun setAnimeContinue() = animeContinue.postValue(Anilist.query.continueMedia("ANIME"))
private val animeFav: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null) private val animeFav: MutableLiveData<ArrayList<Media>> =
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)) suspend fun setAnimeFav() = animeFav.postValue(Anilist.query.favMedia(true))
private val animePlanned: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null) private val animePlanned: MutableLiveData<ArrayList<Media>> =
fun getAnimePlanned(): LiveData<ArrayList<Media>> = animePlanned MutableLiveData<ArrayList<Media>>(null)
suspend fun setAnimePlanned() = animePlanned.postValue(Anilist.query.continueMedia("ANIME", true))
fun getAnimePlanned(): LiveData<ArrayList<Media>> = animePlanned
suspend fun setAnimePlanned() =
animePlanned.postValue(Anilist.query.continueMedia("ANIME", true))
private val mangaContinue: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
private val mangaContinue: MutableLiveData<ArrayList<Media>> = 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")) suspend fun setMangaContinue() = mangaContinue.postValue(Anilist.query.continueMedia("MANGA"))
private val mangaFav: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null) private val mangaFav: MutableLiveData<ArrayList<Media>> =
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)) suspend fun setMangaFav() = mangaFav.postValue(Anilist.query.favMedia(false))
private val mangaPlanned: MutableLiveData<ArrayList<Media>> = MutableLiveData<ArrayList<Media>>(null) private val mangaPlanned: MutableLiveData<ArrayList<Media>> =
fun getMangaPlanned(): LiveData<ArrayList<Media>> = mangaPlanned MutableLiveData<ArrayList<Media>>(null)
suspend fun setMangaPlanned() = mangaPlanned.postValue(Anilist.query.continueMedia("MANGA", true))
fun getMangaPlanned(): LiveData<ArrayList<Media>> = mangaPlanned
suspend fun setMangaPlanned() =
mangaPlanned.postValue(Anilist.query.continueMedia("MANGA", true))
private val recommendation: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
private val recommendation: MutableLiveData<ArrayList<Media>> = 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 setRecommendation() = recommendation.postValue(Anilist.query.recommendations())
@@ -93,7 +118,9 @@ class AnilistAnimeViewModel : ViewModel() {
var notSet = true var notSet = true
lateinit var searchResults: SearchResults lateinit var searchResults: SearchResults
private val type = "ANIME" private val type = "ANIME"
private val trending: MutableLiveData<MutableList<Media>> = MutableLiveData<MutableList<Media>>(null) private val trending: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getTrending(): LiveData<MutableList<Media>> = trending fun getTrending(): LiveData<MutableList<Media>> = trending
suspend fun loadTrending(i: Int) { suspend fun loadTrending(i: Int) {
val (season, year) = Anilist.currentSeasons[i] val (season, year) = Anilist.currentSeasons[i]
@@ -109,7 +136,9 @@ class AnilistAnimeViewModel : ViewModel() {
) )
} }
private val updated: MutableLiveData<MutableList<Media>> = MutableLiveData<MutableList<Media>>(null) private val updated: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getUpdated(): LiveData<MutableList<Media>> = updated fun getUpdated(): LiveData<MutableList<Media>> = updated
suspend fun loadUpdated() = updated.postValue(Anilist.query.recentlyUpdated()) suspend fun loadUpdated() = updated.postValue(Anilist.query.recentlyUpdated())
@@ -157,15 +186,33 @@ class AnilistMangaViewModel : ViewModel() {
var notSet = true var notSet = true
lateinit var searchResults: SearchResults lateinit var searchResults: SearchResults
private val type = "MANGA" private val type = "MANGA"
private val trending: MutableLiveData<MutableList<Media>> = MutableLiveData<MutableList<Media>>(null) private val trending: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getTrending(): LiveData<MutableList<Media>> = trending fun getTrending(): LiveData<MutableList<Media>> = trending
suspend fun loadTrending() = suspend fun loadTrending() =
trending.postValue(Anilist.query.search(type, perPage = 10, sort = Anilist.sortBy[2], hd = true)?.results) trending.postValue(
Anilist.query.search(
type,
perPage = 10,
sort = Anilist.sortBy[2],
hd = true
)?.results
)
private val updated: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
private val updated: MutableLiveData<MutableList<Media>> = MutableLiveData<MutableList<Media>>(null)
fun getTrendingNovel(): LiveData<MutableList<Media>> = updated fun getTrendingNovel(): LiveData<MutableList<Media>> = updated
suspend fun loadTrendingNovel() = suspend fun loadTrendingNovel() =
updated.postValue(Anilist.query.search(type, perPage = 10, sort = Anilist.sortBy[2], format = "NOVEL")?.results) 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

View File

@@ -6,15 +6,20 @@ import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import ani.dantotsu.logError import ani.dantotsu.logError
import ani.dantotsu.logger import ani.dantotsu.logger
import ani.dantotsu.others.LangSet
import ani.dantotsu.startMainActivity import ani.dantotsu.startMainActivity
import ani.dantotsu.themes.ThemeManager
class Login : AppCompatActivity() { class Login : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
val data: Uri? = intent?.data val data: Uri? = intent?.data
logger(data.toString()) logger(data.toString())
try { try {
Anilist.token = Regex("""(?<=access_token=).+(?=&token_type)""").find(data.toString())!!.value Anilist.token =
Regex("""(?<=access_token=).+(?=&token_type)""").find(data.toString())!!.value
val filename = "anilistToken" val filename = "anilistToken"
this.openFileOutput(filename, Context.MODE_PRIVATE).use { this.openFileOutput(filename, Context.MODE_PRIVATE).use {
it.write(Anilist.token!!.toByteArray()) it.write(Anilist.token!!.toByteArray())

View File

@@ -27,7 +27,15 @@ data class SearchResults(
val list = mutableListOf<SearchChip>() val list = mutableListOf<SearchChip>()
sort?.let { sort?.let {
val c = currContext()!! val c = currContext()!!
list.add(SearchChip("SORT", c.getString(R.string.filter_sort, c.resources.getStringArray(R.array.sort_by)[Anilist.sortBy.indexOf(it)]))) list.add(
SearchChip(
"SORT",
c.getString(
R.string.filter_sort,
c.resources.getStringArray(R.array.sort_by)[Anilist.sortBy.indexOf(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)))
@@ -42,13 +50,23 @@ data class SearchResults(
list.add(SearchChip("GENRE", it)) list.add(SearchChip("GENRE", it))
} }
excludedGenres?.forEach { excludedGenres?.forEach {
list.add(SearchChip("EXCLUDED_GENRE", currContext()!!.getString(R.string.filter_exclude, it))) list.add(
SearchChip(
"EXCLUDED_GENRE",
currContext()!!.getString(R.string.filter_exclude, it)
)
)
} }
tags?.forEach { tags?.forEach {
list.add(SearchChip("TAG", it)) list.add(SearchChip("TAG", it))
} }
excludedTags?.forEach { excludedTags?.forEach {
list.add(SearchChip("EXCLUDED_TAG", currContext()!!.getString(R.string.filter_exclude, it))) list.add(
SearchChip(
"EXCLUDED_TAG",
currContext()!!.getString(R.string.filter_exclude, it)
)
)
} }
return list return list
} }

View File

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

View File

@@ -15,6 +15,7 @@ class Query{
val user: ani.dantotsu.connections.anilist.api.User? val user: ani.dantotsu.connections.anilist.api.User?
) )
} }
@Serializable @Serializable
data class Media( data class Media(
@SerialName("data") @SerialName("data")

View File

@@ -3,7 +3,7 @@ package ani.dantotsu.connections.anilist.api
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import java.io.Serializable import java.io.Serializable
import java.text.DateFormatSymbols import java.text.DateFormatSymbols
import java.util.* import java.util.Calendar
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
data class FuzzyDate( data class FuzzyDate(
@@ -16,9 +16,11 @@ data class FuzzyDate(
fun isEmpty(): Boolean { fun isEmpty(): Boolean {
return year == null && month == null && day == null return year == null && month == null && day == null
} }
override fun toString(): String { override fun toString(): String {
return if (isEmpty()) "??" else toStringOrEmpty() return if (isEmpty()) "??" else toStringOrEmpty()
} }
fun toStringOrEmpty(): String { fun toStringOrEmpty(): String {
return listOfNotNull( return listOfNotNull(
day?.toString(), day?.toString(),
@@ -29,7 +31,11 @@ data class FuzzyDate(
fun getToday(): FuzzyDate { fun getToday(): FuzzyDate {
val cal = Calendar.getInstance() val cal = Calendar.getInstance()
return FuzzyDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH)) return FuzzyDate(
cal.get(Calendar.YEAR),
cal.get(Calendar.MONTH) + 1,
cal.get(Calendar.DAY_OF_MONTH)
)
} }
fun toVariableString(): String { fun toVariableString(): String {
@@ -39,6 +45,7 @@ data class FuzzyDate(
day?.let { "day:$it" } day?.let { "day:$it" }
).joinToString(",", "{", "}") ).joinToString(",", "{", "}")
} }
fun toMALString(): String { fun toMALString(): String {
val padding = '0' val padding = '0'
val values = listOf( val values = listOf(

View File

@@ -2,6 +2,7 @@ package ani.dantotsu.connections.anilist.api
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Recommendation( data class Recommendation(
// The id of the recommendation // The id of the recommendation
@@ -22,6 +23,7 @@ data class Recommendation(
// The user that first created the recommendation // The user that first created the recommendation
@SerialName("user") var user: User?, @SerialName("user") var user: User?,
) )
@Serializable @Serializable
data class RecommendationConnection( data class RecommendationConnection(
//@SerialName("edges") var edges: List<RecommendationEdge>?, //@SerialName("edges") var edges: List<RecommendationEdge>?,

View File

@@ -5,14 +5,11 @@ import android.content.Intent
import android.widget.TextView import android.widget.TextView
import androidx.core.content.edit import androidx.core.content.edit
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.discord.serializers.User
import ani.dantotsu.others.CustomBottomDialog import ani.dantotsu.others.CustomBottomDialog
import ani.dantotsu.toast import ani.dantotsu.toast
import ani.dantotsu.tryWith import ani.dantotsu.tryWith
import ani.dantotsu.tryWithSuspend
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin import io.noties.markwon.SoftBreakAddsNewLinePlugin
import kotlinx.coroutines.Dispatchers
import java.io.File import java.io.File
object Discord { object Discord {
@@ -21,7 +18,7 @@ object Discord {
var userid: String? = null var userid: String? = null
var avatar: String? = null var avatar: String? = null
private const val TOKEN = "discord_token" const val TOKEN = "discord_token"
fun getSavedToken(context: Context): Boolean { fun getSavedToken(context: Context): Boolean {
val sharedPref = context.getSharedPreferences( val sharedPref = context.getSharedPreferences(
@@ -61,16 +58,6 @@ object Discord {
} }
private var rpc: RPC? = null private var rpc: RPC? = null
suspend fun getUserData() = tryWithSuspend(true) {
if(rpc==null) {
val rpc = RPC(token!!, Dispatchers.IO).also { rpc = it }
val user: User = rpc.getUserData()
userid = user.username
avatar = user.userAvatar()
rpc.close()
true
} else true
} ?: false
fun warning(context: Context) = CustomBottomDialog().apply { fun warning(context: Context) = CustomBottomDialog().apply {
@@ -97,16 +84,21 @@ object Discord {
context.startActivity(intent) context.startActivity(intent)
} }
fun defaultRPC(): RPC? { const val application_Id = "1163925779692912771"
const val small_Image: String =
"mp:attachments/1167176318266380288/1176997397797277856/logo-best_of_both.png"
/*fun defaultRPC(): RPC? {
return token?.let { return token?.let {
RPC(it, Dispatchers.IO).apply { RPC(it, Dispatchers.IO).apply {
applicationId = "1163925779692912771" applicationId = application_Id
smallImage = RPC.Link( smallImage = RPC.Link(
"Dantotsu", "Dantotsu",
"mp:attachments/1163940221063278672/1163940262423298141/bitmap1024.png" small_Image
) )
buttons.add(RPC.Link("Stream on Dantotsu", "https://github.com/rebelonion/Dantotsu/")) buttons.add(RPC.Link("Stream on Dantotsu", "https://github.com/rebelonion/Dantotsu/"))
} }
} }
} }*/
} }

View File

@@ -0,0 +1,475 @@
package ani.dantotsu.connections.discord
import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.os.IBinder
import android.os.PowerManager
import android.provider.MediaStore
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import ani.dantotsu.MainActivity
import ani.dantotsu.R
import ani.dantotsu.connections.discord.serializers.Presence
import ani.dantotsu.connections.discord.serializers.User
import ani.dantotsu.isOnline
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import java.io.File
import java.io.OutputStreamWriter
class DiscordService : Service() {
private var heartbeat: Int = 0
private var sequence: Int? = null
private var sessionId: String = ""
private var resume = false
private lateinit var logFile: File
private lateinit var webSocket: WebSocket
private lateinit var heartbeatThread: Thread
private lateinit var client: OkHttpClient
private lateinit var wakeLock: PowerManager.WakeLock
var presenceStore = ""
val json = Json {
encodeDefaults = true
allowStructuredMapKeys = true
ignoreUnknownKeys = true
coerceInputValues = true
}
var log = ""
override fun onCreate() {
super.onCreate()
log("Service onCreate()")
val powerManager = baseContext.getSystemService(Context.POWER_SERVICE) as PowerManager
wakeLock = powerManager.newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK,
"discordRPC:backgroundPresence"
)
wakeLock.acquire()
log("WakeLock Acquired")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel(
"discordPresence",
"Discord Presence Service Channel",
NotificationManager.IMPORTANCE_LOW
)
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(serviceChannel)
}
val intent = Intent(this, MainActivity::class.java).apply {
action = Intent.ACTION_MAIN
addCategory(Intent.CATEGORY_LAUNCHER)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
val pendingIntent =
PendingIntent.getActivity(applicationContext, 0, intent, PendingIntent.FLAG_IMMUTABLE)
val builder = NotificationCompat.Builder(this, "discordPresence")
.setSmallIcon(R.mipmap.ic_launcher_round)
.setContentTitle("Discord Presence")
.setContentText("Running in the background")
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_LOW)
startForeground(1, builder.build())
log("Foreground service started, notification shown")
client = OkHttpClient()
client.newWebSocket(
Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json").build(),
DiscordWebSocketListener()
)
client.dispatcher.executorService.shutdown()
SERVICE_RUNNING = true
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
log("Service onStartCommand()")
if (intent != null) {
if (intent.hasExtra("presence")) {
log("Service onStartCommand() setPresence")
val lPresence = intent.getStringExtra("presence")
if (this::webSocket.isInitialized) webSocket.send(lPresence!!)
presenceStore = lPresence!!
} else {
log("Service onStartCommand() no presence")
DiscordServiceRunningSingleton.running = false
//kill the client
client = OkHttpClient()
stopSelf()
}
}
return START_REDELIVER_INTENT
}
override fun onDestroy() {
log("Service Destroyed")
if (DiscordServiceRunningSingleton.running) {
log("Accidental Service Destruction, restarting service")
val intent = Intent(baseContext, DiscordService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
baseContext.startForegroundService(intent)
} else {
baseContext.startService(intent)
}
} else {
if (this::webSocket.isInitialized)
setPresence(
json.encodeToString(
Presence.Response(
3,
Presence(status = "offline")
)
)
)
wakeLock.release()
}
SERVICE_RUNNING = false
client = OkHttpClient()
if (this::webSocket.isInitialized) webSocket.close(1000, "Closed by user")
super.onDestroy()
//saveLogToFile()
}
fun saveProfile(response: String) {
val sharedPref = baseContext.getSharedPreferences(
baseContext.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
val user = json.decodeFromString<User.Response>(response).d.user
log("User data: $user")
with(sharedPref.edit()) {
putString("discord_username", user.username)
putString("discord_id", user.id)
putString("discord_avatar", user.avatar)
apply()
}
}
override fun onBind(p0: Intent?): IBinder? = null
inner class DiscordWebSocketListener : WebSocketListener() {
var retryAttempts = 0
val maxRetryAttempts = 10
override fun onOpen(webSocket: WebSocket, response: Response) {
super.onOpen(webSocket, response)
this@DiscordService.webSocket = webSocket
log("WebSocket: Opened")
}
override fun onMessage(webSocket: WebSocket, text: String) {
super.onMessage(webSocket, text)
val json = JsonParser.parseString(text).asJsonObject
log("WebSocket: Received op code ${json.get("op")}")
when (json.get("op").asInt) {
0 -> {
if (json.has("s")) {
log("WebSocket: Sequence ${json.get("s")} Received")
sequence = json.get("s").asInt
}
if (json.get("t").asString != "READY") return
saveProfile(text)
log(text)
sessionId = json.get("d").asJsonObject.get("session_id").asString
log("WebSocket: SessionID ${json.get("d").asJsonObject.get("session_id")} Received")
if (presenceStore.isNotEmpty()) setPresence(presenceStore)
sendBroadcast(Intent("ServiceToConnectButton"))
}
1 -> {
log("WebSocket: Received Heartbeat request, sending heartbeat")
heartbeatThread.interrupt()
heartbeatSend(webSocket, sequence)
heartbeatThread = Thread(HeartbeatRunnable())
heartbeatThread.start()
}
7 -> {
resume = true
log("WebSocket: Requested to Restart, restarting")
webSocket.close(1000, "Requested to Restart by the server")
client = OkHttpClient()
client.newWebSocket(
Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json")
.build(),
DiscordWebSocketListener()
)
client.dispatcher.executorService.shutdown()
}
9 -> {
log("WebSocket: Invalid Session, restarting")
webSocket.close(1000, "Invalid Session")
Thread.sleep(5000)
client = OkHttpClient()
client.newWebSocket(
Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json")
.build(),
DiscordWebSocketListener()
)
client.dispatcher.executorService.shutdown()
}
10 -> {
heartbeat = json.get("d").asJsonObject.get("heartbeat_interval").asInt
heartbeatThread = Thread(HeartbeatRunnable())
heartbeatThread.start()
if (resume) {
log("WebSocket: Resuming because server requested")
resume()
resume = false
} else {
identify(webSocket, baseContext)
log("WebSocket: Identified")
}
}
11 -> {
log("WebSocket: Heartbeat ACKed")
heartbeatThread = Thread(HeartbeatRunnable())
heartbeatThread.start()
}
}
}
fun identify(webSocket: WebSocket, context: Context) {
val properties = JsonObject()
properties.addProperty("os", "linux")
properties.addProperty("browser", "unknown")
properties.addProperty("device", "unknown")
val d = JsonObject()
d.addProperty("token", getToken(context))
d.addProperty("intents", 0)
d.add("properties", properties)
val payload = JsonObject()
payload.addProperty("op", 2)
payload.add("d", d)
webSocket.send(payload.toString())
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
super.onFailure(webSocket, t, response)
if (!isOnline(baseContext)) {
log("WebSocket: Error, onFailure() reason: No Internet")
errorNotification("Could not set the presence", "No Internet")
return
} else {
retryAttempts++
if (retryAttempts >= maxRetryAttempts) {
log("WebSocket: Error, onFailure() reason: Max Retry Attempts")
errorNotification("Could not set the presence", "Max Retry Attempts")
return
}
}
t.message?.let { Log.d("WebSocket", "onFailure() $it") }
log("WebSocket: Error, onFailure() reason: ${t.message}")
client = OkHttpClient()
client.newWebSocket(
Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json").build(),
DiscordWebSocketListener()
)
client.dispatcher.executorService.shutdown()
if (::heartbeatThread.isInitialized && !heartbeatThread.isInterrupted) {
heartbeatThread.interrupt()
}
}
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
super.onClosing(webSocket, code, reason)
Log.d("WebSocket", "onClosing() $code $reason")
if (::heartbeatThread.isInitialized && !heartbeatThread.isInterrupted) {
heartbeatThread.interrupt()
}
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
super.onClosed(webSocket, code, reason)
Log.d("WebSocket", "onClosed() $code $reason")
if (code >= 4000) {
log("WebSocket: Error, code: $code reason: $reason")
client = OkHttpClient()
client.newWebSocket(
Request.Builder().url("wss://gateway.discord.gg/?v=10&encoding=json").build(),
DiscordWebSocketListener()
)
client.dispatcher.executorService.shutdown()
return
}
}
}
fun getToken(context: Context): String {
val sharedPref = context.getSharedPreferences(
context.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
val token = sharedPref.getString(Discord.TOKEN, null)
if (token == null) {
log("WebSocket: Token not found")
errorNotification("Could not set the presence", "token not found")
return ""
} else {
return token
}
}
fun heartbeatSend(webSocket: WebSocket, seq: Int?) {
val json = JsonObject()
json.addProperty("op", 1)
json.addProperty("d", seq)
webSocket.send(json.toString())
}
private fun errorNotification(title: String, text: String) {
val intent = Intent(this@DiscordService, MainActivity::class.java).apply {
action = Intent.ACTION_MAIN
addCategory(Intent.CATEGORY_LAUNCHER)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
val pendingIntent =
PendingIntent.getActivity(applicationContext, 0, intent, PendingIntent.FLAG_IMMUTABLE)
val builder = NotificationCompat.Builder(this@DiscordService, "discordPresence")
.setSmallIcon(R.mipmap.ic_launcher_round)
.setContentTitle(title)
.setContentText(text)
.setContentIntent(pendingIntent)
.setPriority(NotificationCompat.PRIORITY_HIGH)
val notificationManager = NotificationManagerCompat.from(applicationContext)
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
//TODO: Request permission
return
}
notificationManager.notify(2, builder.build())
log("Error Notified")
}
fun saveSimpleTestPresence() {
val file = File(baseContext.cacheDir, "payload")
//fill with test payload
val payload = JsonObject()
payload.addProperty("op", 3)
payload.add("d", JsonObject().apply {
addProperty("status", "online")
addProperty("afk", false)
add("activities", JsonArray().apply {
add(JsonObject().apply {
addProperty("name", "Test")
addProperty("type", 0)
})
})
})
file.writeText(payload.toString())
log("WebSocket: Simple Test Presence Saved")
}
fun setPresence(String: String) {
log("WebSocket: Sending Presence payload")
log(String)
webSocket.send(String)
}
fun log(string: String) {
Log.d("WebSocket_Discord", string)
//log += "${SimpleDateFormat("HH:mm:ss").format(Calendar.getInstance().time)} $string\n"
}
fun saveLogToFile() {
val fileName = "log_${System.currentTimeMillis()}.txt"
// ContentValues to store file metadata
val values = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.MediaColumns.RELATIVE_PATH, "Download/")
}
}
// Inserting the file in the MediaStore
val resolver = baseContext.contentResolver
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
} else {
val directory =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val file = File(directory, fileName)
// Make sure the Downloads directory exists
if (!directory.exists()) {
directory.mkdirs()
}
// Use FileProvider to get the URI for the file
val authority =
"${baseContext.packageName}.provider" // Adjust with your app's package name
Uri.fromFile(file)
}
// Writing to the file
uri?.let {
resolver.openOutputStream(it).use { outputStream ->
OutputStreamWriter(outputStream).use { writer ->
writer.write(log)
}
}
} ?: run {
log("Error saving log file")
}
}
fun resume() {
log("Sending Resume payload")
val d = JsonObject()
d.addProperty("token", getToken(baseContext))
d.addProperty("session_id", sessionId)
d.addProperty("seq", sequence)
val json = JsonObject()
json.addProperty("op", 6)
json.add("d", d)
log(json.toString())
webSocket.send(json.toString())
}
inner class HeartbeatRunnable : Runnable {
override fun run() {
try {
Thread.sleep(heartbeat.toLong())
heartbeatSend(webSocket, sequence)
log("WebSocket: Heartbeat Sent")
} catch (e: InterruptedException) {
}
}
}
companion object {
var SERVICE_RUNNING = false
}
}
object DiscordServiceRunningSingleton {
var running = false
}

View File

@@ -4,19 +4,24 @@ import android.annotation.SuppressLint
import android.app.Application.getProcessName import android.app.Application.getProcessName
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.webkit.WebResourceRequest
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import androidx.annotation.RequiresApi import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.discord.Discord.saveToken import ani.dantotsu.connections.discord.Discord.saveToken
import ani.dantotsu.others.LangSet
import ani.dantotsu.startMainActivity import ani.dantotsu.startMainActivity
import ani.dantotsu.themes.ThemeManager
class Login : AppCompatActivity() { class Login : AppCompatActivity() {
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val process = getProcessName() val process = getProcessName()
if (packageName != process) WebView.setDataDirectorySuffix(process) if (packageName != process) WebView.setDataDirectorySuffix(process)
@@ -30,28 +35,46 @@ class Login : AppCompatActivity() {
settings.databaseEnabled = true settings.databaseEnabled = true
settings.domStorageEnabled = true settings.domStorageEnabled = true
} }
WebView.setWebContentsDebuggingEnabled(true)
webView.webViewClient = object : WebViewClient() { webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) { override fun shouldOverrideUrlLoading(
if (url != null && url.endsWith("/app")) { view: WebView?,
webView.stopLoading() request: WebResourceRequest?
webView.evaluateJavascript(""" ): Boolean {
// Check if the URL is the one expected after a successful login
if (request?.url.toString() != "https://discord.com/login") {
// Delay the script execution to ensure the page is fully loaded
view?.postDelayed({
view.evaluateJavascript(
"""
(function() { (function() {
const wreq = webpackChunkdiscord_app.push([[Symbol()], {}, w => w]) const wreq = (webpackChunkdiscord_app.push([[''],{},e=>{m=[];for(let c in e.c)m.push(e.c[c])}]),m).find(m=>m?.exports?.default?.getToken!==void 0).exports.default.getToken();
webpackChunkdiscord_app.pop() return wreq;
const token = Object.values(wreq.c).find(m => m.exports?.Z?.getToken).exports.Z.getToken();
return token;
})() })()
""".trimIndent()){ """.trimIndent()
login(it.trim('"')) ) { result ->
} login(result.trim('"'))
} }
}, 2000)
}
return super.shouldOverrideUrlLoading(view, request)
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
} }
} }
webView.loadUrl("https://discord.com/login") webView.loadUrl("https://discord.com/login")
} }
private fun login(token: String) { private fun login(token: String) {
if (token.isEmpty() || token == "null") {
Toast.makeText(this, "Failed to retrieve token", Toast.LENGTH_SHORT).show()
finish()
return
}
Toast.makeText(this, "Logged in successfully", Toast.LENGTH_SHORT).show()
finish() finish()
saveToken(this, token) saveToken(this, token)
startMainActivity(this@Login) startMainActivity(this@Login)

View File

@@ -1,24 +1,10 @@
package ani.dantotsu.connections.discord package ani.dantotsu.connections.discord
import ani.dantotsu.connections.discord.serializers.* import ani.dantotsu.connections.discord.serializers.Activity
import kotlinx.coroutines.CoroutineScope import ani.dantotsu.connections.discord.serializers.Presence
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import java.util.concurrent.TimeUnit.*
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import ani.dantotsu.client as app import ani.dantotsu.client as app
@@ -31,72 +17,30 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
ignoreUnknownKeys = true ignoreUnknownKeys = true
} }
private val client = OkHttpClient.Builder()
.connectTimeout(10, SECONDS)
.readTimeout(10, SECONDS)
.writeTimeout(10, SECONDS)
.build()
private val request = Request.Builder()
.url("wss://gateway.discord.gg/?encoding=json&v=10")
.build()
private var webSocket = client.newWebSocket(request, Listener())
var applicationId: String? = null
var type: Type? = null
var activityName: String? = null
var details: String? = null
var state: String? = null
var largeImage: Link? = null
var smallImage: Link? = null
var status: String? = null
var startTimestamp: Long? = null
var stopTimestamp: Long? = null
enum class Type { enum class Type {
PLAYING, STREAMING, LISTENING, WATCHING, COMPETING PLAYING, STREAMING, LISTENING, WATCHING, COMPETING
} }
var buttons = mutableListOf<Link>()
data class Link(val label: String, val url: String) data class Link(val label: String, val url: String)
private suspend fun createPresence(): String { companion object {
return json.encodeToString(Presence.Response( data class RPCData(
3, val applicationId: String? = null,
Presence( val type: Type? = null,
activities = listOf( val activityName: String? = null,
Activity( val details: String? = null,
name = activityName, val state: String? = null,
state = state, val largeImage: Link? = null,
details = details, val smallImage: Link? = null,
type = type?.ordinal, val status: String? = null,
timestamps = if (startTimestamp != null) val startTimestamp: Long? = null,
Activity.Timestamps(startTimestamp, stopTimestamp) val stopTimestamp: Long? = null,
else null, val buttons: MutableList<Link> = mutableListOf()
assets = Activity.Assets(
largeImage = largeImage?.url?.discordUrl(),
largeText = largeImage?.label,
smallImage = smallImage?.url?.discordUrl(),
smallText = smallImage?.label
),
buttons = buttons.map { it.label },
metadata = Activity.Metadata(
buttonUrls = buttons.map { it.url }
),
applicationId = applicationId,
) )
),
afk = true,
since = startTimestamp,
status = status
)
))
}
@Serializable @Serializable
data class KizzyApi(val id: String) data class KizzyApi(val id: String)
val api = "https://kizzy-api.vercel.app/image?url=" val api = "https://kizzy-api.vercel.app/image?url="
private suspend fun String.discordUrl(): String? { private suspend fun String.discordUrl(): String? {
if (startsWith("mp:")) return this if (startsWith("mp:")) return this
@@ -104,132 +48,42 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
return json?.id return json?.id
} }
private fun sendIdentify() { suspend fun createPresence(data: RPCData): String {
val response = Identity.Response( val json = Json {
op = 2, encodeDefaults = true
d = Identity( allowStructuredMapKeys = true
token = token, ignoreUnknownKeys = true
properties = Identity.Properties(
os = "windows",
browser = "Chrome",
device = "disco"
),
compress = false,
intents = 0
)
)
webSocket.send(json.encodeToString(response))
} }
return json.encodeToString(Presence.Response(
fun send(block: RPC.() -> Unit) {
block.invoke(this)
send()
}
var started = false
var whenStarted: ((User) -> Unit)? = null
fun send() {
val send = {
CoroutineScope(coroutineContext).launch {
webSocket.send(createPresence())
}
}
if (!started) whenStarted = {
send.invoke()
whenStarted = null
}
else send.invoke()
}
fun close() {
webSocket.send(
json.encodeToString(
Presence.Response(
3, 3,
Presence(status = "offline") Presence(
activities = listOf(
Activity(
name = data.activityName,
state = data.state,
details = data.details,
type = data.type?.ordinal,
timestamps = if (data.startTimestamp != null)
Activity.Timestamps(data.startTimestamp, data.stopTimestamp)
else null,
assets = Activity.Assets(
largeImage = data.largeImage?.url?.discordUrl(),
largeText = data.largeImage?.label,
smallImage = data.smallImage?.url?.discordUrl(),
smallText = data.smallImage?.label
),
buttons = data.buttons.map { it.label },
metadata = Activity.Metadata(
buttonUrls = data.buttons.map { it.url }
),
applicationId = data.applicationId,
) )
),
afk = true,
since = data.startTimestamp,
status = data.status
) )
) ))
webSocket.close(4000, "Interrupt")
}
//I hate this, but couldn't find any better way to solve it
suspend fun getUserData(): User {
var user : User? = null
whenStarted = {
user = it
whenStarted = null
}
while (user == null) {
delay(100)
}
return user!!
}
var onReceiveUserData: ((User) -> Deferred<Unit>)? = null
inner class Listener : WebSocketListener() {
private var seq: Int? = null
private var heartbeatInterval: Long? = null
var scope = CoroutineScope(coroutineContext)
private fun sendHeartBeat() {
scope.cancel()
scope = CoroutineScope(coroutineContext)
scope.launch {
delay(heartbeatInterval!!)
webSocket.send("{\"op\":1, \"d\":$seq}")
}
}
override fun onMessage(webSocket: WebSocket, text: String) {
println("Message : $text")
val map = json.decodeFromString<Res>(text)
seq = map.s
when (map.op) {
10 -> {
map.d as JsonObject
heartbeatInterval = map.d["heartbeat_interval"]!!.jsonPrimitive.long
sendHeartBeat()
sendIdentify()
}
0 -> if (map.t == "READY") {
val user = json.decodeFromString<User.Response>(text).d.user
started = true
whenStarted?.invoke(user)
}
1 -> {
if (scope.isActive) scope.cancel()
webSocket.send("{\"op\":1, \"d\":$seq}")
}
11 -> sendHeartBeat()
7 -> webSocket.close(400, "Reconnect")
9 -> {
sendHeartBeat()
sendIdentify()
}
}
}
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
println("Server Closed : $code $reason")
if (code == 4000) {
scope.cancel()
}
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
println("Failure : ${t.message}")
if (t.message != "Interrupt") {
this@RPC.webSocket = client.newWebSocket(request, Listener())
}
} }
} }

View File

@@ -2,6 +2,7 @@ package ani.dantotsu.connections.discord.serializers
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class Activity( data class Activity(
@SerialName("application_id") @SerialName("application_id")

View File

@@ -1,60 +1,60 @@
package ani.dantotsu.connections.discord.serializers package ani.dantotsu.connections.discord.serializers
import kotlinx.serialization.* import kotlinx.serialization.SerialName
import kotlinx.serialization.descriptors.* import kotlinx.serialization.Serializable
import kotlinx.serialization.encoding.* import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.*
@Serializable @Serializable
data class User( data class User(
val verified: Boolean, val verified: Boolean? = null,
val username: String, val username: String,
@SerialName("purchased_flags") @SerialName("purchased_flags")
val purchasedFlags: Long, val purchasedFlags: Long? = null,
@SerialName("public_flags") @SerialName("public_flags")
val publicFlags: Long, val publicFlags: Long? = null,
val pronouns: String, val pronouns: String? = null,
@SerialName("premium_type") @SerialName("premium_type")
val premiumType: Long, val premiumType: Long? = null,
val premium: Boolean, val premium: Boolean? = null,
val phone: String, val phone: String? = null,
@SerialName("nsfw_allowed") @SerialName("nsfw_allowed")
val nsfwAllowed: Boolean, val nsfwAllowed: Boolean? = null,
val mobile: Boolean, val mobile: Boolean? = null,
@SerialName("mfa_enabled") @SerialName("mfa_enabled")
val mfaEnabled: Boolean, val mfaEnabled: Boolean? = null,
val id: String, val id: String,
@SerialName("global_name") @SerialName("global_name")
val globalName: String, val globalName: String? = null,
val flags: Long, val flags: Long? = null,
val email: String, val email: String? = null,
val discriminator: String, val discriminator: String? = null,
val desktop: Boolean, val desktop: Boolean? = null,
val bio: String, val bio: String? = null,
@SerialName("banner_color") @SerialName("banner_color")
val bannerColor: String, val bannerColor: String? = null,
val banner: JsonElement? = null, val banner: JsonElement? = null,
@SerialName("avatar_decoration") @SerialName("avatar_decoration")
val avatarDecoration: JsonElement? = null, val avatarDecoration: JsonElement? = null,
val avatar: String, val avatar: String? = null,
@SerialName("accent_color") @SerialName("accent_color")
val accentColor: Long val accentColor: Long? = null
) { ) {
@Serializable @Serializable
data class Response( data class Response(

View File

@@ -4,15 +4,25 @@ import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import ani.dantotsu.* import ani.dantotsu.R
import ani.dantotsu.client
import ani.dantotsu.connections.mal.MAL.clientId import ani.dantotsu.connections.mal.MAL.clientId
import ani.dantotsu.connections.mal.MAL.saveResponse import ani.dantotsu.connections.mal.MAL.saveResponse
import ani.dantotsu.loadData
import ani.dantotsu.logError
import ani.dantotsu.others.LangSet
import ani.dantotsu.snackString
import ani.dantotsu.startMainActivity
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.tryWithSuspend
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class Login : AppCompatActivity() { class Login : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
try { try {
val data: Uri = intent?.data val data: Uri = intent?.data
?: throw Exception(getString(R.string.mal_login_uri_not_found)) ?: throw Exception(getString(R.string.mal_login_uri_not_found))
@@ -42,8 +52,7 @@ class Login : AppCompatActivity() {
} }
} }
} }
} } catch (e: Exception) {
catch (e:Exception){
logError(e, snackbar = false) logError(e, snackbar = false)
startMainActivity(this) startMainActivity(this)
} }

View File

@@ -6,7 +6,13 @@ 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 androidx.fragment.app.FragmentActivity
import ani.dantotsu.* import ani.dantotsu.R
import ani.dantotsu.client
import ani.dantotsu.currContext
import ani.dantotsu.loadData
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.saveData
import ani.dantotsu.tryWithSuspend
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import java.io.File import java.io.File

View File

@@ -1,7 +1,7 @@
package ani.dantotsu.connections.mal package ani.dantotsu.connections.mal
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.client import ani.dantotsu.client
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.tryWithSuspend import ani.dantotsu.tryWithSuspend
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View File

@@ -0,0 +1,304 @@
package ani.dantotsu.download
import android.content.Context
import android.content.SharedPreferences
import android.os.Environment
import android.widget.Toast
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import java.io.File
import java.io.Serializable
class DownloadsManager(private val context: Context) {
private val prefs: SharedPreferences =
context.getSharedPreferences("downloads_pref", Context.MODE_PRIVATE)
private val gson = Gson()
private val downloadsList = loadDownloads().toMutableList()
val mangaDownloadedTypes: List<DownloadedType>
get() = downloadsList.filter { it.type == DownloadedType.Type.MANGA }
val animeDownloadedTypes: List<DownloadedType>
get() = downloadsList.filter { it.type == DownloadedType.Type.ANIME }
val novelDownloadedTypes: List<DownloadedType>
get() = downloadsList.filter { it.type == DownloadedType.Type.NOVEL }
private fun saveDownloads() {
val jsonString = gson.toJson(downloadsList)
prefs.edit().putString("downloads_key", jsonString).apply()
}
private fun loadDownloads(): List<DownloadedType> {
val jsonString = prefs.getString("downloads_key", null)
return if (jsonString != null) {
val type = object : TypeToken<List<DownloadedType>>() {}.type
gson.fromJson(jsonString, type)
} else {
emptyList()
}
}
fun addDownload(downloadedType: DownloadedType) {
downloadsList.add(downloadedType)
saveDownloads()
}
fun removeDownload(downloadedType: DownloadedType) {
downloadsList.remove(downloadedType)
removeDirectory(downloadedType)
saveDownloads()
}
fun removeMedia(title: String, type: DownloadedType.Type) {
val subDirectory = if (type == DownloadedType.Type.MANGA) {
"Manga"
} else if (type == DownloadedType.Type.ANIME) {
"Anime"
} else {
"Novel"
}
val directory = File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$subDirectory/$title"
)
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()
}
} else {
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
cleanDownloads()
}
when (type) {
DownloadedType.Type.MANGA -> {
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.MANGA }
}
DownloadedType.Type.ANIME -> {
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.ANIME }
}
DownloadedType.Type.NOVEL -> {
downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.NOVEL }
}
}
saveDownloads()
}
private fun cleanDownloads() {
cleanDownload(DownloadedType.Type.MANGA)
cleanDownload(DownloadedType.Type.ANIME)
cleanDownload(DownloadedType.Type.NOVEL)
}
private fun cleanDownload(type: DownloadedType.Type) {
// remove all folders that are not in the downloads list
val subDirectory = if (type == DownloadedType.Type.MANGA) {
"Manga"
} else if (type == DownloadedType.Type.ANIME) {
"Anime"
} else {
"Novel"
}
val directory = File(
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()
if (files != null) {
for (file in files) {
if (!downloadsSubLists.any { it.title == file.name }) {
val deleted = file.deleteRecursively()
}
}
}
}
//now remove all downloads that do not have a folder
val iterator = downloadsList.iterator()
while (iterator.hasNext()) {
val download = iterator.next()
val downloadDir = File(directory, download.title)
if ((!downloadDir.exists() && download.type == type) || download.title.isBlank()) {
iterator.remove()
}
}
}
fun saveDownloadsListToJSONFileInDownloadsFolder(downloadsList: List<DownloadedType>) //for debugging
{
val jsonString = gson.toJson(downloadsList)
val file = File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/downloads.json"
)
if (file.parentFile?.exists() == false) {
file.parentFile?.mkdirs()
}
if (!file.exists()) {
file.createNewFile()
}
file.writeText(jsonString)
}
fun queryDownload(downloadedType: DownloadedType): Boolean {
return downloadsList.contains(downloadedType)
}
fun queryDownload(title: String, chapter: String, type: DownloadedType.Type? = null): Boolean {
return if (type == null) {
downloadsList.any { it.title == title && it.chapter == chapter }
} else {
downloadsList.any { it.title == title && it.chapter == chapter && it.type == type }
}
}
private fun removeDirectory(downloadedType: DownloadedType) {
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"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}"
)
}
// 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()
}
} else {
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
}
}
fun exportDownloads(downloadedType: DownloadedType) { //copies to the downloads folder available to the user
val directory = if (downloadedType.type == DownloadedType.Type.MANGA) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"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) {
Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show()
}
} else {
Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show()
}
downloadsList.removeAll { it.type == type }
saveDownloads()
}
companion object {
const val novelLocation = "Dantotsu/Novel"
const val mangaLocation = "Dantotsu/Manga"
const val animeLocation = "Dantotsu/Anime"
fun getDirectory(context: Context, type: DownloadedType.Type, title: String, chapter: String? = null): File {
return if (type == DownloadedType.Type.MANGA) {
if (chapter != null) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$mangaLocation/$title/$chapter"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$mangaLocation/$title"
)
}
} else if (type == DownloadedType.Type.ANIME) {
if (chapter != null) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$animeLocation/$title/$chapter"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$animeLocation/$title"
)
}
} else {
if (chapter != null) {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$novelLocation/$title/$chapter"
)
} else {
File(
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"$novelLocation/$title"
)
}
}
}
}
}
data class DownloadedType(val title: String, val chapter: String, val type: Type) : Serializable {
enum class Type {
MANGA,
ANIME,
NOVEL
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,425 @@
package ani.dantotsu.download.manga
import android.Manifest
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.graphics.Bitmap
import android.os.Build
import android.os.Environment
import android.os.IBinder
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import ani.dantotsu.R
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger
import ani.dantotsu.media.Media
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_FINISHED
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_PROGRESS
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STARTED
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
import ani.dantotsu.snackString
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PROGRESS
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SChapterImpl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.util.Queue
import java.util.concurrent.ConcurrentLinkedQueue
class MangaDownloaderService : Service() {
private lateinit var notificationManager: NotificationManagerCompat
private lateinit var builder: NotificationCompat.Builder
private val downloadsManager: DownloadsManager = Injekt.get<DownloadsManager>()
private val downloadJobs = mutableMapOf<String, Job>()
private val mutex = Mutex()
private var isCurrentlyProcessing = false
override fun onBind(intent: Intent?): IBinder? {
// This is only required for bound services.
return null
}
override fun onCreate() {
super.onCreate()
notificationManager = NotificationManagerCompat.from(this)
builder = NotificationCompat.Builder(this, CHANNEL_DOWNLOADER_PROGRESS).apply {
setContentTitle("Manga Download Progress")
setSmallIcon(R.drawable.ic_round_download_24)
priority = NotificationCompat.PRIORITY_DEFAULT
setOnlyAlertOnce(true)
setProgress(0, 0, false)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(
NOTIFICATION_ID,
builder.build(),
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
)
} else {
startForeground(NOTIFICATION_ID, builder.build())
}
ContextCompat.registerReceiver(
this,
cancelReceiver,
IntentFilter(ACTION_CANCEL_DOWNLOAD),
ContextCompat.RECEIVER_EXPORTED
)
}
override fun onDestroy() {
super.onDestroy()
MangaServiceDataSingleton.downloadQueue.clear()
downloadJobs.clear()
MangaServiceDataSingleton.isServiceRunning = false
unregisterReceiver(cancelReceiver)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
snackString("Download started")
val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
serviceScope.launch {
mutex.withLock {
if (!isCurrentlyProcessing) {
isCurrentlyProcessing = true
processQueue()
isCurrentlyProcessing = false
}
}
}
return START_NOT_STICKY
}
private fun processQueue() {
CoroutineScope(Dispatchers.Default).launch {
while (MangaServiceDataSingleton.downloadQueue.isNotEmpty()) {
val task = MangaServiceDataSingleton.downloadQueue.poll()
if (task != null) {
val job = launch { download(task) }
mutex.withLock {
downloadJobs[task.chapter] = job
}
job.join() // Wait for the job to complete before continuing to the next task
mutex.withLock {
downloadJobs.remove(task.chapter)
}
updateNotification() // Update the notification after each task is completed
}
if (MangaServiceDataSingleton.downloadQueue.isEmpty()) {
withContext(Dispatchers.Main) {
stopSelf() // Stop the service when the queue is empty
}
}
}
}
}
fun cancelDownload(chapter: String) {
CoroutineScope(Dispatchers.Default).launch {
mutex.withLock {
downloadJobs[chapter]?.cancel()
downloadJobs.remove(chapter)
MangaServiceDataSingleton.downloadQueue.removeAll { it.chapter == chapter }
updateNotification() // Update the notification after cancellation
}
}
}
private fun updateNotification() {
// Update the notification to reflect the current state of the queue
val pendingDownloads = MangaServiceDataSingleton.downloadQueue.size
val text = if (pendingDownloads > 0) {
"Pending downloads: $pendingDownloads"
} else {
"All downloads completed"
}
builder.setContentText(text)
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
return
}
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
suspend fun download(task: DownloadTask) {
try {
withContext(Dispatchers.Main) {
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(
this@MangaDownloaderService,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
} else {
true
}
//val deferredList = mutableListOf<Deferred<Bitmap?>>()
val deferredMap = mutableMapOf<Int, Deferred<Bitmap?>>()
builder.setContentText("Downloading ${task.title} - ${task.chapter}")
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
// Loop through each ImageData object from the task
var farthest = 0
for ((index, image) in task.imageData.withIndex()) {
if (deferredMap.size >= task.simultaneousDownloads) {
deferredMap.values.awaitAll()
deferredMap.clear()
}
deferredMap[index] = async(Dispatchers.IO) {
var bitmap: Bitmap? = null
var retryCount = 0
while (bitmap == null && retryCount < task.retries) {
bitmap = image.fetchAndProcessImage(
image.page,
image.source,
this@MangaDownloaderService
)
retryCount++
}
if (bitmap != null) {
saveToDisk("$index.jpg", bitmap, task.title, task.chapter)
}
farthest++
builder.setProgress(task.imageData.size, farthest, false)
broadcastDownloadProgress(
task.chapter,
farthest * 100 / task.imageData.size
)
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
bitmap
}
}
// Wait for any remaining deferred to complete
deferredMap.values.awaitAll()
builder.setContentText("${task.title} - ${task.chapter} Download complete")
.setProgress(0, 0, false)
notificationManager.notify(NOTIFICATION_ID, builder.build())
saveMediaInfo(task)
downloadsManager.addDownload(
DownloadedType(
task.title,
task.chapter,
DownloadedType.Type.MANGA
)
)
broadcastDownloadFinished(task.chapter)
snackString("${task.title} - ${task.chapter} Download finished")
}
} catch (e: Exception) {
logger("Exception while downloading file: ${e.message}")
snackString("Exception while downloading file: ${e.message}")
FirebaseCrashlytics.getInstance().recordException(e)
broadcastDownloadFailed(task.chapter)
}
}
private fun saveToDisk(fileName: String, bitmap: Bitmap, title: String, chapter: String) {
try {
// Define the directory within the private external storage space
val directory = File(
this.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/$title/$chapter"
)
if (!directory.exists()) {
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
FileOutputStream(file).use { outputStream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
}
} catch (e: Exception) {
println("Exception while saving image: ${e.message}")
snackString("Exception while saving image: ${e.message}")
FirebaseCrashlytics.getInstance().recordException(e)
}
}
private fun saveMediaInfo(task: DownloadTask) {
GlobalScope.launch(Dispatchers.IO) {
val directory = File(
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${task.title}"
)
if (!directory.exists()) directory.mkdirs()
val file = File(directory, "media.json")
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
})
.create()
val mediaJson = gson.toJson(task.sourceMedia)
val media = gson.fromJson(mediaJson, Media::class.java)
if (media != null) {
media.cover = media.cover?.let { downloadImage(it, directory, "cover.jpg") }
media.banner = media.banner?.let { downloadImage(it, directory, "banner.jpg") }
val jsonString = gson.toJson(media)
withContext(Dispatchers.Main) {
try {
file.writeText(jsonString)
} catch (e: android.system.ErrnoException) {
e.printStackTrace()
Toast.makeText(
this@MangaDownloaderService,
"Error while saving: ${e.localizedMessage}",
Toast.LENGTH_LONG
).show()
}
}
}
}
}
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
withContext(Dispatchers.IO) {
var connection: HttpURLConnection? = null
println("Downloading url $url")
try {
connection = URL(url).openConnection() as HttpURLConnection
connection.connect()
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
}
val file = File(directory, name)
FileOutputStream(file).use { output ->
connection.inputStream.use { input ->
input.copyTo(output)
}
}
return@withContext file.absolutePath
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
Toast.makeText(
this@MangaDownloaderService,
"Exception while saving ${name}: ${e.message}",
Toast.LENGTH_LONG
).show()
}
null
} finally {
connection?.disconnect()
}
}
private fun broadcastDownloadStarted(chapterNumber: String) {
val intent = Intent(ACTION_DOWNLOAD_STARTED).apply {
putExtra(EXTRA_CHAPTER_NUMBER, chapterNumber)
}
sendBroadcast(intent)
}
private fun broadcastDownloadFinished(chapterNumber: String) {
val intent = Intent(ACTION_DOWNLOAD_FINISHED).apply {
putExtra(EXTRA_CHAPTER_NUMBER, chapterNumber)
}
sendBroadcast(intent)
}
private fun broadcastDownloadFailed(chapterNumber: String) {
val intent = Intent(ACTION_DOWNLOAD_FAILED).apply {
putExtra(EXTRA_CHAPTER_NUMBER, chapterNumber)
}
sendBroadcast(intent)
}
private fun broadcastDownloadProgress(chapterNumber: String, progress: Int) {
val intent = Intent(ACTION_DOWNLOAD_PROGRESS).apply {
putExtra(EXTRA_CHAPTER_NUMBER, chapterNumber)
putExtra("progress", progress)
}
sendBroadcast(intent)
}
private val cancelReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == ACTION_CANCEL_DOWNLOAD) {
val chapter = intent.getStringExtra(EXTRA_CHAPTER)
chapter?.let {
cancelDownload(it)
}
}
}
}
data class DownloadTask(
val title: String,
val chapter: String,
val imageData: List<ImageData>,
val sourceMedia: Media? = null,
val retries: Int = 2,
val simultaneousDownloads: Int = 2,
)
companion object {
private const val NOTIFICATION_ID = 1103
const val ACTION_CANCEL_DOWNLOAD = "action_cancel_download"
const val EXTRA_CHAPTER = "extra_chapter"
}
}
object MangaServiceDataSingleton {
var imageData: List<ImageData> = listOf()
var sourceMedia: Media? = null
var downloadQueue: Queue<MangaDownloaderService.DownloadTask> = ConcurrentLinkedQueue()
@Volatile
var isServiceRunning: Boolean = false
}

View File

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

View File

@@ -0,0 +1,443 @@
package ani.dantotsu.download.manga
import android.animation.ObjectAnimator
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.text.Editable
import android.text.TextWatcher
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.AlphaAnimation
import android.view.animation.LayoutAnimationController
import android.view.animation.OvershootInterpolator
import android.widget.AbsListView
import android.widget.AutoCompleteTextView
import android.widget.GridView
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
import androidx.core.util.Pair
import androidx.core.view.ViewCompat
import androidx.core.view.marginBottom
import androidx.fragment.app.Fragment
import ani.dantotsu.R
import ani.dantotsu.bottomBar
import ani.dantotsu.currActivity
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.initActivity
import ani.dantotsu.loadData
import ani.dantotsu.logger
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.SettingsDialogFragment
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import com.google.android.material.card.MaterialCardView
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.textfield.TextInputLayout
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SChapterImpl
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import kotlin.math.max
import kotlin.math.min
class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private val downloadManager = Injekt.get<DownloadsManager>()
private var downloads: List<OfflineMangaModel> = listOf()
private lateinit var gridView: GridView
private lateinit var adapter: OfflineMangaAdapter
private lateinit var total: TextView
private var uiSettings: UserInterfaceSettings =
loadData("ui_settings") ?: UserInterfaceSettings()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_offline_page, container, false)
val textInputLayout = view.findViewById<TextInputLayout>(R.id.offlineMangaSearchBar)
textInputLayout.hint = "Manga"
val currentColor = textInputLayout.boxBackgroundColor
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
textInputLayout.boxBackgroundColor = semiTransparentColor
val materialCardView = view.findViewById<MaterialCardView>(R.id.offlineMangaAvatarContainer)
materialCardView.setCardBackgroundColor(semiTransparentColor)
val typedValue = TypedValue()
requireContext().theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
val color = typedValue.data
val animeUserAvatar = view.findViewById<ShapeableImageView>(R.id.offlineMangaUserAvatar)
animeUserAvatar.setSafeOnClickListener {
val dialogFragment =
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.OfflineMANGA)
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
}
if (!uiSettings.immersiveMode) {
view.rootView.fitsSystemWindows = true
}
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("colorOverflow", false) ?: false
if (!colorOverflow) {
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
}
val searchView = view.findViewById<AutoCompleteTextView>(R.id.animeSearchBarText)
searchView.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
onSearchQuery(s.toString())
}
})
var style = context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getInt("offline_view", 0)
val layoutList = view.findViewById<ImageView>(R.id.downloadedList)
val layoutcompact = view.findViewById<ImageView>(R.id.downloadedGrid)
var selected = when (style) {
0 -> layoutList
1 -> layoutcompact
else -> layoutList
}
selected.alpha = 1f
fun selected(it: ImageView) {
selected.alpha = 0.33f
selected = it
selected.alpha = 1f
}
layoutList.setOnClickListener {
selected(it as ImageView)
style = 0
requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit()
.putInt("offline_view", style!!).apply()
gridView.visibility = View.GONE
gridView = view.findViewById(R.id.gridView)
adapter.notifyNewGrid()
grid()
}
layoutcompact.setOnClickListener {
selected(it as ImageView)
style = 1
requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit()
.putInt("offline_view", style!!).apply()
gridView.visibility = View.GONE
gridView = view.findViewById(R.id.gridView1)
adapter.notifyNewGrid()
grid()
}
gridView =
if (style == 0) view.findViewById(R.id.gridView) else view.findViewById(R.id.gridView1)
total = view.findViewById(R.id.total)
grid()
return view
}
private fun grid() {
gridView.visibility = View.VISIBLE
getDownloads()
val fadeIn = AlphaAnimation(0f, 1f)
fadeIn.duration = 300 // animations pog
gridView.layoutAnimation = LayoutAnimationController(fadeIn)
adapter = OfflineMangaAdapter(requireContext(), downloads, this)
gridView.adapter = adapter
gridView.scheduleLayoutAnimation()
total.text =
if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
gridView.setOnItemClickListener { _, _, position, _ ->
// Get the OfflineMangaModel that was clicked
val item = adapter.getItem(position) as OfflineMangaModel
val media =
downloadManager.mangaDownloadedTypes.firstOrNull { it.title == item.title }
?: downloadManager.novelDownloadedTypes.firstOrNull { it.title == item.title }
media?.let {
ContextCompat.startActivity(
requireActivity(),
Intent(requireContext(), MediaDetailsActivity::class.java)
.putExtra("media", getMedia(it))
.putExtra("download", true),
ActivityOptionsCompat.makeSceneTransitionAnimation(
requireActivity(),
Pair.create(
gridView.getChildAt(position)
.findViewById<ImageView>(R.id.itemCompactImage),
ViewCompat.getTransitionName(requireActivity().findViewById(R.id.itemCompactImage))
)
).toBundle()
)
} ?: run {
snackString("no media found")
}
}
gridView.setOnItemLongClickListener { _, _, position, _ ->
// Get the OfflineMangaModel that was clicked
val item = adapter.getItem(position) as OfflineMangaModel
val type: DownloadedType.Type =
if (downloadManager.mangaDownloadedTypes.any { it.title == item.title }) {
DownloadedType.Type.MANGA
} else {
DownloadedType.Type.NOVEL
}
// Alert dialog to confirm deletion
val builder =
androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup)
builder.setTitle("Delete ${item.title}?")
builder.setMessage("Are you sure you want to delete ${item.title}?")
builder.setPositiveButton("Yes") { _, _ ->
downloadManager.removeMedia(item.title, type)
getDownloads()
adapter.setItems(downloads)
total.text =
if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
}
builder.setNegativeButton("No") { _, _ ->
// Do nothing
}
val dialog = builder.show()
dialog.window?.setDimAmount(0.8f)
true
}
}
override fun onSearchQuery(query: String) {
adapter.onSearchQuery(query)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initActivity(requireActivity())
var height = statusBarHeight
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val displayCutout = activity?.window?.decorView?.rootWindowInsets?.displayCutout
if (displayCutout != null) {
if (displayCutout.boundingRects.size > 0) {
height = max(
statusBarHeight,
min(
displayCutout.boundingRects[0].width(),
displayCutout.boundingRects[0].height()
)
)
}
}
}
val scrollTop = view.findViewById<CardView>(R.id.mangaPageScrollTop)
scrollTop.translationY =
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
val visible = false
fun animate() {
val start = if (visible) 0f else 1f
val end = if (!visible) 0f else 1f
ObjectAnimator.ofFloat(scrollTop, "scaleX", start, end).apply {
duration = 300
interpolator = OvershootInterpolator(2f)
start()
}
ObjectAnimator.ofFloat(scrollTop, "scaleY", start, end).apply {
duration = 300
interpolator = OvershootInterpolator(2f)
start()
}
}
scrollTop.setOnClickListener {
gridView.smoothScrollToPositionFromTop(0, 0)
}
// Assuming 'scrollTop' is a view that you want to hide/show
scrollTop.visibility = View.GONE
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
// Implement behavior for different scroll states if needed
}
override fun onScroll(
view: AbsListView,
firstVisibleItem: Int,
visibleItemCount: Int,
totalItemCount: Int
) {
val first = view.getChildAt(0)
val visibility = first != null && first.top < -height
scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE
}
})
}
override fun onResume() {
super.onResume()
getDownloads()
adapter.notifyDataSetChanged()
}
override fun onPause() {
super.onPause()
downloads = listOf()
}
override fun onDestroy() {
super.onDestroy()
downloads = listOf()
}
override fun onStop() {
super.onStop()
downloads = listOf()
}
private fun getDownloads() {
downloads = listOf()
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
for (title in mangaTitles) {
val _downloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
val download = _downloads.first()
val offlineMangaModel = loadOfflineMangaModel(download)
newMangaDownloads += offlineMangaModel
}
downloads = newMangaDownloads
val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
for (title in novelTitles) {
val _downloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
val download = _downloads.first()
val offlineMangaModel = loadOfflineMangaModel(download)
newNovelDownloads += offlineMangaModel
}
downloads += newNovelDownloads
}
private fun getMedia(downloadedType: DownloadedType): Media? {
val type = if (downloadedType.type == DownloadedType.Type.MANGA) {
"Manga"
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
"Anime"
} else {
"Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
return try {
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
})
.create()
val media = File(directory, "media.json")
val mediaJson = media.readText()
gson.fromJson(mediaJson, Media::class.java)
} catch (e: Exception) {
logger("Error loading media.json: ${e.message}")
logger(e.printStackTrace())
FirebaseCrashlytics.getInstance().recordException(e)
null
}
}
private fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
val type = if (downloadedType.type == DownloadedType.Type.MANGA) {
"Manga"
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
"Anime"
} else {
"Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
try {
val media = File(directory, "media.json")
val mediaJson = media.readText()
val mediaModel = getMedia(downloadedType)!!
val cover = File(directory, "cover.jpg")
val coverUri: Uri? = if (cover.exists()) {
Uri.fromFile(cover)
} else null
val banner = File(directory, "banner.jpg")
val bannerUri: Uri? = if (banner.exists()) {
Uri.fromFile(banner)
} else null
val title = mediaModel.mainName()
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
?: 0) else mediaModel.userScore) / 10.0).toString()
val isOngoing =
mediaModel.status == currActivity()!!.getString(R.string.status_releasing)
val isUserScored = mediaModel.userScore != 0
val 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("Error loading media.json: ${e.message}")
logger(e.printStackTrace())
FirebaseCrashlytics.getInstance().recordException(e)
return OfflineMangaModel(
"unknown",
"0",
"??",
"??",
"movie",
"hmm",
false,
false,
null,
null
)
}
}
}
interface OfflineMangaSearchListener {
fun onSearchQuery(query: String)
}

View File

@@ -0,0 +1,16 @@
package ani.dantotsu.download.manga
import android.net.Uri
data class OfflineMangaModel(
val title: String,
val score: String,
val totalChapter: String,
val readChapter: String,
val type: String,
val chapters: String,
val isOngoing: Boolean,
val isUserScored: Boolean,
val image: Uri?,
val banner: Uri?
)

View File

@@ -0,0 +1,478 @@
package ani.dantotsu.download.novel
import android.Manifest
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.Environment
import android.os.IBinder
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import ani.dantotsu.R
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger
import ani.dantotsu.media.Media
import ani.dantotsu.media.novel.NovelReadFragment
import ani.dantotsu.snackString
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SChapterImpl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import okhttp3.Request
import okio.buffer
import okio.sink
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL
import java.util.Queue
import java.util.concurrent.ConcurrentLinkedQueue
class NovelDownloaderService : Service() {
private lateinit var notificationManager: NotificationManagerCompat
private lateinit var builder: NotificationCompat.Builder
private val downloadsManager: DownloadsManager = Injekt.get<DownloadsManager>()
private val downloadJobs = mutableMapOf<String, Job>()
private val mutex = Mutex()
private var isCurrentlyProcessing = false
val networkHelper = Injekt.get<NetworkHelper>()
override fun onBind(intent: Intent?): IBinder? {
// This is only required for bound services.
return null
}
override fun onCreate() {
super.onCreate()
notificationManager = NotificationManagerCompat.from(this)
builder =
NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER_PROGRESS).apply {
setContentTitle("Novel Download Progress")
setSmallIcon(R.drawable.ic_round_download_24)
priority = NotificationCompat.PRIORITY_DEFAULT
setOnlyAlertOnce(true)
setProgress(0, 0, false)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(
NOTIFICATION_ID,
builder.build(),
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
)
} else {
startForeground(NOTIFICATION_ID, builder.build())
}
ContextCompat.registerReceiver(
this,
cancelReceiver,
IntentFilter(ACTION_CANCEL_DOWNLOAD),
ContextCompat.RECEIVER_EXPORTED
)
}
override fun onDestroy() {
super.onDestroy()
NovelServiceDataSingleton.downloadQueue.clear()
downloadJobs.clear()
NovelServiceDataSingleton.isServiceRunning = false
unregisterReceiver(cancelReceiver)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
snackString("Download started")
val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
serviceScope.launch {
mutex.withLock {
if (!isCurrentlyProcessing) {
isCurrentlyProcessing = true
processQueue()
isCurrentlyProcessing = false
}
}
}
return START_NOT_STICKY
}
private fun processQueue() {
CoroutineScope(Dispatchers.Default).launch {
while (NovelServiceDataSingleton.downloadQueue.isNotEmpty()) {
val task = NovelServiceDataSingleton.downloadQueue.poll()
if (task != null) {
val job = launch { download(task) }
mutex.withLock {
downloadJobs[task.chapter] = job
}
job.join() // Wait for the job to complete before continuing to the next task
mutex.withLock {
downloadJobs.remove(task.chapter)
}
updateNotification() // Update the notification after each task is completed
}
if (NovelServiceDataSingleton.downloadQueue.isEmpty()) {
withContext(Dispatchers.Main) {
stopSelf() // Stop the service when the queue is empty
}
}
}
}
}
fun cancelDownload(chapter: String) {
CoroutineScope(Dispatchers.Default).launch {
mutex.withLock {
downloadJobs[chapter]?.cancel()
downloadJobs.remove(chapter)
NovelServiceDataSingleton.downloadQueue.removeAll { it.chapter == chapter }
updateNotification() // Update the notification after cancellation
}
}
}
private fun updateNotification() {
// Update the notification to reflect the current state of the queue
val pendingDownloads = NovelServiceDataSingleton.downloadQueue.size
val text = if (pendingDownloads > 0) {
"Pending downloads: $pendingDownloads"
} else {
"All downloads completed"
}
builder.setContentText(text)
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
return
}
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
private suspend fun isEpubFile(urlString: String): Boolean {
return withContext(Dispatchers.IO) {
try {
val request = Request.Builder()
.url(urlString)
.head()
.build()
networkHelper.client.newCall(request).execute().use { response ->
val contentType = response.header("Content-Type")
val contentDisposition = response.header("Content-Disposition")
logger("Content-Type: $contentType")
logger("Content-Disposition: $contentDisposition")
// Return true if the Content-Type or Content-Disposition indicates an EPUB file
contentType == "application/epub+zip" ||
(contentDisposition?.contains(".epub") == true)
}
} catch (e: Exception) {
logger("Error checking file type: ${e.message}")
false
}
}
}
private fun isAlreadyDownloaded(urlString: String): Boolean {
return urlString.contains("file://")
}
suspend fun download(task: DownloadTask) {
try {
withContext(Dispatchers.Main) {
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(
this@NovelDownloaderService,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
} else {
true
}
broadcastDownloadStarted(task.originalLink)
if (notifi) {
builder.setContentText("Downloading ${task.title} - ${task.chapter}")
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
if (!isEpubFile(task.downloadLink)) {
if (isAlreadyDownloaded(task.originalLink)) {
logger("Already downloaded")
broadcastDownloadFinished(task.originalLink)
snackString("Already downloaded")
return@withContext
}
logger("Download link is not an .epub file")
broadcastDownloadFailed(task.originalLink)
snackString("Download link is not an .epub file")
return@withContext
}
// Start the download
withContext(Dispatchers.IO) {
try {
val request = Request.Builder()
.url(task.downloadLink)
.build()
networkHelper.downloadClient.newCall(request).execute().use { response ->
// Ensure the response is successful and has a body
if (!response.isSuccessful || response.body == null) {
throw IOException("Failed to download file: ${response.message}")
}
val file = File(
this@NovelDownloaderService.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"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
task.coverUrl?.let {
file.parentFile?.let { it1 -> downloadImage(it, it1, "cover.jpg") }
}
val sink = file.sink().buffer()
val responseBody = response.body
val totalBytes = responseBody.contentLength()
var downloadedBytes = 0L
val notificationUpdateInterval = 1024 * 1024 // 1 MB
val broadcastUpdateInterval = 1024 * 256 // 256 KB
var lastNotificationUpdate = 0L
var lastBroadcastUpdate = 0L
responseBody.source().use { source ->
while (true) {
val read = source.read(sink.buffer, 8192)
if (read == -1L) break
downloadedBytes += read
sink.emit()
// Update progress at intervals
if (downloadedBytes - lastNotificationUpdate >= notificationUpdateInterval) {
withContext(Dispatchers.Main) {
val progress =
(downloadedBytes * 100 / totalBytes).toInt()
builder.setProgress(100, progress, false)
if (notifi) {
notificationManager.notify(
NOTIFICATION_ID,
builder.build()
)
}
}
lastNotificationUpdate = downloadedBytes
}
if (downloadedBytes - lastBroadcastUpdate >= broadcastUpdateInterval) {
withContext(Dispatchers.Main) {
val progress =
(downloadedBytes * 100 / totalBytes).toInt()
logger("Download progress: $progress")
broadcastDownloadProgress(task.originalLink, progress)
}
lastBroadcastUpdate = downloadedBytes
}
}
}
sink.close()
//if the file is smaller than 95% of totalBytes, it means the download was interrupted
if (file.length() < totalBytes * 0.95) {
throw IOException("Failed to download file: ${response.message}")
}
}
} catch (e: Exception) {
logger("Exception while downloading .epub inside request: ${e.message}")
throw e
}
}
// Update notification for download completion
builder.setContentText("${task.title} - ${task.chapter} Download complete")
.setProgress(0, 0, false)
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
saveMediaInfo(task)
downloadsManager.addDownload(
DownloadedType(
task.title,
task.chapter,
DownloadedType.Type.NOVEL
)
)
broadcastDownloadFinished(task.originalLink)
snackString("${task.title} - ${task.chapter} Download finished")
}
} catch (e: Exception) {
logger("Exception while downloading .epub: ${e.message}")
snackString("Exception while downloading .epub: ${e.message}")
FirebaseCrashlytics.getInstance().recordException(e)
broadcastDownloadFailed(task.originalLink)
}
}
private fun saveMediaInfo(task: DownloadTask) {
GlobalScope.launch(Dispatchers.IO) {
val directory = File(
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Novel/${task.title}"
)
if (!directory.exists()) directory.mkdirs()
val file = File(directory, "media.json")
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
})
.create()
val mediaJson = gson.toJson(task.sourceMedia)
val media = gson.fromJson(mediaJson, Media::class.java)
if (media != null) {
media.cover = media.cover?.let { downloadImage(it, directory, "cover.jpg") }
media.banner = media.banner?.let { downloadImage(it, directory, "banner.jpg") }
val jsonString = gson.toJson(media)
withContext(Dispatchers.Main) {
file.writeText(jsonString)
}
}
}
}
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
withContext(
Dispatchers.IO
) {
var connection: HttpURLConnection? = null
println("Downloading url $url")
try {
connection = URL(url).openConnection() as HttpURLConnection
connection.connect()
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
}
val file = File(directory, name)
FileOutputStream(file).use { output ->
connection.inputStream.use { input ->
input.copyTo(output)
}
}
return@withContext file.absolutePath
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
Toast.makeText(
this@NovelDownloaderService,
"Exception while saving ${name}: ${e.message}",
Toast.LENGTH_LONG
).show()
}
null
} finally {
connection?.disconnect()
}
}
private fun broadcastDownloadStarted(link: String) {
val intent = Intent(NovelReadFragment.ACTION_DOWNLOAD_STARTED).apply {
putExtra(NovelReadFragment.EXTRA_NOVEL_LINK, link)
}
sendBroadcast(intent)
}
private fun broadcastDownloadFinished(link: String) {
val intent = Intent(NovelReadFragment.ACTION_DOWNLOAD_FINISHED).apply {
putExtra(NovelReadFragment.EXTRA_NOVEL_LINK, link)
}
sendBroadcast(intent)
}
private fun broadcastDownloadFailed(link: String) {
val intent = Intent(NovelReadFragment.ACTION_DOWNLOAD_FAILED).apply {
putExtra(NovelReadFragment.EXTRA_NOVEL_LINK, link)
}
sendBroadcast(intent)
}
private fun broadcastDownloadProgress(link: String, progress: Int) {
val intent = Intent(NovelReadFragment.ACTION_DOWNLOAD_PROGRESS).apply {
putExtra(NovelReadFragment.EXTRA_NOVEL_LINK, link)
putExtra("progress", progress)
}
sendBroadcast(intent)
}
private val cancelReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == ACTION_CANCEL_DOWNLOAD) {
val chapter = intent.getStringExtra(EXTRA_CHAPTER)
chapter?.let {
cancelDownload(it)
}
}
}
}
data class DownloadTask(
val title: String,
val chapter: String,
val downloadLink: String,
val originalLink: String,
val sourceMedia: Media? = null,
val coverUrl: String? = null,
val retries: Int = 2,
)
companion object {
private const val NOTIFICATION_ID = 1103
const val ACTION_CANCEL_DOWNLOAD = "action_cancel_download"
const val EXTRA_CHAPTER = "extra_chapter"
}
}
object NovelServiceDataSingleton {
var sourceMedia: Media? = null
var downloadQueue: Queue<NovelDownloaderService.DownloadTask> = ConcurrentLinkedQueue()
@Volatile
var isServiceRunning: Boolean = false
}

View File

@@ -11,7 +11,8 @@ import androidx.media3.exoplayer.scheduler.Scheduler
import ani.dantotsu.R import ani.dantotsu.R
@UnstableApi @UnstableApi
class MyDownloadService : DownloadService(1, 1, "download_service", R.string.downloads, 0) { class ExoplayerDownloadService :
DownloadService(1, 2000, "download_service", R.string.downloads, 0) {
companion object { companion object {
private const val JOB_ID = 1 private const val JOB_ID = 1
private const val FOREGROUND_NOTIFICATION_ID = 1 private const val FOREGROUND_NOTIFICATION_ID = 1
@@ -21,10 +22,13 @@ class MyDownloadService : DownloadService(1, 1, "download_service", R.string.dow
override fun getScheduler(): Scheduler = PlatformScheduler(this, JOB_ID) override fun getScheduler(): Scheduler = PlatformScheduler(this, JOB_ID)
override fun getForegroundNotification(downloads: MutableList<Download>, notMetRequirements: Int): Notification = override fun getForegroundNotification(
downloads: MutableList<Download>,
notMetRequirements: Int
): Notification =
DownloadNotificationHelper(this, "download_service").buildProgressNotification( DownloadNotificationHelper(this, "download_service").buildProgressNotification(
this, this,
R.drawable.monochrome, R.drawable.mono,
null, null,
null, null,
downloads, downloads,

View File

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

View File

@@ -2,6 +2,7 @@ package ani.dantotsu.home
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@@ -47,8 +48,10 @@ import kotlin.math.min
class AnimeFragment : Fragment() { class AnimeFragment : Fragment() {
private var _binding: FragmentAnimeBinding? = null private var _binding: FragmentAnimeBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var animePageAdapter: AnimePageAdapter
private var uiSettings: UserInterfaceSettings = loadData("ui_settings") ?: UserInterfaceSettings() private var uiSettings: UserInterfaceSettings =
loadData("ui_settings") ?: UserInterfaceSettings()
val model: AnilistAnimeViewModel by activityViewModels() val model: AnilistAnimeViewModel by activityViewModels()
@@ -94,7 +97,7 @@ class AnimeFragment : Fragment() {
binding.animePageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px) binding.animePageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px)
val animePageAdapter = AnimePageAdapter() animePageAdapter = AnimePageAdapter()
var loading = true var loading = true
if (model.notSet) { if (model.notSet) {
@@ -224,7 +227,8 @@ class AnimeFragment : Fragment() {
} }
} }
} }
binding.animePageScrollTop.translationY = -(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat() binding.animePageScrollTop.translationY =
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
} }
} }
@@ -264,7 +268,8 @@ class AnimeFragment : Fragment() {
model.loaded = true model.loaded = true
model.loadTrending(1) model.loadTrending(1)
model.loadUpdated() model.loadUpdated()
model.loadPopular("ANIME", sort = Anilist.sortBy[1]) model.loadPopular("ANIME", sort = Anilist.sortBy[1], onList = requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
.getBoolean("popular_list", false))
} }
live.postValue(false) live.postValue(false)
_binding?.animeRefresh?.isRefreshing = false _binding?.animeRefresh?.isRefreshing = false
@@ -275,6 +280,11 @@ class AnimeFragment : Fragment() {
override fun onResume() { override fun onResume() {
if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true) if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true)
if (animePageAdapter.trendingViewPager != null) {
binding.root.requestApplyInsets()
binding.root.requestLayout()
}
super.onResume() super.onResume()
} }
} }

View File

@@ -1,8 +1,10 @@
package ani.dantotsu.home package ani.dantotsu.home
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
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
@@ -15,13 +17,15 @@ import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.media.GenreActivity
import ani.dantotsu.MediaPageTransformer import ani.dantotsu.MediaPageTransformer
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemAnimePageBinding import ani.dantotsu.databinding.ItemAnimePageBinding
import ani.dantotsu.loadData import ani.dantotsu.loadData
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
import ani.dantotsu.media.CalendarActivity import ani.dantotsu.media.CalendarActivity
import ani.dantotsu.media.GenreActivity
import ani.dantotsu.media.MediaAdaptor import ani.dantotsu.media.MediaAdaptor
import ani.dantotsu.media.SearchActivity import ani.dantotsu.media.SearchActivity
import ani.dantotsu.px import ani.dantotsu.px
@@ -31,6 +35,8 @@ import ani.dantotsu.setSlideUp
import ani.dantotsu.settings.SettingsDialogFragment import ani.dantotsu.settings.SettingsDialogFragment
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import com.google.android.material.card.MaterialCardView
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)
@@ -38,10 +44,12 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
private var trendHandler: Handler? = null private var trendHandler: Handler? = null
private lateinit var trendRun: Runnable private lateinit var trendRun: Runnable
var trendingViewPager: ViewPager2? = null var trendingViewPager: ViewPager2? = null
private var uiSettings: UserInterfaceSettings = loadData("ui_settings") ?: UserInterfaceSettings() private var uiSettings: UserInterfaceSettings =
loadData("ui_settings") ?: UserInterfaceSettings()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnimePageViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnimePageViewHolder {
val binding = ItemAnimePageBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding =
ItemAnimePageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return AnimePageViewHolder(binding) return AnimePageViewHolder(binding)
} }
@@ -49,6 +57,25 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
binding = holder.binding binding = holder.binding
trendingViewPager = binding.animeTrendingViewPager trendingViewPager = binding.animeTrendingViewPager
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.animeSearchBar)
val currentColor = textInputLayout.boxBackgroundColor
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
textInputLayout.boxBackgroundColor = semiTransparentColor
val materialCardView =
holder.itemView.findViewById<MaterialCardView>(R.id.animeUserAvatarContainer)
materialCardView.setCardBackgroundColor(semiTransparentColor)
val typedValue = TypedValue()
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
val color = typedValue.data
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("colorOverflow", false) ?: false
if (!colorOverflow) {
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
}
binding.animeTitleContainer.updatePadding(top = statusBarHeight) binding.animeTitleContainer.updatePadding(top = statusBarHeight)
if (uiSettings.smallView) binding.animeTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { if (uiSettings.smallView) binding.animeTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
@@ -71,7 +98,9 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
} }
binding.animeUserAvatar.setSafeOnClickListener { binding.animeUserAvatar.setSafeOnClickListener {
SettingsDialogFragment().show((it.context as AppCompatActivity).supportFragmentManager, "dialog") val dialogFragment =
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.ANIME)
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
} }
listOf( listOf(
@@ -101,9 +130,17 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
) )
} }
binding.animeIncludeList.visibility = if(Anilist.userid!=null) View.VISIBLE else View.GONE binding.animeIncludeList.visibility =
if (Anilist.userid != null) View.VISIBLE else View.GONE
binding.animeIncludeList.isChecked = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("popular_list", true) ?: true
binding.animeIncludeList.setOnCheckedChangeListener { _, isChecked -> binding.animeIncludeList.setOnCheckedChangeListener { _, isChecked ->
onIncludeListClick.invoke(isChecked) onIncludeListClick.invoke(isChecked)
currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
?.putBoolean("popular_list", isChecked)?.apply()
} }
if (ready.value == false) if (ready.value == false)
ready.postValue(true) ready.postValue(true)
@@ -128,7 +165,8 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
trendHandler = Handler(Looper.getMainLooper()) trendHandler = Handler(Looper.getMainLooper())
trendRun = Runnable { trendRun = Runnable {
binding.animeTrendingViewPager.currentItem = binding.animeTrendingViewPager.currentItem + 1 binding.animeTrendingViewPager.currentItem =
binding.animeTrendingViewPager.currentItem + 1
} }
binding.animeTrendingViewPager.registerOnPageChangeCallback( binding.animeTrendingViewPager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() { object : ViewPager2.OnPageChangeCallback() {
@@ -140,22 +178,30 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
} }
) )
binding.animeTrendingViewPager.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f) binding.animeTrendingViewPager.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
binding.animeTitleContainer.startAnimation(setSlideUp(uiSettings)) binding.animeTitleContainer.startAnimation(setSlideUp(uiSettings))
binding.animeListContainer.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f) binding.animeListContainer.layoutAnimation =
binding.animeSeasonsCont.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f) LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
binding.animeSeasonsCont.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
} }
fun updateRecent(adaptor: MediaAdaptor) { fun updateRecent(adaptor: MediaAdaptor) {
binding.animeUpdatedProgressBar.visibility = View.GONE binding.animeUpdatedProgressBar.visibility = View.GONE
binding.animeUpdatedRecyclerView.adapter = adaptor binding.animeUpdatedRecyclerView.adapter = adaptor
binding.animeUpdatedRecyclerView.layoutManager = binding.animeUpdatedRecyclerView.layoutManager =
LinearLayoutManager(binding.animeUpdatedRecyclerView.context, LinearLayoutManager.HORIZONTAL, false) LinearLayoutManager(
binding.animeUpdatedRecyclerView.context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.animeUpdatedRecyclerView.visibility = View.VISIBLE binding.animeUpdatedRecyclerView.visibility = View.VISIBLE
binding.animeRecently.visibility = View.VISIBLE binding.animeRecently.visibility = View.VISIBLE
binding.animeRecently.startAnimation(setSlideUp(uiSettings)) binding.animeRecently.startAnimation(setSlideUp(uiSettings))
binding.animeUpdatedRecyclerView.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f) binding.animeUpdatedRecyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
binding.animePopular.visibility = View.VISIBLE binding.animePopular.visibility = View.VISIBLE
binding.animePopular.startAnimation(setSlideUp(uiSettings)) binding.animePopular.startAnimation(setSlideUp(uiSettings))
} }
@@ -163,8 +209,10 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
fun updateAvatar() { fun updateAvatar() {
if (Anilist.avatar != null && ready.value == true) { if (Anilist.avatar != null && ready.value == true) {
binding.animeUserAvatar.loadImage(Anilist.avatar) binding.animeUserAvatar.loadImage(Anilist.avatar)
binding.animeUserAvatar.imageTintList = null
} }
} }
inner class AnimePageViewHolder(val binding: ItemAnimePageBinding) : RecyclerView.ViewHolder(binding.root) inner class AnimePageViewHolder(val binding: ItemAnimePageBinding) :
RecyclerView.ViewHolder(binding.root)
} }

View File

@@ -31,13 +31,13 @@ import ani.dantotsu.loadData
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaAdaptor import ani.dantotsu.media.MediaAdaptor
import ani.dantotsu.settings.SettingsDialogFragment
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.media.user.ListActivity import ani.dantotsu.media.user.ListActivity
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.setSafeOnClickListener import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.setSlideIn import ani.dantotsu.setSlideIn
import ani.dantotsu.setSlideUp import ani.dantotsu.setSlideUp
import ani.dantotsu.settings.SettingsDialogFragment
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -52,7 +52,11 @@ class HomeFragment : Fragment() {
private var _binding: FragmentHomeBinding? = null private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentHomeBinding.inflate(inflater, container, false) _binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }
@@ -96,18 +100,24 @@ class HomeFragment : Fragment() {
binding.homeUserAvatarContainer.startAnimation(setSlideUp(uiSettings)) binding.homeUserAvatarContainer.startAnimation(setSlideUp(uiSettings))
binding.homeUserDataContainer.visibility = View.VISIBLE binding.homeUserDataContainer.visibility = View.VISIBLE
binding.homeUserDataContainer.layoutAnimation = LayoutAnimationController(setSlideUp(uiSettings), 0.25f) binding.homeUserDataContainer.layoutAnimation =
LayoutAnimationController(setSlideUp(uiSettings), 0.25f)
binding.homeAnimeList.visibility = View.VISIBLE binding.homeAnimeList.visibility = View.VISIBLE
binding.homeMangaList.visibility = View.VISIBLE binding.homeMangaList.visibility = View.VISIBLE
binding.homeListContainer.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f) binding.homeListContainer.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
} }
else { else {
snackString(currContext()?.getString(R.string.please_reload)) snackString(currContext()?.getString(R.string.please_reload))
} }
} }
binding.homeUserAvatarContainer.setSafeOnClickListener { binding.homeUserAvatarContainer.setSafeOnClickListener {
SettingsDialogFragment().show(parentFragmentManager, "dialog") val dialogFragment =
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.HOME)
dialogFragment.show(
(it.context as androidx.appcompat.app.AppCompatActivity).supportFragmentManager,
"dialog"
)
} }
binding.homeContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { binding.homeContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
@@ -118,17 +128,17 @@ class HomeFragment : Fragment() {
var reached = false var reached = false
val duration = (uiSettings.animationSpeed * 200).toLong() val duration = (uiSettings.animationSpeed * 200).toLong()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
binding.homeScroll.setOnScrollChangeListener { _, _, _, _, _ -> binding.homeScroll.setOnScrollChangeListener { _, _, _, _, _ ->
if (!binding.homeScroll.canScrollVertically(1)) { if (!binding.homeScroll.canScrollVertically(1)) {
reached = true reached = true
bottomBar.animate().translationZ(0f).setDuration(duration).start() bottomBar.animate().translationZ(0f).setDuration(duration).start()
ObjectAnimator.ofFloat(bottomBar, "elevation", 4f, 0f).setDuration(duration).start() ObjectAnimator.ofFloat(bottomBar, "elevation", 4f, 0f).setDuration(duration)
.start()
} else { } else {
if (reached) { if (reached) {
bottomBar.animate().translationZ(12f).setDuration(duration).start() bottomBar.animate().translationZ(12f).setDuration(duration).start()
ObjectAnimator.ofFloat(bottomBar, "elevation", 0f, 4f).setDuration(duration).start() ObjectAnimator.ofFloat(bottomBar, "elevation", 0f, 4f).setDuration(duration)
} .start()
} }
} }
} }
@@ -138,7 +148,13 @@ class HomeFragment : Fragment() {
if (displayCutout != null) { if (displayCutout != null) {
if (displayCutout.boundingRects.size > 0) { if (displayCutout.boundingRects.size > 0) {
height = height =
max(statusBarHeight, min(displayCutout.boundingRects[0].width(), displayCutout.boundingRects[0].height())) max(
statusBarHeight,
min(
displayCutout.boundingRects[0].width(),
displayCutout.boundingRects[0].height()
)
)
} }
} }
} }
@@ -189,7 +205,8 @@ class HomeFragment : Fragment() {
false false
) )
recyclerView.visibility = View.VISIBLE recyclerView.visibility = View.VISIBLE
recyclerView.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f) recyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
} else { } else {
empty.visibility = View.VISIBLE empty.visibility = View.VISIBLE
@@ -313,7 +330,8 @@ class HomeFragment : Fragment() {
live.observe(viewLifecycleOwner) { live.observe(viewLifecycleOwner) {
if (it) { if (it) {
scope.launch { scope.launch {
uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings() uiSettings =
loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
//Get userData First //Get userData First
getUserId(requireContext()) { getUserId(requireContext()) {

View File

@@ -15,7 +15,11 @@ class LoginFragment : Fragment() {
private var _binding: FragmentLoginBinding? = null private var _binding: FragmentLoginBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentLoginBinding.inflate(layoutInflater, container, false) _binding = FragmentLoginBinding.inflate(layoutInflater, container, false)
return binding.root return binding.root
} }
@@ -24,5 +28,6 @@ class LoginFragment : Fragment() {
binding.loginButton.setOnClickListener { Anilist.loginIntent(requireActivity()) } binding.loginButton.setOnClickListener { Anilist.loginIntent(requireActivity()) }
binding.loginDiscord.setOnClickListener { openLinkInBrowser(getString(R.string.discord)) } binding.loginDiscord.setOnClickListener { openLinkInBrowser(getString(R.string.discord)) }
binding.loginGithub.setOnClickListener { openLinkInBrowser(getString(R.string.github)) } binding.loginGithub.setOnClickListener { openLinkInBrowser(getString(R.string.github)) }
binding.loginTelegram.setOnClickListener { openLinkInBrowser(getString(R.string.telegram)) }
} }
} }

View File

@@ -2,6 +2,7 @@ package ani.dantotsu.home
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@@ -43,12 +44,18 @@ import kotlin.math.min
class MangaFragment : Fragment() { class MangaFragment : Fragment() {
private var _binding: FragmentMangaBinding? = null private var _binding: FragmentMangaBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var mangaPageAdapter: MangaPageAdapter
private var uiSettings: UserInterfaceSettings = loadData("ui_settings") ?: UserInterfaceSettings() private var uiSettings: UserInterfaceSettings =
loadData("ui_settings") ?: UserInterfaceSettings()
val model: AnilistMangaViewModel by activityViewModels() val model: AnilistMangaViewModel by activityViewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentMangaBinding.inflate(inflater, container, false) _binding = FragmentMangaBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }
@@ -85,7 +92,7 @@ class MangaFragment : Fragment() {
binding.mangaPageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px) binding.mangaPageRecyclerView.updatePaddingRelative(bottom = navBarHeight + 160f.px)
val mangaPageAdapter = MangaPageAdapter() mangaPageAdapter = MangaPageAdapter()
var loading = true var loading = true
if (model.notSet) { if (model.notSet) {
model.notSet = false model.notSet = false
@@ -100,7 +107,8 @@ class MangaFragment : Fragment() {
} }
val popularAdaptor = MediaAdaptor(1, model.searchResults.results, requireActivity()) val popularAdaptor = MediaAdaptor(1, model.searchResults.results, requireActivity())
val progressAdaptor = ProgressAdapter(searched = model.searched) val progressAdaptor = ProgressAdapter(searched = model.searched)
binding.mangaPageRecyclerView.adapter = ConcatAdapter(mangaPageAdapter, popularAdaptor, progressAdaptor) binding.mangaPageRecyclerView.adapter =
ConcatAdapter(mangaPageAdapter, popularAdaptor, progressAdaptor)
val layout = LinearLayoutManager(requireContext()) val layout = LinearLayoutManager(requireContext())
binding.mangaPageRecyclerView.layoutManager = layout binding.mangaPageRecyclerView.layoutManager = layout
@@ -177,7 +185,8 @@ class MangaFragment : Fragment() {
} }
} }
} }
binding.mangaPageScrollTop.translationY = -(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat() binding.mangaPageScrollTop.translationY =
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
} }
} }
@@ -233,7 +242,8 @@ class MangaFragment : Fragment() {
model.loaded = true model.loaded = true
model.loadTrending() model.loadTrending()
model.loadTrendingNovel() model.loadTrendingNovel()
model.loadPopular("MANGA", sort = Anilist.sortBy[1]) model.loadPopular("MANGA", sort = Anilist.sortBy[1], onList = requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
.getBoolean("popular_list", false) )
} }
live.postValue(false) live.postValue(false)
_binding?.mangaRefresh?.isRefreshing = false _binding?.mangaRefresh?.isRefreshing = false
@@ -244,6 +254,11 @@ class MangaFragment : Fragment() {
override fun onResume() { override fun onResume() {
if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true) if (!model.loaded) Refresh.activity[this.hashCode()]!!.postValue(true)
//make sure mangaPageAdapter is initialized
if (mangaPageAdapter.trendingViewPager != null) {
binding.root.requestApplyInsets()
binding.root.requestLayout()
}
super.onResume() super.onResume()
} }

View File

@@ -1,8 +1,10 @@
package ani.dantotsu.home package ani.dantotsu.home
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
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
@@ -15,12 +17,14 @@ import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.media.GenreActivity
import ani.dantotsu.MediaPageTransformer import ani.dantotsu.MediaPageTransformer
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemMangaPageBinding import ani.dantotsu.databinding.ItemMangaPageBinding
import ani.dantotsu.loadData import ani.dantotsu.loadData
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
import ani.dantotsu.media.GenreActivity
import ani.dantotsu.media.MediaAdaptor import ani.dantotsu.media.MediaAdaptor
import ani.dantotsu.media.SearchActivity import ani.dantotsu.media.SearchActivity
import ani.dantotsu.px import ani.dantotsu.px
@@ -30,6 +34,8 @@ import ani.dantotsu.setSlideUp
import ani.dantotsu.settings.SettingsDialogFragment import ani.dantotsu.settings.SettingsDialogFragment
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import com.google.android.material.card.MaterialCardView
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)
@@ -37,10 +43,12 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
private var trendHandler: Handler? = null private var trendHandler: Handler? = null
private lateinit var trendRun: Runnable private lateinit var trendRun: Runnable
var trendingViewPager: ViewPager2? = null var trendingViewPager: ViewPager2? = null
private var uiSettings: UserInterfaceSettings = loadData("ui_settings") ?: UserInterfaceSettings() private var uiSettings: UserInterfaceSettings =
loadData("ui_settings") ?: UserInterfaceSettings()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MangaPageViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MangaPageViewHolder {
val binding = ItemMangaPageBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding =
ItemMangaPageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MangaPageViewHolder(binding) return MangaPageViewHolder(binding)
} }
@@ -48,6 +56,25 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
binding = holder.binding binding = holder.binding
trendingViewPager = binding.mangaTrendingViewPager trendingViewPager = binding.mangaTrendingViewPager
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.mangaSearchBar)
val currentColor = textInputLayout.boxBackgroundColor
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
textInputLayout.boxBackgroundColor = semiTransparentColor
val materialCardView =
holder.itemView.findViewById<MaterialCardView>(R.id.mangaUserAvatarContainer)
materialCardView.setCardBackgroundColor(semiTransparentColor)
val typedValue = TypedValue()
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
val color = typedValue.data
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("colorOverflow", false) ?: false
if (!colorOverflow) {
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
}
binding.mangaTitleContainer.updatePadding(top = statusBarHeight) binding.mangaTitleContainer.updatePadding(top = statusBarHeight)
if (uiSettings.smallView) binding.mangaTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { if (uiSettings.smallView) binding.mangaTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
@@ -66,7 +93,9 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
} }
binding.mangaUserAvatar.setSafeOnClickListener { binding.mangaUserAvatar.setSafeOnClickListener {
SettingsDialogFragment().show((it.context as AppCompatActivity).supportFragmentManager, "dialog") val dialogFragment =
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.MANGA)
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
} }
binding.mangaSearchBar.setEndIconOnClickListener { binding.mangaSearchBar.setEndIconOnClickListener {
@@ -94,11 +123,18 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
) )
} }
binding.mangaIncludeList.visibility = if(Anilist.userid!=null) View.VISIBLE else View.GONE binding.mangaIncludeList.visibility =
if (Anilist.userid != null) View.VISIBLE else View.GONE
binding.mangaIncludeList.isChecked = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("popular_list", true) ?: true
binding.mangaIncludeList.setOnCheckedChangeListener { _, isChecked -> binding.mangaIncludeList.setOnCheckedChangeListener { _, isChecked ->
onIncludeListClick.invoke(isChecked) onIncludeListClick.invoke(isChecked)
}
currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
?.putBoolean("popular_list", isChecked)?.apply()
}
if (ready.value == false) if (ready.value == false)
ready.postValue(true) ready.postValue(true)
} }
@@ -119,7 +155,8 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
binding.mangaTrendingViewPager.setPageTransformer(MediaPageTransformer()) binding.mangaTrendingViewPager.setPageTransformer(MediaPageTransformer())
trendHandler = Handler(Looper.getMainLooper()) trendHandler = Handler(Looper.getMainLooper())
trendRun = Runnable { trendRun = Runnable {
binding.mangaTrendingViewPager.currentItem = binding.mangaTrendingViewPager.currentItem + 1 binding.mangaTrendingViewPager.currentItem =
binding.mangaTrendingViewPager.currentItem + 1
} }
binding.mangaTrendingViewPager.registerOnPageChangeCallback( binding.mangaTrendingViewPager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() { object : ViewPager2.OnPageChangeCallback() {
@@ -131,21 +168,28 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
} }
) )
binding.mangaTrendingViewPager.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f) binding.mangaTrendingViewPager.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
binding.mangaTitleContainer.startAnimation(setSlideUp(uiSettings)) binding.mangaTitleContainer.startAnimation(setSlideUp(uiSettings))
binding.mangaListContainer.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f) binding.mangaListContainer.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
} }
fun updateNovel(adaptor: MediaAdaptor) { fun updateNovel(adaptor: MediaAdaptor) {
binding.mangaNovelProgressBar.visibility = View.GONE binding.mangaNovelProgressBar.visibility = View.GONE
binding.mangaNovelRecyclerView.adapter = adaptor binding.mangaNovelRecyclerView.adapter = adaptor
binding.mangaNovelRecyclerView.layoutManager = binding.mangaNovelRecyclerView.layoutManager =
LinearLayoutManager(binding.mangaNovelRecyclerView.context, LinearLayoutManager.HORIZONTAL, false) LinearLayoutManager(
binding.mangaNovelRecyclerView.context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.mangaNovelRecyclerView.visibility = View.VISIBLE binding.mangaNovelRecyclerView.visibility = View.VISIBLE
binding.mangaNovel.visibility = View.VISIBLE binding.mangaNovel.visibility = View.VISIBLE
binding.mangaNovel.startAnimation(setSlideUp(uiSettings)) binding.mangaNovel.startAnimation(setSlideUp(uiSettings))
binding.mangaNovelRecyclerView.layoutAnimation = LayoutAnimationController(setSlideIn(uiSettings), 0.25f) binding.mangaNovelRecyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
binding.mangaPopular.visibility = View.VISIBLE binding.mangaPopular.visibility = View.VISIBLE
binding.mangaPopular.startAnimation(setSlideUp(uiSettings)) binding.mangaPopular.startAnimation(setSlideUp(uiSettings))
} }
@@ -153,8 +197,10 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
fun updateAvatar() { fun updateAvatar() {
if (Anilist.avatar != null && ready.value == true) { if (Anilist.avatar != null && ready.value == true) {
binding.mangaUserAvatar.loadImage(Anilist.avatar) binding.mangaUserAvatar.loadImage(Anilist.avatar)
binding.mangaUserAvatar.imageTintList = null
} }
} }
inner class MangaPageViewHolder(val binding: ItemMangaPageBinding) : RecyclerView.ViewHolder(binding.root) inner class MangaPageViewHolder(val binding: ItemMangaPageBinding) :
RecyclerView.ViewHolder(binding.root)
} }

View File

@@ -1,29 +1,124 @@
package ani.dantotsu.home package ani.dantotsu.home
import android.content.Context
import android.graphics.drawable.GradientDrawable
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.doOnAttach
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.viewpager2.adapter.FragmentStateAdapter
import ani.dantotsu.R
import ani.dantotsu.ZoomOutPageTransformer
import ani.dantotsu.databinding.ActivityNoInternetBinding import ani.dantotsu.databinding.ActivityNoInternetBinding
import ani.dantotsu.isOnline import ani.dantotsu.download.anime.OfflineAnimeFragment
import ani.dantotsu.download.manga.OfflineMangaFragment
import ani.dantotsu.initActivity
import ani.dantotsu.loadData
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.startMainActivity import ani.dantotsu.offline.OfflineFragment
import ani.dantotsu.statusBarHeight import ani.dantotsu.others.LangSet
import ani.dantotsu.selectedOption
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.snackString
import ani.dantotsu.themes.ThemeManager
import nl.joery.animatedbottombar.AnimatedBottomBar
class NoInternet : AppCompatActivity() { class NoInternet : AppCompatActivity() {
private lateinit var binding: ActivityNoInternetBinding
lateinit var bottomBar: AnimatedBottomBar
private var uiSettings = UserInterfaceSettings()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
val binding = ActivityNoInternetBinding.inflate(layoutInflater) binding = ActivityNoInternetBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
binding.refreshContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
topMargin = statusBarHeight if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val backgroundDrawable = _bottomBar.background as GradientDrawable
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xE8000000.toInt()
backgroundDrawable.setColor(semiTransparentColor)
_bottomBar.background = backgroundDrawable
}
val colorOverflow = this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
.getBoolean("colorOverflow", false)
if (!colorOverflow) {
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
}
var doubleBackToExitPressedOnce = false
onBackPressedDispatcher.addCallback(this) {
if (doubleBackToExitPressedOnce) {
finishAffinity()
}
doubleBackToExitPressedOnce = true
snackString(this@NoInternet.getString(R.string.back_to_exit))
Handler(Looper.getMainLooper()).postDelayed(
{ doubleBackToExitPressedOnce = false },
2000
)
}
binding.root.doOnAttach {
initActivity(this)
uiSettings = loadData("ui_settings") ?: uiSettings
selectedOption = uiSettings.defaultStartUpTab
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight bottomMargin = navBarHeight
} }
binding.refreshButton.setOnClickListener { }
if (isOnline(this)) { val navbar = binding.includedNavbar.navbar
startMainActivity(this) ani.dantotsu.bottomBar = navbar
navbar.visibility = View.VISIBLE
val mainViewPager = binding.viewpager
mainViewPager.isUserInputEnabled = false
mainViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle)
mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
navbar.setOnTabSelectListener(object :
AnimatedBottomBar.OnTabSelectListener {
override fun onTabSelected(
lastIndex: Int,
lastTab: AnimatedBottomBar.Tab?,
newIndex: Int,
newTab: AnimatedBottomBar.Tab
) {
navbar.animate().translationZ(12f).setDuration(200).start()
selectedOption = newIndex
mainViewPager.setCurrentItem(newIndex, false)
}
})
navbar.selectTabAt(selectedOption)
//supportFragmentManager.beginTransaction().replace(binding.fragmentContainer.id, OfflineFragment()).commit()
}
private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :
FragmentStateAdapter(fragmentManager, lifecycle) {
override fun getItemCount(): Int = 3
override fun createFragment(position: Int): Fragment {
return when (position) {
0 -> OfflineAnimeFragment()
2 -> OfflineMangaFragment()
else -> OfflineFragment()
} }
} }
} }

View File

@@ -12,9 +12,17 @@ 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 ani.dantotsu.* import ani.dantotsu.EmptyAdapter
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.databinding.ActivityAuthorBinding import ani.dantotsu.databinding.ActivityAuthorBinding
import ani.dantotsu.initActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.others.LangSet
import ani.dantotsu.others.getSerialized import ani.dantotsu.others.getSerialized
import ani.dantotsu.px
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -28,6 +36,8 @@ class AuthorActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityAuthorBinding.inflate(layoutInflater) binding = ActivityAuthorBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)

View File

@@ -2,16 +2,27 @@ package ani.dantotsu.media
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.util.TypedValue
import android.view.View import android.view.View
import android.view.ViewGroup
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
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.Refresh import ani.dantotsu.Refresh
import ani.dantotsu.databinding.ActivityListBinding import ani.dantotsu.databinding.ActivityListBinding
import ani.dantotsu.loadData
import ani.dantotsu.media.user.ListViewPagerAdapter import ani.dantotsu.media.user.ListViewPagerAdapter
import ani.dantotsu.navBarHeight
import ani.dantotsu.others.LangSet
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -27,10 +38,55 @@ class CalendarActivity : AppCompatActivity() {
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityListBinding.inflate(layoutInflater) binding = ActivityListBinding.inflate(layoutInflater)
val typedValue = TypedValue()
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
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()
theme.resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue3, true)
val primaryTextColor = typedValue3.data
val typedValue4 = TypedValue()
theme.resolveAttribute(com.google.android.material.R.attr.colorOutline, typedValue4, true)
val secondaryTextColor = typedValue4.data
window.statusBarColor = primaryColor
window.navigationBarColor = primaryColor
binding.listTabLayout.setBackgroundColor(primaryColor)
binding.listAppBar.setBackgroundColor(primaryColor)
binding.listTitle.setTextColor(primaryTextColor)
binding.listTabLayout.setTabTextColors(secondaryTextColor, primaryTextColor)
binding.listTabLayout.setSelectedTabIndicatorColor(primaryTextColor)
val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
if (!uiSettings.immersiveMode) {
this.window.statusBarColor =
ContextCompat.getColor(this, R.color.nav_bg_inv)
binding.root.fitsSystemWindows = true
} else {
binding.root.fitsSystemWindows = false
requestWindowFeature(Window.FEATURE_NO_TITLE)
window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight
bottomMargin = navBarHeight
}
}
setContentView(binding.root) setContentView(binding.root)
window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg)
binding.listTitle.setText(R.string.release_calendar) binding.listTitle.setText(R.string.release_calendar)
binding.listSort.visibility = View.GONE binding.listSort.visibility = View.GONE
@@ -38,6 +94,7 @@ class CalendarActivity : AppCompatActivity() {
override fun onTabSelected(tab: TabLayout.Tab?) { override fun onTabSelected(tab: TabLayout.Tab?) {
this@CalendarActivity.selectedTabIdx = tab?.position ?: 1 this@CalendarActivity.selectedTabIdx = tab?.position ?: 1
} }
override fun onTabUnselected(tab: TabLayout.Tab?) {} override fun onTabUnselected(tab: TabLayout.Tab?) {}
override fun onTabReselected(tab: TabLayout.Tab?) {} override fun onTabReselected(tab: TabLayout.Tab?) {}
}) })

View File

@@ -21,11 +21,13 @@ class CharacterAdapter(
private val characterList: ArrayList<Character> private val characterList: ArrayList<Character>
) : RecyclerView.Adapter<CharacterAdapter.CharacterViewHolder>() { ) : RecyclerView.Adapter<CharacterAdapter.CharacterViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CharacterViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CharacterViewHolder {
val binding = ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding =
ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return CharacterViewHolder(binding) return CharacterViewHolder(binding)
} }
private val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings() private val uiSettings =
loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) { override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) {
@@ -38,16 +40,23 @@ class CharacterAdapter(
} }
override fun getItemCount(): Int = characterList.size override fun getItemCount(): Int = characterList.size
inner class CharacterViewHolder(val binding: ItemCharacterBinding) : RecyclerView.ViewHolder(binding.root) { inner class CharacterViewHolder(val binding: ItemCharacterBinding) :
RecyclerView.ViewHolder(binding.root) {
init { init {
itemView.setOnClickListener { itemView.setOnClickListener {
val char = characterList[bindingAdapterPosition] val char = characterList[bindingAdapterPosition]
ContextCompat.startActivity( ContextCompat.startActivity(
itemView.context, itemView.context,
Intent(itemView.context, CharacterDetailsActivity::class.java).putExtra("character", char as Serializable), Intent(
itemView.context,
CharacterDetailsActivity::class.java
).putExtra("character", char as Serializable),
ActivityOptionsCompat.makeSceneTransitionAnimation( ActivityOptionsCompat.makeSceneTransitionAnimation(
itemView.context as Activity, itemView.context as Activity,
Pair.create(binding.itemCompactImage, ViewCompat.getTransitionName(binding.itemCompactImage)!!), Pair.create(
binding.itemCompactImage,
ViewCompat.getTransitionName(binding.itemCompactImage)!!
),
).toBundle() ).toBundle()
) )
} }

View File

@@ -13,11 +13,20 @@ 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 ani.dantotsu.* import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.databinding.ActivityCharacterBinding import ani.dantotsu.databinding.ActivityCharacterBinding
import ani.dantotsu.initActivity
import ani.dantotsu.loadData
import ani.dantotsu.loadImage
import ani.dantotsu.navBarHeight
import ani.dantotsu.others.ImageViewDialog import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.others.LangSet
import ani.dantotsu.others.getSerialized import ani.dantotsu.others.getSerialized
import ani.dantotsu.px
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -33,14 +42,18 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityCharacterBinding.inflate(layoutInflater) binding = ActivityCharacterBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
initActivity(this) initActivity(this)
screenWidth = resources.displayMetrics.run { widthPixels / density } screenWidth = resources.displayMetrics.run { widthPixels / density }
if (uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.status) if (uiSettings.immersiveMode) this.window.statusBarColor =
ContextCompat.getColor(this, R.color.status)
val banner = if (uiSettings.bannerAnimations) binding.characterBanner else binding.characterBannerNoKen val banner =
if (uiSettings.bannerAnimations) binding.characterBanner else binding.characterBannerNoKen
banner.updateLayoutParams { height += statusBarHeight } banner.updateLayoutParams { height += statusBarHeight }
binding.characterClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight } binding.characterClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
@@ -57,7 +70,13 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
binding.characterTitle.text = character.name binding.characterTitle.text = character.name
banner.loadImage(character.banner) banner.loadImage(character.banner)
binding.characterCoverImage.loadImage(character.image) binding.characterCoverImage.loadImage(character.image)
binding.characterCoverImage.setOnLongClickListener { ImageViewDialog.newInstance(this, character.name, character.image) } binding.characterCoverImage.setOnLongClickListener {
ImageViewDialog.newInstance(
this,
character.name,
character.image
)
}
model.getCharacter().observe(this) { model.getCharacter().observe(this) {
if (it != null && !loaded) { if (it != null && !loaded) {
@@ -69,7 +88,8 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
val roles = character.roles val roles = character.roles
if (roles != null) { if (roles != null) {
val mediaAdaptor = MediaAdaptor(0, roles, this, matchParent = true) val mediaAdaptor = MediaAdaptor(0, roles, this, matchParent = true)
val concatAdaptor = ConcatAdapter(CharacterDetailsAdapter(character, this), mediaAdaptor) val concatAdaptor =
ConcatAdapter(CharacterDetailsAdapter(character, this), mediaAdaptor)
val gridSize = (screenWidth / 124f).toInt() val gridSize = (screenWidth / 124f).toInt()
val gridLayoutManager = GridLayoutManager(this, gridSize) val gridLayoutManager = GridLayoutManager(this, gridSize)
@@ -114,16 +134,19 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
binding.characterCover.scaleY = 1f * cap binding.characterCover.scaleY = 1f * cap
binding.characterCover.cardElevation = 32f * cap binding.characterCover.cardElevation = 32f * cap
binding.characterCover.visibility = if (binding.characterCover.scaleX == 0f) View.GONE else View.VISIBLE binding.characterCover.visibility =
if (binding.characterCover.scaleX == 0f) View.GONE else View.VISIBLE
if (percentage >= percent && !isCollapsed) { if (percentage >= percent && !isCollapsed) {
isCollapsed = true isCollapsed = true
if (uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg) if (uiSettings.immersiveMode) this.window.statusBarColor =
ContextCompat.getColor(this, R.color.nav_bg)
binding.characterAppBar.setBackgroundResource(R.color.nav_bg) binding.characterAppBar.setBackgroundResource(R.color.nav_bg)
} }
if (percentage <= percent && isCollapsed) { if (percentage <= percent && isCollapsed) {
isCollapsed = false isCollapsed = false
if (uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.status) if (uiSettings.immersiveMode) this.window.statusBarColor =
ContextCompat.getColor(this, R.color.status)
binding.characterAppBar.setBackgroundResource(R.color.bg) binding.characterAppBar.setBackgroundResource(R.color.bg)
} }
} }

View File

@@ -15,7 +15,8 @@ import io.noties.markwon.SoftBreakAddsNewLinePlugin
class CharacterDetailsAdapter(private val character: Character, private val activity: Activity) : class CharacterDetailsAdapter(private val character: Character, private val activity: Activity) :
RecyclerView.Adapter<CharacterDetailsAdapter.GenreViewHolder>() { RecyclerView.Adapter<CharacterDetailsAdapter.GenreViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GenreViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GenreViewHolder {
val binding = ItemCharacterDetailsBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding =
ItemCharacterDetailsBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return GenreViewHolder(binding) return GenreViewHolder(binding)
} }
@@ -32,11 +33,13 @@ class CharacterDetailsAdapter(private val character: Character, private val acti
} else "") + "\n" + character.description } else "") + "\n" + character.description
binding.characterDesc.isTextSelectable binding.characterDesc.isTextSelectable
val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create()).usePlugin(SpoilerPlugin()).build() val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create())
.usePlugin(SpoilerPlugin()).build()
markWon.setMarkdown(binding.characterDesc, desc) markWon.setMarkdown(binding.characterDesc, desc)
} }
override fun getItemCount(): Int = 1 override fun getItemCount(): Int = 1
inner class GenreViewHolder(val binding: ItemCharacterDetailsBinding) : RecyclerView.ViewHolder(binding.root) inner class GenreViewHolder(val binding: ItemCharacterDetailsBinding) :
RecyclerView.ViewHolder(binding.root)
} }

View File

@@ -14,7 +14,9 @@ import ani.dantotsu.databinding.ActivityGenreBinding
import ani.dantotsu.initActivity import ani.dantotsu.initActivity
import ani.dantotsu.loadData import ani.dantotsu.loadData
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.others.LangSet
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -25,6 +27,8 @@ class GenreActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityGenreBinding.inflate(layoutInflater) binding = ActivityGenreBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
initActivity(this) initActivity(this)
@@ -46,7 +50,8 @@ class GenreActivity : AppCompatActivity() {
model.doneListener?.invoke() model.doneListener?.invoke()
} }
binding.mediaInfoGenresRecyclerView.adapter = adapter binding.mediaInfoGenresRecyclerView.adapter = adapter
binding.mediaInfoGenresRecyclerView.layoutManager = GridLayoutManager(this, (screenWidth / 156f).toInt()) binding.mediaInfoGenresRecyclerView.layoutManager =
GridLayoutManager(this, (screenWidth / 156f).toInt())
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
model.loadGenres(Anilist.genres ?: loadData("genres_list") ?: arrayListOf()) { model.loadGenres(Anilist.genres ?: loadData("genres_list") ?: arrayListOf()) {

View File

@@ -37,7 +37,8 @@ class GenreAdapter(
} }
override fun getItemCount(): Int = genres.size override fun getItemCount(): Int = genres.size
inner class GenreViewHolder(val binding: ItemGenreBinding) : RecyclerView.ViewHolder(binding.root) { inner class GenreViewHolder(val binding: ItemGenreBinding) :
RecyclerView.ViewHolder(binding.root) {
init { init {
itemView.setOnClickListener { itemView.setOnClickListener {
ContextCompat.startActivity( ContextCompat.startActivity(

View File

@@ -1,5 +1,6 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.graphics.Bitmap
import ani.dantotsu.connections.anilist.api.FuzzyDate import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.anilist.api.MediaEdge import ani.dantotsu.connections.anilist.api.MediaEdge
import ani.dantotsu.connections.anilist.api.MediaList import ani.dantotsu.connections.anilist.api.MediaList
@@ -22,7 +23,7 @@ data class Media(
val userPreferredName: String, val userPreferredName: String,
var cover: String? = null, var cover: String? = null,
val banner: String? = null, var banner: String? = null,
var relation: String? = null, var relation: String? = null,
var popularity: Int? = null, var popularity: Int? = null,
@@ -117,6 +118,20 @@ data class Media(
fun mangaName() = if (countryOfOrigin != "JP") mainName() else nameRomaji fun mangaName() = if (countryOfOrigin != "JP") mainName() else nameRomaji
} }
fun emptyMedia() = Media(
id = 0,
name = "No media found",
nameRomaji = "No media found",
userPreferredName = "",
isAdult = false,
isFav = false,
isListPrivate = false,
userScore = 0,
userStatus = "",
format = "",
)
object MediaSingleton { object MediaSingleton {
var media: Media? = null var media: Media? = null
var bitmap: Bitmap? = null
} }

View File

@@ -4,12 +4,19 @@ import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.BitmapDrawable
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.AccelerateDecelerateInterpolator import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.ImageView
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.util.Pair
import androidx.core.view.ViewCompat
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@@ -37,13 +44,35 @@ class MediaAdaptor(
private val viewPager: ViewPager2? = null, private val viewPager: ViewPager2? = null,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings() private val uiSettings =
loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (type) { return when (type) {
0 -> MediaViewHolder(ItemMediaCompactBinding.inflate(LayoutInflater.from(parent.context), parent, false)) 0 -> MediaViewHolder(
1 -> MediaLargeViewHolder(ItemMediaLargeBinding.inflate(LayoutInflater.from(parent.context), parent, false)) ItemMediaCompactBinding.inflate(
2 -> MediaPageViewHolder(ItemMediaPageBinding.inflate(LayoutInflater.from(parent.context), parent, false)) LayoutInflater.from(parent.context),
parent,
false
)
)
1 -> MediaLargeViewHolder(
ItemMediaLargeBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
2 -> MediaPageViewHolder(
ItemMediaPageBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
3 -> MediaPageSmallViewHolder( 3 -> MediaPageSmallViewHolder(
ItemMediaPageSmallBinding.inflate( ItemMediaPageSmallBinding.inflate(
LayoutInflater.from(parent.context), LayoutInflater.from(parent.context),
@@ -51,6 +80,7 @@ class MediaAdaptor(
false false
) )
) )
else -> throw IllegalArgumentException() else -> throw IllegalArgumentException()
} }
@@ -65,10 +95,12 @@ 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 = if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE b.itemCompactOngoing.visibility =
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
b.itemCompactTitle.text = media.userPreferredName b.itemCompactTitle.text = media.userPreferredName
b.itemCompactScore.text = b.itemCompactScore.text =
((if (media.userScore == 0) (media.meanScore ?: 0) else media.userScore) / 10.0).toString() ((if (media.userScore == 0) (media.meanScore
?: 0) else media.userScore) / 10.0).toString()
b.itemCompactScoreBG.background = ContextCompat.getDrawable( b.itemCompactScoreBG.background = ContextCompat.getDrawable(
b.root.context, b.root.context,
(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)
@@ -100,29 +132,37 @@ class MediaAdaptor(
} }
} }
} }
1 -> { 1 -> {
val b = (holder as MediaLargeViewHolder).binding val b = (holder as MediaLargeViewHolder).binding
setAnimation(activity, b.root, uiSettings) setAnimation(activity, b.root, uiSettings)
val media = mediaList?.get(position) val media = mediaList?.get(position)
if (media != null) { if (media != null) {
b.itemCompactImage.loadImage(media.cover) b.itemCompactImage.loadImage(media.cover)
b.itemCompactBanner.loadImage(media.banner ?: media.cover, 400) b.itemCompactBanner.loadImage(media.banner ?: media.cover)
b.itemCompactOngoing.visibility = if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE b.itemCompactOngoing.visibility =
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
b.itemCompactTitle.text = media.userPreferredName b.itemCompactTitle.text = media.userPreferredName
b.itemCompactScore.text = b.itemCompactScore.text =
((if (media.userScore == 0) (media.meanScore ?: 0) else media.userScore) / 10.0).toString() ((if (media.userScore == 0) (media.meanScore
?: 0) else media.userScore) / 10.0).toString()
b.itemCompactScoreBG.background = ContextCompat.getDrawable( b.itemCompactScoreBG.background = ContextCompat.getDrawable(
b.root.context, b.root.context,
(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 ?: 0) != 1) currActivity()!!.getString(R.string.episode_plural) b.itemTotal.text = " " + if ((media.anime.totalEpisodes
?: 0) != 1
) currActivity()!!.getString(R.string.episode_plural)
else currActivity()!!.getString(R.string.episode_singular) else currActivity()!!.getString(R.string.episode_singular)
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() ?: "??").toString()) else (media.anime.totalEpisodes
?: "??").toString()
} else if (media.manga != null) { } else if (media.manga != null) {
b.itemTotal.text = " " + if ((media.manga.totalChapters ?: 0) != 1) currActivity()!!.getString(R.string.chapter_plural) b.itemTotal.text = " " + if ((media.manga.totalChapters
?: 0) != 1
) currActivity()!!.getString(R.string.chapter_plural)
else currActivity()!!.getString(R.string.chapter_singular) else currActivity()!!.getString(R.string.chapter_singular)
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}" b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
} }
@@ -133,6 +173,7 @@ class MediaAdaptor(
} }
} }
} }
2 -> { 2 -> {
val b = (holder as MediaPageViewHolder).binding val b = (holder as MediaPageViewHolder).binding
val media = mediaList?.get(position) val media = mediaList?.get(position)
@@ -145,7 +186,8 @@ class MediaAdaptor(
AccelerateDecelerateInterpolator() AccelerateDecelerateInterpolator()
) )
) )
val banner = if (uiSettings.bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen val banner =
if (uiSettings.bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
val context = b.itemCompactBanner.context val context = b.itemCompactBanner.context
if (!(context as Activity).isDestroyed) if (!(context as Activity).isDestroyed)
Glide.with(context as Context) Glide.with(context as Context)
@@ -153,22 +195,29 @@ class MediaAdaptor(
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400) .diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3))) .apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
.into(banner) .into(banner)
b.itemCompactOngoing.visibility = if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE b.itemCompactOngoing.visibility =
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
b.itemCompactTitle.text = media.userPreferredName b.itemCompactTitle.text = media.userPreferredName
b.itemCompactScore.text = b.itemCompactScore.text =
((if (media.userScore == 0) (media.meanScore ?: 0) else media.userScore) / 10.0).toString() ((if (media.userScore == 0) (media.meanScore
?: 0) else media.userScore) / 10.0).toString()
b.itemCompactScoreBG.background = ContextCompat.getDrawable( b.itemCompactScoreBG.background = ContextCompat.getDrawable(
b.root.context, b.root.context,
(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 ?: 0) != 1) currActivity()!!.getString(R.string.episode_plural) b.itemTotal.text = " " + if ((media.anime.totalEpisodes
?: 0) != 1
) currActivity()!!.getString(R.string.episode_plural)
else currActivity()!!.getString(R.string.episode_singular) else currActivity()!!.getString(R.string.episode_singular)
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() ?: "??").toString()) else (media.anime.totalEpisodes
?: "??").toString()
} else if (media.manga != null) { } else if (media.manga != null) {
b.itemTotal.text =" " + if ((media.manga.totalChapters ?: 0) != 1) currActivity()!!.getString(R.string.chapter_plural) b.itemTotal.text = " " + if ((media.manga.totalChapters
?: 0) != 1
) currActivity()!!.getString(R.string.chapter_plural)
else currActivity()!!.getString(R.string.chapter_singular) else currActivity()!!.getString(R.string.chapter_singular)
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}" b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
} }
@@ -180,6 +229,7 @@ class MediaAdaptor(
} }
} }
} }
3 -> { 3 -> {
val b = (holder as MediaPageSmallViewHolder).binding val b = (holder as MediaPageSmallViewHolder).binding
val media = mediaList?.get(position) val media = mediaList?.get(position)
@@ -192,7 +242,8 @@ class MediaAdaptor(
AccelerateDecelerateInterpolator() AccelerateDecelerateInterpolator()
) )
) )
val banner = if (uiSettings.bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen val banner =
if (uiSettings.bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
val context = b.itemCompactBanner.context val context = b.itemCompactBanner.context
if (!(context as Activity).isDestroyed) if (!(context as Activity).isDestroyed)
Glide.with(context as Context) Glide.with(context as Context)
@@ -200,10 +251,12 @@ class MediaAdaptor(
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400) .diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3))) .apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
.into(banner) .into(banner)
b.itemCompactOngoing.visibility = if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE b.itemCompactOngoing.visibility =
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
b.itemCompactTitle.text = media.userPreferredName b.itemCompactTitle.text = media.userPreferredName
b.itemCompactScore.text = b.itemCompactScore.text =
((if (media.userScore == 0) (media.meanScore ?: 0) else media.userScore) / 10.0).toString() ((if (media.userScore == 0) (media.meanScore
?: 0) else media.userScore) / 10.0).toString()
b.itemCompactScoreBG.background = ContextCompat.getDrawable( b.itemCompactScoreBG.background = ContextCompat.getDrawable(
b.root.context, b.root.context,
(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)
@@ -218,13 +271,18 @@ class MediaAdaptor(
} }
b.itemCompactStatus.text = media.status ?: "" b.itemCompactStatus.text = media.status ?: ""
if (media.anime != null) { if (media.anime != null) {
b.itemTotal.text = " " + if ((media.anime.totalEpisodes ?: 0) != 1) currActivity()!!.getString(R.string.episode_plural) b.itemTotal.text = " " + if ((media.anime.totalEpisodes
?: 0) != 1
) currActivity()!!.getString(R.string.episode_plural)
else currActivity()!!.getString(R.string.episode_singular) else currActivity()!!.getString(R.string.episode_singular)
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() ?: "??").toString()) else (media.anime.totalEpisodes
?: "??").toString()
} else if (media.manga != null) { } else if (media.manga != null) {
b.itemTotal.text = " " + if ((media.manga.totalChapters ?: 0) != 1) currActivity()!!.getString(R.string.chapter_plural) b.itemTotal.text = " " + if ((media.manga.totalChapters
?: 0) != 1
) currActivity()!!.getString(R.string.chapter_plural)
else currActivity()!!.getString(R.string.chapter_singular) else currActivity()!!.getString(R.string.chapter_singular)
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}" b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
} }
@@ -245,61 +303,163 @@ class MediaAdaptor(
return type return type
} }
inner class MediaViewHolder(val binding: ItemMediaCompactBinding) : RecyclerView.ViewHolder(binding.root) { fun randomOptionClick() {
val media = if (!mediaList.isNullOrEmpty()) {
mediaList.random()
} else {
null
}
media?.let {
val index = mediaList?.indexOf(it) ?: -1
clicked(index, null)
}
}
inner class MediaViewHolder(val binding: ItemMediaCompactBinding) :
RecyclerView.ViewHolder(binding.root) {
init { init {
if (matchParent) itemView.updateLayoutParams { width = -1 } if (matchParent) itemView.updateLayoutParams { width = -1 }
itemView.setSafeOnClickListener { clicked(bindingAdapterPosition) } itemView.setSafeOnClickListener {
clicked(
bindingAdapterPosition,
binding.itemCompactImage,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
)
}
itemView.setOnLongClickListener { longClicked(bindingAdapterPosition) } itemView.setOnLongClickListener { longClicked(bindingAdapterPosition) }
} }
} }
inner class MediaLargeViewHolder(val binding: ItemMediaLargeBinding) : RecyclerView.ViewHolder(binding.root) { inner class MediaLargeViewHolder(val binding: ItemMediaLargeBinding) :
RecyclerView.ViewHolder(binding.root) {
init { init {
itemView.setSafeOnClickListener { clicked(bindingAdapterPosition) } itemView.setSafeOnClickListener {
clicked(
bindingAdapterPosition,
binding.itemCompactImage,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
)
}
itemView.setOnLongClickListener { longClicked(bindingAdapterPosition) } itemView.setOnLongClickListener { longClicked(bindingAdapterPosition) }
} }
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
inner class MediaPageViewHolder(val binding: ItemMediaPageBinding) : RecyclerView.ViewHolder(binding.root) { inner class MediaPageViewHolder(val binding: ItemMediaPageBinding) :
RecyclerView.ViewHolder(binding.root) {
init { init {
binding.itemCompactImage.setSafeOnClickListener { clicked(bindingAdapterPosition) } binding.itemCompactImage.setSafeOnClickListener {
clicked(
bindingAdapterPosition,
binding.itemCompactImage,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
)
}
itemView.setOnTouchListener { _, _ -> true } itemView.setOnTouchListener { _, _ -> true }
binding.itemCompactImage.setOnLongClickListener { longClicked(bindingAdapterPosition) } binding.itemCompactImage.setOnLongClickListener { longClicked(bindingAdapterPosition) }
} }
} }
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
inner class MediaPageSmallViewHolder(val binding: ItemMediaPageSmallBinding) : RecyclerView.ViewHolder(binding.root) { inner class MediaPageSmallViewHolder(val binding: ItemMediaPageSmallBinding) :
RecyclerView.ViewHolder(binding.root) {
init { init {
binding.itemCompactImage.setSafeOnClickListener { clicked(bindingAdapterPosition) } binding.itemCompactImage.setSafeOnClickListener {
binding.itemCompactTitleContainer.setSafeOnClickListener { clicked(bindingAdapterPosition) } clicked(
bindingAdapterPosition,
binding.itemCompactImage,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
)
}
binding.itemCompactTitleContainer.setSafeOnClickListener {
clicked(
bindingAdapterPosition,
binding.itemCompactImage,
resizeBitmap(getBitmapFromImageView(binding.itemCompactImage), 100)
)
}
itemView.setOnTouchListener { _, _ -> true } itemView.setOnTouchListener { _, _ -> true }
binding.itemCompactImage.setOnLongClickListener { longClicked(bindingAdapterPosition) } binding.itemCompactImage.setOnLongClickListener { longClicked(bindingAdapterPosition) }
} }
} }
fun clicked(position: Int) { fun clicked(position: Int, itemCompactImage: ImageView?, bitmap: Bitmap? = null) {
if ((mediaList?.size ?: 0) > position && position != -1) { if ((mediaList?.size ?: 0) > position && position != -1) {
val media = mediaList?.get(position) val media = mediaList?.get(position)
if (bitmap != null) MediaSingleton.bitmap = bitmap
ContextCompat.startActivity( ContextCompat.startActivity(
activity, activity,
Intent(activity, MediaDetailsActivity::class.java).putExtra( Intent(activity, MediaDetailsActivity::class.java).putExtra(
"media", "media",
media as Serializable media as Serializable
), null ),
if (itemCompactImage != null) {
ActivityOptionsCompat.makeSceneTransitionAnimation(
activity,
Pair.create(
itemCompactImage,
ViewCompat.getTransitionName(activity.findViewById(R.id.itemCompactImage))!!
),
).toBundle()
} else {
null
}
) )
} }
} }
fun longClicked(position: Int): Boolean { fun longClicked(position: Int): Boolean {
if ((mediaList?.size ?: 0) > position && position != -1) { if ((mediaList?.size ?: 0) > position && position != -1) {
val media = mediaList?.get(position) ?: return false val media = mediaList?.get(position) ?: return false
if (activity.supportFragmentManager.findFragmentByTag("list") == null) { if (activity.supportFragmentManager.findFragmentByTag("list") == null) {
MediaListDialogSmallFragment.newInstance(media).show(activity.supportFragmentManager, "list") MediaListDialogSmallFragment.newInstance(media)
.show(activity.supportFragmentManager, "list")
return true return true
} }
} }
return false return false
} }
fun getBitmapFromImageView(imageView: ImageView): Bitmap? {
val drawable = imageView.drawable ?: return null
// If the drawable is a BitmapDrawable, then just get the bitmap
if (drawable is BitmapDrawable) {
return drawable.bitmap
}
// Create a bitmap with the same dimensions as the drawable
val bitmap = Bitmap.createBitmap(
drawable.intrinsicWidth,
drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
// Draw the drawable onto the bitmap
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
}
fun resizeBitmap(source: Bitmap?, maxDimension: Int): Bitmap? {
if (source == null) return null
val width = source.width
val height = source.height
val newWidth: Int
val newHeight: Int
if (width > height) {
newWidth = maxDimension
newHeight = (height * (maxDimension.toFloat() / width)).toInt()
} else {
newHeight = maxDimension
newWidth = (width * (maxDimension.toFloat() / height)).toInt()
}
return Bitmap.createScaledBitmap(source, newWidth, newHeight, true)
}
} }

View File

@@ -2,6 +2,7 @@ package ani.dantotsu.media
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
@@ -31,15 +32,15 @@ import ani.dantotsu.R
import ani.dantotsu.Refresh import ani.dantotsu.Refresh
import ani.dantotsu.ZoomOutPageTransformer import ani.dantotsu.ZoomOutPageTransformer
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.media.anime.AnimeWatchFragment
import ani.dantotsu.copyToClipboard import ani.dantotsu.copyToClipboard
import ani.dantotsu.databinding.ActivityMediaBinding import ani.dantotsu.databinding.ActivityMediaBinding
import ani.dantotsu.initActivity import ani.dantotsu.initActivity
import ani.dantotsu.loadData import ani.dantotsu.loadData
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
import ani.dantotsu.media.anime.AnimeWatchFragment
import ani.dantotsu.media.manga.MangaReadFragment import ani.dantotsu.media.manga.MangaReadFragment
import ani.dantotsu.navBarHeight
import ani.dantotsu.media.novel.NovelReadFragment import ani.dantotsu.media.novel.NovelReadFragment
import ani.dantotsu.navBarHeight
import ani.dantotsu.openLinkInBrowser import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.ImageViewDialog import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.others.getSerialized import ani.dantotsu.others.getSerialized
@@ -47,6 +48,7 @@ import ani.dantotsu.saveData
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import com.flaviofaria.kenburnsview.RandomTransitionGenerator import com.flaviofaria.kenburnsview.RandomTransitionGenerator
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.navigation.NavigationBarView import com.google.android.material.navigation.NavigationBarView
@@ -71,6 +73,16 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
@SuppressLint("SetTextI18n", "ClickableViewAccessibility") @SuppressLint("SetTextI18n", "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()
if (media.name == "No media found") {
snackString(media.name)
onBackPressedDispatcher.onBackPressed()
return
}
mediaSingleton = null
ThemeManager(this).applyTheme(MediaSingleton.bitmap)
MediaSingleton.bitmap = null
binding = ActivityMediaBinding.inflate(layoutInflater) binding = ActivityMediaBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
screenWidth = resources.displayMetrics.widthPixels.toFloat() screenWidth = resources.displayMetrics.widthPixels.toFloat()
@@ -79,11 +91,11 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
initActivity(this) initActivity(this)
uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings() uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
if (!uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg_inv)
binding.mediaBanner.updateLayoutParams { height += statusBarHeight } binding.mediaBanner.updateLayoutParams { height += statusBarHeight }
binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight } binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight }
binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight } binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.incognito.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.mediaCollapsing.minimumHeight = statusBarHeight binding.mediaCollapsing.minimumHeight = statusBarHeight
if (binding.mediaTab is CustomBottomNavBar) binding.mediaTab.updateLayoutParams<ViewGroup.MarginLayoutParams> { if (binding.mediaTab is CustomBottomNavBar) binding.mediaTab.updateLayoutParams<ViewGroup.MarginLayoutParams> {
@@ -101,17 +113,22 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
if (uiSettings.bannerAnimations) { if (uiSettings.bannerAnimations) {
val adi = AccelerateDecelerateInterpolator() val adi = AccelerateDecelerateInterpolator()
val generator = RandomTransitionGenerator((10000 + 15000 * (uiSettings.animationSpeed)).toLong(), adi) val generator = RandomTransitionGenerator(
(10000 + 15000 * (uiSettings.animationSpeed)).toLong(),
adi
)
binding.mediaBanner.setTransitionGenerator(generator) binding.mediaBanner.setTransitionGenerator(generator)
} }
val banner = if (uiSettings.bannerAnimations) binding.mediaBanner else binding.mediaBannerNoKen val banner =
if (uiSettings.bannerAnimations) binding.mediaBanner else binding.mediaBannerNoKen
val viewPager = binding.mediaViewPager val viewPager = binding.mediaViewPager
tabLayout = binding.mediaTab as NavigationBarView tabLayout = binding.mediaTab as NavigationBarView
viewPager.isUserInputEnabled = false viewPager.isUserInputEnabled = false
viewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings)) viewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
var media: Media = intent.getSerialized("media") ?: return
media.selected = model.loadSelected(media) val isDownload = intent.getBooleanExtra("download", false)
media.selected = model.loadSelected(media, isDownload)
binding.mediaCoverImage.loadImage(media.cover) binding.mediaCoverImage.loadImage(media.cover)
binding.mediaCoverImage.setOnLongClickListener { binding.mediaCoverImage.setOnLongClickListener {
@@ -142,7 +159,13 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
} }
}) })
banner.setOnTouchListener { _, motionEvent -> gestureDetector.onTouchEvent(motionEvent);true } banner.setOnTouchListener { _, motionEvent -> gestureDetector.onTouchEvent(motionEvent);true }
if (this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
.getBoolean("incognito", false)) {
binding.mediaTitle.text = " ${media.userPreferredName}"
binding.incognito.visibility = View.VISIBLE
}else {
binding.mediaTitle.text = media.userPreferredName binding.mediaTitle.text = media.userPreferredName
}
binding.mediaTitle.setOnLongClickListener { binding.mediaTitle.setOnLongClickListener {
copyToClipboard(media.userPreferredName) copyToClipboard(media.userPreferredName)
true true
@@ -162,13 +185,28 @@ 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,
binding.mediaFav, binding.mediaFav,
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.nav_tab, R.color.bg_opp,
R.color.fav, R.color.violet_400,//TODO: Change to colorSecondary
media.isFav media.isFav
) { ) {
media.isFav = it media.isFav = it
@@ -180,17 +218,36 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
null null
} }
@SuppressLint("ResourceType")
fun total() { fun total() {
val text = SpannableStringBuilder().apply { val text = SpannableStringBuilder().apply {
val white = ContextCompat.getColor(this@MediaDetailsActivity, R.color.bg_opp) val typedValue = TypedValue()
this@MediaDetailsActivity.theme.resolveAttribute(
com.google.android.material.R.attr.colorOnBackground,
typedValue,
true
)
val white = typedValue.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()
theme.resolveAttribute(com.google.android.material.R.attr.colorSecondary, typedValue, true) theme.resolveAttribute(
com.google.android.material.R.attr.colorSecondary,
typedValue,
true
)
bold { color(typedValue.data) { append("${media.userProgress}") } } bold { color(typedValue.data) { append("${media.userProgress}") } }
append(if (media.anime != null) getString(R.string.episodes_out_of) else getString(R.string.chapters_out_of)) append(
if (media.anime != null) getString(R.string.episodes_out_of) else getString(
R.string.chapters_out_of
)
)
} else { } else {
append(if (media.anime != null) getString(R.string.episodes_total_of) else getString(R.string.chapters_total_of)) append(
if (media.anime != null) getString(R.string.episodes_total_of) else getString(
R.string.chapters_total_of
)
)
} }
if (media.anime != null) { if (media.anime != null) {
if (media.anime!!.nextAiringEpisode != null) { if (media.anime!!.nextAiringEpisode != null) {
@@ -206,8 +263,12 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
fun progress() { fun progress() {
val statuses: Array<String> = resources.getStringArray(R.array.status) val statuses: Array<String> = resources.getStringArray(R.array.status)
val statusStrings = if (media.manga==null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(R.array.status_manga) val statusStrings =
val userStatus = if(media.userStatus != null) statusStrings[statuses.indexOf(media.userStatus)] else statusStrings[0] if (media.manga == null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(
R.array.status_manga
)
val userStatus =
if (media.userStatus != null) statusStrings[statuses.indexOf(media.userStatus)] else statusStrings[0]
if (media.userStatus != null) { if (media.userStatus != null) {
binding.mediaTotal.visibility = View.VISIBLE binding.mediaTotal.visibility = View.VISIBLE
@@ -255,14 +316,22 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
} }
adult = media.isAdult adult = media.isAdult
tabLayout.menu.clear() tabLayout.menu.clear()
if (media.anime != null) { if (media.anime != null) {
viewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle, SupportedMedia.ANIME) viewPager.adapter =
ViewPagerAdapter(supportFragmentManager, lifecycle, SupportedMedia.ANIME)
tabLayout.inflateMenu(R.menu.anime_menu_detail) tabLayout.inflateMenu(R.menu.anime_menu_detail)
} else if (media.manga != null) { } else if (media.manga != null) {
viewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle, if(media.format=="NOVEL") SupportedMedia.NOVEL else SupportedMedia.MANGA) viewPager.adapter = ViewPagerAdapter(
supportFragmentManager,
lifecycle,
if (media.format == "NOVEL") SupportedMedia.NOVEL else SupportedMedia.MANGA
)
if (media.format == "NOVEL") {
tabLayout.inflateMenu(R.menu.novel_menu_detail)
} else {
tabLayout.inflateMenu(R.menu.manga_menu_detail) tabLayout.inflateMenu(R.menu.manga_menu_detail)
}
anime = false anime = false
} }
@@ -274,7 +343,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
tabLayout.setOnItemSelectedListener { item -> tabLayout.setOnItemSelectedListener { item ->
selectFromID(item.itemId) selectFromID(item.itemId)
viewPager.setCurrentItem(selected, false) viewPager.setCurrentItem(selected, false)
val sel = model.loadSelected(media) val sel = model.loadSelected(media, isDownload)
sel.window = selected sel.window = selected
model.saveSelected(media.id, sel, this) model.saveSelected(media.id, sel, this)
true true
@@ -306,6 +375,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
R.id.info -> { R.id.info -> {
selected = 0 selected = 0
} }
R.id.watch, R.id.read -> { R.id.watch, R.id.read -> {
selected = 1 selected = 1
} }
@@ -332,6 +402,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
private enum class SupportedMedia { private enum class SupportedMedia {
ANIME, MANGA, NOVEL ANIME, MANGA, NOVEL
} }
//ViewPager //ViewPager
private class ViewPagerAdapter( private class ViewPagerAdapter(
fragmentManager: FragmentManager, fragmentManager: FragmentManager,
@@ -349,6 +420,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
SupportedMedia.MANGA -> MangaReadFragment() SupportedMedia.MANGA -> MangaReadFragment()
SupportedMedia.NOVEL -> NovelReadFragment() SupportedMedia.NOVEL -> NovelReadFragment()
} }
else -> MediaInfoFragment() else -> MediaInfoFragment()
} }
} }
@@ -363,27 +435,43 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
if (mMaxScrollSize == 0) mMaxScrollSize = appBar.totalScrollRange if (mMaxScrollSize == 0) mMaxScrollSize = appBar.totalScrollRange
val percentage = abs(i) * 100 / mMaxScrollSize val percentage = abs(i) * 100 / mMaxScrollSize
binding.mediaCover.visibility = if (binding.mediaCover.scaleX == 0f) View.GONE else View.VISIBLE binding.mediaCover.visibility =
if (binding.mediaCover.scaleX == 0f) View.GONE else View.VISIBLE
val duration = (200 * uiSettings.animationSpeed).toLong() val duration = (200 * uiSettings.animationSpeed).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).start() ObjectAnimator.ofFloat(binding.mediaTitle, "translationX", 0f).setDuration(duration)
ObjectAnimator.ofFloat(binding.mediaAccessContainer, "translationX", screenWidth).setDuration(duration).start() .start()
ObjectAnimator.ofFloat(binding.mediaCover, "translationX", screenWidth).setDuration(duration).start() ObjectAnimator.ofFloat(binding.mediaAccessContainer, "translationX", screenWidth)
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", screenWidth).setDuration(duration).start() .setDuration(duration).start()
ObjectAnimator.ofFloat(binding.mediaCover, "translationX", screenWidth)
.setDuration(duration).start()
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", screenWidth)
.setDuration(duration).start()
binding.mediaBanner.pause() binding.mediaBanner.pause()
if (!uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg)
} }
if (percentage <= percent && isCollapsed) { if (percentage <= percent && isCollapsed) {
isCollapsed = false isCollapsed = false
ObjectAnimator.ofFloat(binding.mediaTitle, "translationX", -screenWidth).setDuration(duration).start() ObjectAnimator.ofFloat(binding.mediaTitle, "translationX", -screenWidth)
ObjectAnimator.ofFloat(binding.mediaAccessContainer, "translationX", 0f).setDuration(duration).start() .setDuration(duration).start()
ObjectAnimator.ofFloat(binding.mediaCover, "translationX", 0f).setDuration(duration).start() ObjectAnimator.ofFloat(binding.mediaAccessContainer, "translationX", 0f)
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", 0f).setDuration(duration).start() .setDuration(duration).start()
ObjectAnimator.ofFloat(binding.mediaCover, "translationX", 0f).setDuration(duration)
.start()
ObjectAnimator.ofFloat(binding.mediaCollapseContainer, "translationX", 0f)
.setDuration(duration).start()
if (uiSettings.bannerAnimations) binding.mediaBanner.resume() if (uiSettings.bannerAnimations) binding.mediaBanner.resume()
if (!uiSettings.immersiveMode) this.window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg_inv)
} }
if (percentage == 1 && model.scrolledToTop.value != false) model.scrolledToTop.postValue(false) if (percentage == 1 && model.scrolledToTop.value != false) model.scrolledToTop.postValue(
false
)
if (percentage == 0 && model.scrolledToTop.value != true) model.scrolledToTop.postValue(true) if (percentage == 0 && model.scrolledToTop.value != true) model.scrolledToTop.postValue(true)
} }
@@ -404,6 +492,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
init { init {
enabled(true) enabled(true)
scope.launch { scope.launch {
delay(100) //TODO: a listener would be better
clicked() clicked()
} }
image.setOnClickListener { image.setOnClickListener {
@@ -425,8 +514,10 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
ObjectAnimator.ofFloat(image, "scaleX", 1f, 0f).setDuration(69).start() ObjectAnimator.ofFloat(image, "scaleX", 1f, 0f).setDuration(69).start()
ObjectAnimator.ofFloat(image, "scaleY", 1f, 0f).setDuration(100).start() ObjectAnimator.ofFloat(image, "scaleY", 1f, 0f).setDuration(100).start()
delay(100) delay(100)
if (clicked) { if (clicked) {
ObjectAnimator.ofArgb(image, ObjectAnimator.ofArgb(
image,
"ColorFilter", "ColorFilter",
ContextCompat.getColor(context, c1), ContextCompat.getColor(context, c1),
ContextCompat.getColor(context, c2) ContextCompat.getColor(context, c2)
@@ -439,18 +530,24 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
ObjectAnimator.ofFloat(image, "scaleX", 1.5f, 1f).setDuration(100).start() ObjectAnimator.ofFloat(image, "scaleX", 1.5f, 1f).setDuration(100).start()
ObjectAnimator.ofFloat(image, "scaleY", 1.5f, 1f).setDuration(100).start() ObjectAnimator.ofFloat(image, "scaleY", 1.5f, 1f).setDuration(100).start()
delay(200) delay(200)
if (clicked) ObjectAnimator.ofArgb( if (clicked) {
ObjectAnimator.ofArgb(
image, image,
"ColorFilter", "ColorFilter",
ContextCompat.getColor(context, c2), ContextCompat.getColor(context, c2),
ContextCompat.getColor(context, c1) ContextCompat.getColor(context, c1)
).setDuration(200).start() ).setDuration(200).start()
} }
}
fun enabled(enabled: Boolean) { fun enabled(enabled: Boolean) {
disabled = !enabled disabled = !enabled
image.alpha = if (disabled) 0.33f else 1f image.alpha = if (disabled) 0.33f else 1f
} }
} }
companion object {
var mediaSingleton: Media? = null
}
} }

View File

@@ -1,7 +1,6 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.app.Activity import android.app.Activity
import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
@@ -9,18 +8,22 @@ import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.media.anime.Episode import ani.dantotsu.currContext
import ani.dantotsu.media.anime.SelectorDialogFragment
import ani.dantotsu.loadData import ani.dantotsu.loadData
import ani.dantotsu.logger import ani.dantotsu.logger
import ani.dantotsu.media.anime.Episode
import ani.dantotsu.media.anime.SelectorDialogFragment
import ani.dantotsu.media.manga.MangaChapter import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.others.AniSkip import ani.dantotsu.others.AniSkip
import ani.dantotsu.others.Jikan import ani.dantotsu.others.Jikan
import ani.dantotsu.others.Kitsu import ani.dantotsu.others.Kitsu
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.Book import ani.dantotsu.parsers.Book
import ani.dantotsu.parsers.MangaImage import ani.dantotsu.parsers.MangaImage
import ani.dantotsu.parsers.MangaReadSources import ani.dantotsu.parsers.MangaReadSources
import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.parsers.NovelSources import ani.dantotsu.parsers.NovelSources
import ani.dantotsu.parsers.ShowResponse import ani.dantotsu.parsers.ShowResponse
import ani.dantotsu.parsers.VideoExtractor import ani.dantotsu.parsers.VideoExtractor
@@ -28,20 +31,10 @@ import ani.dantotsu.parsers.WatchSources
import ani.dantotsu.saveData import ani.dantotsu.saveData
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.tryWithSuspend import ani.dantotsu.tryWithSuspend
import ani.dantotsu.currContext
import ani.dantotsu.R
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.AniyomiAdapter
import ani.dantotsu.parsers.DynamicMangaParser
import ani.dantotsu.parsers.HAnimeSources
import ani.dantotsu.parsers.HMangaSources
import ani.dantotsu.parsers.MangaSources
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@@ -53,7 +46,7 @@ class MediaDetailsViewModel : ViewModel() {
} }
fun loadSelected(media: Media): Selected { fun loadSelected(media: Media, isDownload: Boolean = false): Selected {
val sharedPreferences = Injekt.get<SharedPreferences>() val sharedPreferences = Injekt.get<SharedPreferences>()
val data = loadData<Selected>("${media.id}-select") ?: Selected().let { val data = loadData<Selected>("${media.id}-select") ?: Selected().let {
it.sourceIndex = if (media.isAdult) 0 else when (media.anime != null) { it.sourceIndex = if (media.isAdult) 0 else when (media.anime != null) {
@@ -64,13 +57,24 @@ class MediaDetailsViewModel : ViewModel() {
saveSelected(media.id, it) saveSelected(media.id, it)
it it
} }
if (isDownload) {
data.sourceIndex = if (media.anime != null) {
AnimeSources.list.size - 1
} else if (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 { fun loadSelectedStringLocation(sourceName: String): Int {
//find the location of the source in the list //find the location of the source in the list
var location = watchSources?.list?.indexOfFirst { it.name == sourceName } ?: 0 var location = watchSources?.list?.indexOfFirst { it.name == sourceName } ?: 0
if (location == -1) {location = 0} if (location == -1) {
location = 0
}
return location return location
} }
@@ -95,7 +99,9 @@ class MediaDetailsViewModel : ViewModel() {
//Anime //Anime
private val kitsuEpisodes: MutableLiveData<Map<String, Episode>> = MutableLiveData<Map<String, Episode>>(null) private val kitsuEpisodes: MutableLiveData<Map<String, Episode>> =
MutableLiveData<Map<String, Episode>>(null)
fun getKitsuEpisodes(): LiveData<Map<String, Episode>> = kitsuEpisodes fun getKitsuEpisodes(): LiveData<Map<String, Episode>> = kitsuEpisodes
suspend fun loadKitsuEpisodes(s: Media) { suspend fun loadKitsuEpisodes(s: Media) {
tryWithSuspend { tryWithSuspend {
@@ -103,7 +109,9 @@ class MediaDetailsViewModel : ViewModel() {
} }
} }
private val fillerEpisodes: MutableLiveData<Map<String, Episode>> = MutableLiveData<Map<String, Episode>>(null) private val fillerEpisodes: MutableLiveData<Map<String, Episode>> =
MutableLiveData<Map<String, Episode>>(null)
fun getFillerEpisodes(): LiveData<Map<String, Episode>> = fillerEpisodes fun getFillerEpisodes(): LiveData<Map<String, Episode>> = fillerEpisodes
suspend fun loadFillerEpisodes(s: Media) { suspend fun loadFillerEpisodes(s: Media) {
tryWithSuspend { tryWithSuspend {
@@ -120,8 +128,8 @@ class MediaDetailsViewModel : ViewModel() {
private val episodes = MutableLiveData<MutableMap<Int, MutableMap<String, Episode>>>(null) private val episodes = MutableLiveData<MutableMap<Int, MutableMap<String, Episode>>>(null)
private val epsLoaded = mutableMapOf<Int, MutableMap<String, Episode>>() private val epsLoaded = mutableMapOf<Int, MutableMap<String, Episode>>()
fun getEpisodes(): LiveData<MutableMap<Int, MutableMap<String, Episode>>> = episodes fun getEpisodes(): LiveData<MutableMap<Int, MutableMap<String, Episode>>> = episodes
suspend fun loadEpisodes(media: Media, i: Int) { suspend fun loadEpisodes(media: Media, i: Int, invalidate: Boolean = false) {
if (!epsLoaded.containsKey(i)) { if (!epsLoaded.containsKey(i) || invalidate) {
epsLoaded[i] = watchSources?.loadEpisodesFromMedia(i, media) ?: return epsLoaded[i] = watchSources?.loadEpisodesFromMedia(i, media) ?: return
} }
episodes.postValue(epsLoaded) episodes.postValue(epsLoaded)
@@ -134,7 +142,8 @@ class MediaDetailsViewModel : ViewModel() {
suspend fun overrideEpisodes(i: Int, source: ShowResponse, id: Int) { suspend fun overrideEpisodes(i: Int, source: ShowResponse, id: Int) {
watchSources?.saveResponse(i, id, source) watchSources?.saveResponse(i, id, source)
epsLoaded[i] = watchSources?.loadEpisodes(i, source.link, source.extra, source.sAnime) ?: return epsLoaded[i] =
watchSources?.loadEpisodes(i, source.link, source.extra, source.sAnime) ?: return
episodes.postValue(epsLoaded) episodes.postValue(epsLoaded)
} }
@@ -173,7 +182,12 @@ class MediaDetailsViewModel : ViewModel() {
val timeStamps = MutableLiveData<List<AniSkip.Stamp>?>() val timeStamps = MutableLiveData<List<AniSkip.Stamp>?>()
private val timeStampsMap: MutableMap<Int, List<AniSkip.Stamp>?> = mutableMapOf() private val timeStampsMap: MutableMap<Int, List<AniSkip.Stamp>?> = mutableMapOf()
suspend fun loadTimeStamps(malId: Int?, episodeNum: Int?, duration: Long, useProxyForTimeStamps: Boolean) { suspend fun loadTimeStamps(
malId: Int?,
episodeNum: Int?,
duration: Long,
useProxyForTimeStamps: Boolean
) {
malId ?: return malId ?: return
episodeNum ?: return episodeNum ?: return
if (timeStampsMap.containsKey(episodeNum)) if (timeStampsMap.containsKey(episodeNum))
@@ -183,7 +197,11 @@ class MediaDetailsViewModel : ViewModel() {
timeStamps.postValue(result) timeStamps.postValue(result)
} }
suspend fun loadEpisodeSingleVideo(ep: Episode, selected: Selected, post: Boolean = true): Boolean { suspend fun loadEpisodeSingleVideo(
ep: Episode,
selected: Selected,
post: Boolean = true
): Boolean {
if (ep.extractors.isNullOrEmpty()) { if (ep.extractors.isNullOrEmpty()) {
val server = selected.server ?: return false val server = selected.server ?: return false
@@ -193,8 +211,10 @@ class MediaDetailsViewModel : ViewModel() {
selected.sourceIndex = selected.sourceIndex selected.sourceIndex = selected.sourceIndex
if (!post && !it.allowsPreloading) null if (!post && !it.allowsPreloading) null
else ep.sEpisode?.let { it1 -> else ep.sEpisode?.let { it1 ->
it.loadSingleVideoServer(server, link, ep.extra, it.loadSingleVideoServer(
it1, post) server, link, ep.extra,
it1, post
)
} }
} ?: return false) } ?: return false)
ep.allStreams = false ep.allStreams = false
@@ -217,7 +237,14 @@ class MediaDetailsViewModel : ViewModel() {
} }
val epChanged = MutableLiveData(true) val epChanged = MutableLiveData(true)
fun onEpisodeClick(media: Media, i: String, manager: FragmentManager, launch: Boolean = true, prevEp: String? = null) { fun onEpisodeClick(
media: Media,
i: String,
manager: FragmentManager,
launch: Boolean = true,
prevEp: String? = null,
isDownload: Boolean = false
) {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
if (manager.findFragmentByTag("dialog") == null && !manager.isDestroyed) { if (manager.findFragmentByTag("dialog") == null && !manager.isDestroyed) {
if (media.anime?.episodes?.get(i) != null) { if (media.anime?.episodes?.get(i) != null) {
@@ -227,23 +254,32 @@ class MediaDetailsViewModel : ViewModel() {
return@post return@post
} }
media.selected = this.loadSelected(media) media.selected = this.loadSelected(media)
val selector = SelectorDialogFragment.newInstance(media.selected!!.server, launch, prevEp) val selector =
SelectorDialogFragment.newInstance(
media.selected!!.server,
launch,
prevEp,
isDownload
)
selector.show(manager, "dialog") selector.show(manager, "dialog")
} }
} }
} }
//Manga //Manga
var mangaReadSources: MangaReadSources? = null var mangaReadSources: MangaReadSources? = null
private val mangaChapters = MutableLiveData<MutableMap<Int, MutableMap<String, MangaChapter>>>(null) private val mangaChapters =
MutableLiveData<MutableMap<Int, MutableMap<String, MangaChapter>>>(null)
private val mangaLoaded = mutableMapOf<Int, MutableMap<String, MangaChapter>>() private val mangaLoaded = mutableMapOf<Int, MutableMap<String, MangaChapter>>()
fun getMangaChapters(): LiveData<MutableMap<Int, MutableMap<String, MangaChapter>>> = mangaChapters fun getMangaChapters(): LiveData<MutableMap<Int, MutableMap<String, MangaChapter>>> =
suspend fun loadMangaChapters(media: Media, i: Int) { mangaChapters
suspend fun loadMangaChapters(media: Media, i: Int, invalidate: Boolean = false) {
logger("Loading Manga Chapters : $mangaLoaded") logger("Loading Manga Chapters : $mangaLoaded")
if (!mangaLoaded.containsKey(i)) tryWithSuspend { if (!mangaLoaded.containsKey(i) || invalidate) tryWithSuspend {
mangaLoaded[i] = mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend mangaLoaded[i] =
mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend
} }
mangaChapters.postValue(mangaLoaded) mangaChapters.postValue(mangaLoaded)
} }
@@ -258,10 +294,17 @@ class MediaDetailsViewModel : ViewModel() {
private val mangaChapter = MutableLiveData<MangaChapter?>(null) private val mangaChapter = MutableLiveData<MangaChapter?>(null)
fun getMangaChapter(): LiveData<MangaChapter?> = mangaChapter fun getMangaChapter(): LiveData<MangaChapter?> = mangaChapter
suspend fun loadMangaChapterImages(chapter: MangaChapter, selected: Selected, post: Boolean = true): Boolean { suspend fun loadMangaChapterImages(
chapter: MangaChapter,
selected: Selected,
series: String,
post: Boolean = true
): Boolean {
return tryWithSuspend(true) { return tryWithSuspend(true) {
chapter.addImages( chapter.addImages(
mangaReadSources?.get(selected.sourceIndex)?.loadImages(chapter.link, chapter.sChapter) ?: return@tryWithSuspend false mangaReadSources?.get(selected.sourceIndex)
?.loadImages(chapter.link, chapter.sChapter) ?: return@tryWithSuspend false
) )
if (post) mangaChapter.postValue(chapter) if (post) mangaChapter.postValue(chapter)
true true
@@ -269,13 +312,15 @@ class MediaDetailsViewModel : ViewModel() {
} }
fun loadTransformation(mangaImage: MangaImage, source: Int): BitmapTransformation? { fun loadTransformation(mangaImage: MangaImage, source: Int): BitmapTransformation? {
return if (mangaImage.useTransformation) mangaReadSources?.get(source)?.getTransformation() else null return if (mangaImage.useTransformation) mangaReadSources?.get(source)
?.getTransformation() else null
} }
val novelSources = NovelSources val novelSources = NovelSources
val novelResponses = MutableLiveData<List<ShowResponse>>(null) val novelResponses = MutableLiveData<List<ShowResponse>>(null)
suspend fun searchNovels(query: String, i: Int) { suspend fun searchNovels(query: String, i: Int) {
val source = novelSources[i] val position = if (i >= novelSources.list.size) 0 else i
val source = novelSources[position]
tryWithSuspend(post = true) { tryWithSuspend(post = true) {
if (source != null) { if (source != null) {
novelResponses.postValue(source.search(query)) novelResponses.postValue(source.search(query))
@@ -295,7 +340,9 @@ class MediaDetailsViewModel : ViewModel() {
val book: MutableLiveData<Book> = MutableLiveData(null) val book: MutableLiveData<Book> = MutableLiveData(null)
suspend fun loadBook(novel: ShowResponse, i: Int) { suspend fun loadBook(novel: ShowResponse, i: Int) {
tryWithSuspend { tryWithSuspend {
book.postValue(novelSources[i]?.loadBook(novel.link, novel.extra) ?: return@tryWithSuspend) book.postValue(
novelSources[i]?.loadBook(novel.link, novel.extra) ?: return@tryWithSuspend
)
} }
} }

View File

@@ -2,6 +2,7 @@ package ani.dantotsu.media
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@@ -43,7 +44,11 @@ class MediaInfoFragment : Fragment() {
private var type = "ANIME" private var type = "ANIME"
private val genreModel: GenresViewModel by activityViewModels() private val genreModel: GenresViewModel by activityViewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentMediaInfoBinding.inflate(inflater, container, false) _binding = FragmentMediaInfoBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }
@@ -55,6 +60,7 @@ class MediaInfoFragment : Fragment() {
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val model: MediaDetailsViewModel by activityViewModels() val model: MediaDetailsViewModel by activityViewModels()
val offline = requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getBoolean("offlineMode", false) || !isOnline(requireContext())
binding.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE binding.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
binding.mediaInfoContainer.visibility = if (loaded) View.VISIBLE else View.GONE binding.mediaInfoContainer.visibility = if (loaded) View.VISIBLE else View.GONE
binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight } binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight }
@@ -73,13 +79,15 @@ class MediaInfoFragment : Fragment() {
copyToClipboard(media.name ?: media.nameRomaji) copyToClipboard(media.name ?: media.nameRomaji)
true true
} }
if (media.name != null) binding.mediaInfoNameRomajiContainer.visibility = View.VISIBLE if (media.name != null) binding.mediaInfoNameRomajiContainer.visibility =
View.VISIBLE
binding.mediaInfoNameRomaji.text = "\t\t\t" + media.nameRomaji binding.mediaInfoNameRomaji.text = "\t\t\t" + media.nameRomaji
binding.mediaInfoNameRomaji.setOnLongClickListener { binding.mediaInfoNameRomaji.setOnLongClickListener {
copyToClipboard(media.nameRomaji) copyToClipboard(media.nameRomaji)
true true
} }
binding.mediaInfoMeanScore.text = if (media.meanScore != null) (media.meanScore / 10.0).toString() else "??" binding.mediaInfoMeanScore.text =
if (media.meanScore != null) (media.meanScore / 10.0).toString() else "??"
binding.mediaInfoStatus.text = media.status binding.mediaInfoStatus.text = media.status
binding.mediaInfoFormat.text = media.format binding.mediaInfoFormat.text = media.format
binding.mediaInfoSource.text = media.source binding.mediaInfoSource.text = media.source
@@ -95,6 +103,7 @@ class MediaInfoFragment : Fragment() {
if (media.anime.mainStudio != null) { if (media.anime.mainStudio != null) {
binding.mediaInfoStudioContainer.visibility = View.VISIBLE binding.mediaInfoStudioContainer.visibility = View.VISIBLE
binding.mediaInfoStudio.text = media.anime.mainStudio!!.name binding.mediaInfoStudio.text = media.anime.mainStudio!!.name
if (!offline) {
binding.mediaInfoStudioContainer.setOnClickListener { binding.mediaInfoStudioContainer.setOnClickListener {
ContextCompat.startActivity( ContextCompat.startActivity(
requireActivity(), requireActivity(),
@@ -106,9 +115,11 @@ class MediaInfoFragment : Fragment() {
) )
} }
} }
}
if (media.anime.author != null) { if (media.anime.author != null) {
binding.mediaInfoAuthorContainer.visibility = View.VISIBLE binding.mediaInfoAuthorContainer.visibility = View.VISIBLE
binding.mediaInfoAuthor.text = media.anime.author!!.name binding.mediaInfoAuthor.text = media.anime.author!!.name
if (!offline) {
binding.mediaInfoAuthorContainer.setOnClickListener { binding.mediaInfoAuthorContainer.setOnClickListener {
ContextCompat.startActivity( ContextCompat.startActivity(
requireActivity(), requireActivity(),
@@ -120,6 +131,7 @@ class MediaInfoFragment : Fragment() {
) )
} }
} }
}
binding.mediaInfoTotalTitle.setText(R.string.total_eps) binding.mediaInfoTotalTitle.setText(R.string.total_eps)
binding.mediaInfoTotal.text = binding.mediaInfoTotal.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
@@ -131,6 +143,7 @@ class MediaInfoFragment : Fragment() {
if (media.manga.author != null) { if (media.manga.author != null) {
binding.mediaInfoAuthorContainer.visibility = View.VISIBLE binding.mediaInfoAuthorContainer.visibility = View.VISIBLE
binding.mediaInfoAuthor.text = media.manga.author!!.name binding.mediaInfoAuthor.text = media.manga.author!!.name
if (!offline) {
binding.mediaInfoAuthorContainer.setOnClickListener { binding.mediaInfoAuthorContainer.setOnClickListener {
ContextCompat.startActivity( ContextCompat.startActivity(
requireActivity(), requireActivity(),
@@ -143,6 +156,7 @@ class MediaInfoFragment : Fragment() {
} }
} }
} }
}
val desc = HtmlCompat.fromHtml( val desc = HtmlCompat.fromHtml(
(media.description ?: "null").replace("\\n", "<br>").replace("\\\"", "\""), (media.description ?: "null").replace("\\n", "<br>").replace("\\\"", "\""),
@@ -183,7 +197,7 @@ class MediaInfoFragment : Fragment() {
parent.addView(bind.root) parent.addView(bind.root)
} }
if (media.trailer != null) { if (media.trailer != null && !offline) {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
class MyChrome : WebChromeClient() { class MyChrome : WebChromeClient() {
private var mCustomView: View? = null private var mCustomView: View? = null
@@ -237,7 +251,7 @@ class MediaInfoFragment : Fragment() {
parent.addView(bind.root) parent.addView(bind.root)
} }
if (media.anime != null && (media.anime.op.isNotEmpty() || media.anime.ed.isNotEmpty())) { if (media.anime != null && (media.anime.op.isNotEmpty() || media.anime.ed.isNotEmpty()) && !offline) {
val markWon = Markwon.builder(requireContext()) val markWon = Markwon.builder(requireContext())
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build() .usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
@@ -246,7 +260,12 @@ class MediaInfoFragment : Fragment() {
val end = a.indexOf('"', first).let { if (it != -1) it else return a } val end = a.indexOf('"', first).let { if (it != -1) it else return a }
val name = a.subSequence(first, end).toString() val name = a.subSequence(first, end).toString()
return "${a.subSequence(0, first)}" + return "${a.subSequence(0, first)}" +
"[$name](https://www.youtube.com/results?search_query=${URLEncoder.encode(name, "utf-8")})" + "[$name](https://www.youtube.com/results?search_query=${
URLEncoder.encode(
name,
"utf-8"
)
})" +
"${a.subSequence(end, a.length)}" "${a.subSequence(end, a.length)}"
} }
@@ -270,7 +289,11 @@ class MediaInfoFragment : Fragment() {
} }
if (media.anime.op.isNotEmpty()) { if (media.anime.op.isNotEmpty()) {
val bind = ItemTitleTextBinding.inflate(LayoutInflater.from(context), parent, false) val bind = ItemTitleTextBinding.inflate(
LayoutInflater.from(context),
parent,
false
)
bind.itemTitle.setText(R.string.opening) bind.itemTitle.setText(R.string.opening)
makeText(bind.itemText, media.anime.op) makeText(bind.itemText, media.anime.op)
parent.addView(bind.root) parent.addView(bind.root)
@@ -278,14 +301,18 @@ class MediaInfoFragment : Fragment() {
if (media.anime.ed.isNotEmpty()) { if (media.anime.ed.isNotEmpty()) {
val bind = ItemTitleTextBinding.inflate(LayoutInflater.from(context), parent, false) val bind = ItemTitleTextBinding.inflate(
LayoutInflater.from(context),
parent,
false
)
bind.itemTitle.setText(R.string.ending) bind.itemTitle.setText(R.string.ending)
makeText(bind.itemText, media.anime.ed) makeText(bind.itemText, media.anime.ed)
parent.addView(bind.root) parent.addView(bind.root)
} }
} }
if (media.genres.isNotEmpty()) { if (media.genres.isNotEmpty() && !offline) {
val bind = ActivityGenreBinding.inflate( val bind = ActivityGenreBinding.inflate(
LayoutInflater.from(context), LayoutInflater.from(context),
parent, parent,
@@ -316,7 +343,7 @@ class MediaInfoFragment : Fragment() {
parent.addView(bind.root) parent.addView(bind.root)
} }
if (media.tags.isNotEmpty()) { if (media.tags.isNotEmpty() && !offline) {
val bind = ItemTitleChipgroupBinding.inflate( val bind = ItemTitleChipgroupBinding.inflate(
LayoutInflater.from(context), LayoutInflater.from(context),
parent, parent,
@@ -357,7 +384,7 @@ class MediaInfoFragment : Fragment() {
parent.addView(bind.root) parent.addView(bind.root)
} }
if (!media.characters.isNullOrEmpty()) { if (!media.characters.isNullOrEmpty() && !offline) {
val bind = ItemTitleRecyclerBinding.inflate( val bind = ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context), LayoutInflater.from(context),
parent, parent,
@@ -374,7 +401,7 @@ class MediaInfoFragment : Fragment() {
parent.addView(bind.root) parent.addView(bind.root)
} }
if (!media.relations.isNullOrEmpty()) { if (!media.relations.isNullOrEmpty() && !offline) {
if (media.sequel != null || media.prequel != null) { if (media.sequel != null || media.prequel != null) {
val bind = ItemQuelsBinding.inflate( val bind = ItemQuelsBinding.inflate(
LayoutInflater.from(context), LayoutInflater.from(context),
@@ -437,7 +464,7 @@ class MediaInfoFragment : Fragment() {
parent.addView(bindi.root) parent.addView(bindi.root)
} }
if (!media.recommendations.isNullOrEmpty()) { if (!media.recommendations.isNullOrEmpty() && !offline ) {
val bind = ItemTitleRecyclerBinding.inflate( val bind = ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context), LayoutInflater.from(context),
parent, parent,
@@ -458,7 +485,8 @@ class MediaInfoFragment : Fragment() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val cornerTop = ObjectAnimator.ofFloat(binding.root, "radius", 0f, 32f).setDuration(200) val cornerTop = ObjectAnimator.ofFloat(binding.root, "radius", 0f, 32f).setDuration(200)
val cornerNotTop = ObjectAnimator.ofFloat(binding.root, "radius", 32f, 0f).setDuration(200) val cornerNotTop =
ObjectAnimator.ofFloat(binding.root, "radius", 32f, 0f).setDuration(200)
var cornered = true var cornered = true
cornerTop.start() cornerTop.start()
binding.mediaInfoScroll.setOnScrollChangeListener { v, _, _, _, _ -> binding.mediaInfoScroll.setOnScrollChangeListener { v, _, _, _, _ ->

View File

@@ -14,9 +14,9 @@ import androidx.lifecycle.lifecycleScope
import ani.dantotsu.* import ani.dantotsu.*
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.databinding.BottomSheetMediaListBinding
import ani.dantotsu.connections.mal.MAL import ani.dantotsu.connections.mal.MAL
import com.google.android.material.switchmaterial.SwitchMaterial import ani.dantotsu.databinding.BottomSheetMediaListBinding
import com.google.android.material.materialswitch.MaterialSwitch
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -27,7 +27,11 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
private var _binding: BottomSheetMediaListBinding? = null private var _binding: BottomSheetMediaListBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = BottomSheetMediaListBinding.inflate(inflater, container, false) _binding = BottomSheetMediaListBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }
@@ -46,8 +50,12 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
binding.mediaListLayout.visibility = View.VISIBLE binding.mediaListLayout.visibility = View.VISIBLE
val statuses: Array<String> = resources.getStringArray(R.array.status) val statuses: Array<String> = resources.getStringArray(R.array.status)
val statusStrings = if (media?.manga==null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(R.array.status_manga) val statusStrings =
val userStatus = if(media!!.userStatus != null) statusStrings[statuses.indexOf(media!!.userStatus)] else statusStrings[0] if (media?.manga == null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(
R.array.status_manga
)
val userStatus =
if (media!!.userStatus != null) statusStrings[statuses.indexOf(media!!.userStatus)] else statusStrings[0]
binding.mediaListStatus.setText(userStatus) binding.mediaListStatus.setText(userStatus)
binding.mediaListStatus.setAdapter( binding.mediaListStatus.setAdapter(
@@ -160,7 +168,9 @@ 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 ?: 5000)) binding.mediaListProgress.setText((init + 1).toString()) if (init < (total
?: 5000)
) binding.mediaListProgress.setText((init + 1).toString())
if (init + 1 == (total ?: 5000)) { if (init + 1 == (total ?: 5000)) {
binding.mediaListStatus.setText(statusStrings[2], false) binding.mediaListStatus.setText(statusStrings[2], false)
onComplete() onComplete()
@@ -186,7 +196,7 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
} }
media?.inCustomListsOf?.forEach { media?.inCustomListsOf?.forEach {
SwitchMaterial(requireContext()).apply { MaterialSwitch(requireContext()).apply {
isChecked = it.value isChecked = it.value
text = it.key text = it.key
setOnCheckedChangeListener { _, isChecked -> setOnCheckedChangeListener { _, isChecked ->
@@ -201,11 +211,15 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
scope.launch { scope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
if (media != null) { if (media != null) {
val progress = _binding?.mediaListProgress?.text.toString().toIntOrNull() val progress =
_binding?.mediaListProgress?.text.toString().toIntOrNull()
val score = val score =
(_binding?.mediaListScore?.text.toString().toDoubleOrNull()?.times(10))?.toInt() (_binding?.mediaListScore?.text.toString().toDoubleOrNull()
val status = statuses[statusStrings.indexOf(_binding?.mediaListStatus?.text.toString())] ?.times(10))?.toInt()
val rewatch = _binding?.mediaListRewatch?.text?.toString()?.toIntOrNull() val status =
statuses[statusStrings.indexOf(_binding?.mediaListStatus?.text.toString())]
val rewatch =
_binding?.mediaListRewatch?.text?.toString()?.toIntOrNull()
val notes = _binding?.mediaListNotes?.text?.toString() val notes = _binding?.mediaListNotes?.text?.toString()
val startD = start.date val startD = start.date
val endD = end.date val endD = end.date

View File

@@ -12,8 +12,8 @@ import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding
import ani.dantotsu.connections.mal.MAL import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding
import ani.dantotsu.others.getSerialized import ani.dantotsu.others.getSerialized
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -44,7 +44,11 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
private var _binding: BottomSheetMediaListSmallBinding? = null private var _binding: BottomSheetMediaListSmallBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = BottomSheetMediaListSmallBinding.inflate(inflater, container, false) _binding = BottomSheetMediaListSmallBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }
@@ -58,8 +62,12 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
binding.mediaListProgressBar.visibility = View.GONE binding.mediaListProgressBar.visibility = View.GONE
binding.mediaListLayout.visibility = View.VISIBLE binding.mediaListLayout.visibility = View.VISIBLE
val statuses: Array<String> = resources.getStringArray(R.array.status) val statuses: Array<String> = resources.getStringArray(R.array.status)
val statusStrings = if (media.manga==null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(R.array.status_manga) val statusStrings =
val userStatus = if(media.userStatus != null) statusStrings[statuses.indexOf(media.userStatus)] else statusStrings[0] if (media.manga == null) resources.getStringArray(R.array.status_anime) else resources.getStringArray(
R.array.status_manga
)
val userStatus =
if (media.userStatus != null) statusStrings[statuses.indexOf(media.userStatus)] else statusStrings[0]
binding.mediaListStatus.setText(userStatus) binding.mediaListStatus.setText(userStatus)
binding.mediaListStatus.setAdapter( binding.mediaListStatus.setAdapter(
@@ -128,10 +136,26 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val progress = _binding?.mediaListProgress?.text.toString().toIntOrNull() val progress = _binding?.mediaListProgress?.text.toString().toIntOrNull()
val score = (_binding?.mediaListScore?.text.toString().toDoubleOrNull()?.times(10))?.toInt() val score = (_binding?.mediaListScore?.text.toString().toDoubleOrNull()
val status = statuses[statusStrings.indexOf(_binding?.mediaListStatus?.text.toString())] ?.times(10))?.toInt()
Anilist.mutation.editList(media.id, progress, score, null, null, status, media.isListPrivate) val status =
MAL.query.editList(media.idMAL, media.anime != null, progress, score, status) statuses[statusStrings.indexOf(_binding?.mediaListStatus?.text.toString())]
Anilist.mutation.editList(
media.id,
progress,
score,
null,
null,
status,
media.isListPrivate
)
MAL.query.editList(
media.idMAL,
media.anime != null,
progress,
score,
status
)
} }
} }
Refresh.all() Refresh.all()

View File

@@ -5,7 +5,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import java.text.DateFormat import java.text.DateFormat
import java.util.* import java.util.Date
class OtherDetailsViewModel : ViewModel() { class OtherDetailsViewModel : ViewModel() {
private val character: MutableLiveData<Character> = MutableLiveData(null) private val character: MutableLiveData<Character> = MutableLiveData(null)
@@ -19,11 +19,13 @@ class OtherDetailsViewModel : ViewModel() {
suspend fun loadStudio(m: Studio) { suspend fun loadStudio(m: Studio) {
if (studio.value == null) studio.postValue(Anilist.query.getStudioDetails(m)) if (studio.value == null) studio.postValue(Anilist.query.getStudioDetails(m))
} }
private val author: MutableLiveData<Author> = MutableLiveData(null) private val author: MutableLiveData<Author> = MutableLiveData(null)
fun getAuthor(): LiveData<Author> = author fun getAuthor(): LiveData<Author> = author
suspend fun loadAuthor(m: Author) { suspend fun loadAuthor(m: Author) {
if (author.value == null) author.postValue(Anilist.query.getAuthorDetails(m)) if (author.value == null) author.postValue(Anilist.query.getAuthorDetails(m))
} }
private val calendar: MutableLiveData<Map<String, MutableList<Media>>> = MutableLiveData(null) private val calendar: MutableLiveData<Map<String, MutableList<Media>>> = MutableLiveData(null)
fun getCalendar(): LiveData<Map<String, MutableList<Media>>> = calendar fun getCalendar(): LiveData<Map<String, MutableList<Media>>> = calendar
suspend fun loadCalendar() { suspend fun loadCalendar() {

View File

@@ -22,7 +22,8 @@ class ProgressAdapter(private val horizontal: Boolean = true, searched: Boolean)
var bar: ProgressBar? = null var bar: ProgressBar? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProgressViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProgressViewHolder {
val binding = ItemProgressbarBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding =
ItemProgressbarBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ProgressViewHolder(binding) return ProgressViewHolder(binding)
} }
@@ -33,7 +34,12 @@ class ProgressAdapter(private val horizontal: Boolean = true, searched: Boolean)
val doubleClickDetector = GestureDetector(progressBar.context, object : GesturesListener() { val doubleClickDetector = GestureDetector(progressBar.context, object : GesturesListener() {
override fun onDoubleClick(event: MotionEvent) { override fun onDoubleClick(event: MotionEvent) {
snackString(currContext()?.getString(R.string.cant_wait)) snackString(currContext()?.getString(R.string.cant_wait))
ObjectAnimator.ofFloat(progressBar, "translationX", progressBar.translationX, progressBar.translationX + 100f) ObjectAnimator.ofFloat(
progressBar,
"translationX",
progressBar.translationX,
progressBar.translationX + 100f
)
.setDuration(300).start() .setDuration(300).start()
} }
@@ -51,7 +57,8 @@ class ProgressAdapter(private val horizontal: Boolean = true, searched: Boolean)
} }
override fun getItemCount(): Int = 1 override fun getItemCount(): Int = 1
inner class ProgressViewHolder(val binding: ItemProgressbarBinding) : RecyclerView.ViewHolder(binding.root) { inner class ProgressViewHolder(val binding: ItemProgressbarBinding) :
RecyclerView.ViewHolder(binding.root) {
init { init {
itemView.updateLayoutParams { if (horizontal) width = -1 else height = -1 } itemView.updateLayoutParams { if (horizontal) width = -1 else height = -1 }
} }

View File

@@ -16,6 +16,8 @@ import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistSearch import ani.dantotsu.connections.anilist.AnilistSearch
import ani.dantotsu.connections.anilist.SearchResults import ani.dantotsu.connections.anilist.SearchResults
import ani.dantotsu.databinding.ActivitySearchBinding import ani.dantotsu.databinding.ActivitySearchBinding
import ani.dantotsu.others.LangSet
import ani.dantotsu.themes.ThemeManager
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.* import java.util.*
@@ -37,6 +39,8 @@ class SearchActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivitySearchBinding.inflate(layoutInflater) binding = ActivitySearchBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
initActivity(this) initActivity(this)
@@ -74,7 +78,7 @@ class SearchActivity : AppCompatActivity() {
mediaAdaptor = MediaAdaptor(style, model.searchResults.results, this, matchParent = true) mediaAdaptor = MediaAdaptor(style, model.searchResults.results, this, matchParent = true)
val headerAdaptor = SearchAdapter(this) val headerAdaptor = SearchAdapter(this)
val gridSize = (screenWidth / 124f).toInt() val gridSize = (screenWidth / 120f).toInt()
val gridLayoutManager = GridLayoutManager(this, gridSize) val gridLayoutManager = GridLayoutManager(this, gridSize)
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int { override fun getSpanSize(position: Int): Int {
@@ -155,7 +159,7 @@ class SearchActivity : AppCompatActivity() {
fun search() { fun search() {
val size = model.searchResults.results.size val size = model.searchResults.results.size
model.searchResults.results.clear() model.searchResults.results.clear()
runOnUiThread { binding.searchRecyclerView.post {
mediaAdaptor.notifyItemRangeRemoved(0, size) mediaAdaptor.notifyItemRangeRemoved(0, size)
} }

View File

@@ -1,6 +1,8 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.graphics.drawable.Drawable
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.view.LayoutInflater import android.view.LayoutInflater
@@ -10,24 +12,30 @@ import android.view.ViewGroup
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.HORIZONTAL import androidx.recyclerview.widget.RecyclerView.HORIZONTAL
import ani.dantotsu.App.Companion.context
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemChipBinding import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.databinding.ItemSearchHeaderBinding import ani.dantotsu.databinding.ItemSearchHeaderBinding
import ani.dantotsu.saveData import ani.dantotsu.saveData
import com.google.android.material.checkbox.MaterialCheckBox.* import com.google.android.material.checkbox.MaterialCheckBox.*
class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter<SearchAdapter.SearchHeaderViewHolder>() { class SearchAdapter(private val activity: SearchActivity) :
RecyclerView.Adapter<SearchAdapter.SearchHeaderViewHolder>() {
private val itemViewType = 6969 private val itemViewType = 6969
var search: Runnable? = null var search: Runnable? = null
var requestFocus: Runnable? = null var requestFocus: Runnable? = null
private var textWatcher: TextWatcher? = null private var textWatcher: TextWatcher? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder {
val binding = ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding =
ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SearchHeaderViewHolder(binding) return SearchHeaderViewHolder(binding)
} }
@@ -36,13 +44,15 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
val binding = holder.binding val binding = holder.binding
val imm: InputMethodManager = activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager val imm: InputMethodManager =
activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
when (activity.style) { when (activity.style) {
0 -> { 0 -> {
binding.searchResultGrid.alpha = 1f binding.searchResultGrid.alpha = 1f
binding.searchResultList.alpha = 0.33f binding.searchResultList.alpha = 0.33f
} }
1 -> { 1 -> {
binding.searchResultList.alpha = 1f binding.searchResultList.alpha = 1f
binding.searchResultGrid.alpha = 0.33f binding.searchResultGrid.alpha = 0.33f
@@ -50,6 +60,14 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
} }
binding.searchBar.hint = activity.result.type binding.searchBar.hint = activity.result.type
if (currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("incognito", false ) == true){
val startIconDrawableRes = R.drawable.ic_incognito_24
val startIconDrawable: Drawable? =
context?.let { AppCompatResources.getDrawable(it, startIconDrawableRes) }
binding.searchBar.startIconDrawable = startIconDrawable
}
var adult = activity.result.isAdult var adult = activity.result.isAdult
var listOnly = activity.result.onList var listOnly = activity.result.onList
@@ -62,7 +80,8 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
binding.searchChipRecycler.adapter = SearchChipAdapter(activity).also { binding.searchChipRecycler.adapter = SearchChipAdapter(activity).also {
activity.updateChips = { it.update() } activity.updateChips = { it.update() }
} }
binding.searchChipRecycler.layoutManager = LinearLayoutManager(binding.root.context, HORIZONTAL, false) binding.searchChipRecycler.layoutManager =
LinearLayoutManager(binding.root.context, HORIZONTAL, false)
binding.searchFilter.setOnClickListener { binding.searchFilter.setOnClickListener {
SearchFilterBottomDialog.newInstance().show(activity.supportFragmentManager, "dialog") SearchFilterBottomDialog.newInstance().show(activity.supportFragmentManager, "dialog")
@@ -70,7 +89,8 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
fun searchTitle() { fun searchTitle() {
activity.result.apply { activity.result.apply {
search = if (binding.searchBarText.text.toString() != "") binding.searchBarText.text.toString() else null search =
if (binding.searchBarText.text.toString() != "") binding.searchBarText.text.toString() else null
onList = listOnly onList = listOnly
isAdult = adult isAdult = adult
} }
@@ -96,6 +116,7 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0) imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0)
true true
} }
else -> false else -> false
} }
} }
@@ -158,20 +179,24 @@ class SearchAdapter(private val activity: SearchActivity) : RecyclerView.Adapter
override fun getItemCount(): Int = 1 override fun getItemCount(): Int = 1
inner class SearchHeaderViewHolder(val binding: ItemSearchHeaderBinding) : RecyclerView.ViewHolder(binding.root) inner class SearchHeaderViewHolder(val binding: ItemSearchHeaderBinding) :
RecyclerView.ViewHolder(binding.root)
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int): Int {
return itemViewType return itemViewType
} }
class SearchChipAdapter(val activity: SearchActivity) : RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() { class SearchChipAdapter(val activity: SearchActivity) :
RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() {
private var chips = activity.result.toChipList() private var chips = activity.result.toChipList()
inner class SearchChipViewHolder(val binding: ItemChipBinding) : RecyclerView.ViewHolder(binding.root) inner class SearchChipViewHolder(val binding: ItemChipBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchChipViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchChipViewHolder {
val binding = ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding =
ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SearchChipViewHolder(binding) return SearchChipViewHolder(binding)
} }

View File

@@ -18,13 +18,17 @@ 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
class SearchFilterBottomDialog() : BottomSheetDialogFragment() { class SearchFilterBottomDialog : BottomSheetDialogFragment() {
private var _binding: BottomSheetSearchFilterBinding? = null private var _binding: BottomSheetSearchFilterBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private lateinit var activity: SearchActivity private lateinit var activity: SearchActivity
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = BottomSheetSearchFilterBinding.inflate(inflater, container, false) _binding = BottomSheetSearchFilterBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }
@@ -99,7 +103,7 @@ class SearchFilterBottomDialog() : BottomSheetDialogFragment() {
ArrayAdapter( ArrayAdapter(
binding.root.context, binding.root.context,
R.layout.item_dropdown, R.layout.item_dropdown,
(1970 until 2024).map { it.toString() }.reversed().toTypedArray() (1970 until 2025).map { it.toString() }.reversed().toTypedArray()
) )
) )
} }
@@ -129,7 +133,8 @@ class SearchFilterBottomDialog() : BottomSheetDialogFragment() {
} }
binding.searchGenresGrid.isChecked = false binding.searchGenresGrid.isChecked = false
binding.searchFilterTags.adapter = FilterChipAdapter(Anilist.tags?.get(activity.result.isAdult) ?: listOf()) { chip -> binding.searchFilterTags.adapter =
FilterChipAdapter(Anilist.tags?.get(activity.result.isAdult) ?: listOf()) { chip ->
val tag = chip.text.toString() val tag = chip.text.toString()
chip.isChecked = selectedTags.contains(tag) chip.isChecked = selectedTags.contains(tag)
chip.isCloseIconVisible = exTags.contains(tag) chip.isCloseIconVisible = exTags.contains(tag)
@@ -158,10 +163,12 @@ class SearchFilterBottomDialog() : BottomSheetDialogFragment() {
class FilterChipAdapter(val list: List<String>, private val perform: ((Chip) -> Unit)) : class FilterChipAdapter(val list: List<String>, private val perform: ((Chip) -> Unit)) :
RecyclerView.Adapter<FilterChipAdapter.SearchChipViewHolder>() { RecyclerView.Adapter<FilterChipAdapter.SearchChipViewHolder>() {
inner class SearchChipViewHolder(val binding: ItemChipBinding) : RecyclerView.ViewHolder(binding.root) inner class SearchChipViewHolder(val binding: ItemChipBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchChipViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchChipViewHolder {
val binding = ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding =
ItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SearchChipViewHolder(binding) return SearchChipViewHolder(binding)
} }

View File

@@ -9,8 +9,10 @@ data class Selected(
var chip: Int = 0, var chip: Int = 0,
//var source: String = "", //var source: String = "",
var sourceIndex: Int = 0, var sourceIndex: Int = 0,
var langIndex: Int = 0,
var preferDub: Boolean = false, var preferDub: Boolean = false,
var server: String? = null, var server: String? = null,
var video: Int = 0, var video: Int = 0,
var latest: Float = 0f, var latest: Float = 0f,
var scanlators: List<String>? = null,
) : Serializable ) : Serializable

View File

@@ -17,7 +17,8 @@ abstract class SourceAdapter(
private val scope: CoroutineScope private val scope: CoroutineScope
) : RecyclerView.Adapter<SourceAdapter.SourceViewHolder>() { ) : RecyclerView.Adapter<SourceAdapter.SourceViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SourceViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SourceViewHolder {
val binding = ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding =
ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SourceViewHolder(binding) return SourceViewHolder(binding)
} }
@@ -34,7 +35,8 @@ abstract class SourceAdapter(
abstract suspend fun onItemClick(source: ShowResponse) abstract suspend fun onItemClick(source: ShowResponse)
inner class SourceViewHolder(val binding: ItemCharacterBinding) : RecyclerView.ViewHolder(binding.root) { inner class SourceViewHolder(val binding: ItemCharacterBinding) :
RecyclerView.ViewHolder(binding.root) {
init { init {
itemView.setOnClickListener { itemView.setOnClickListener {
dialogFragment.dismiss() dialogFragment.dismiss()

View File

@@ -13,8 +13,8 @@ import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import ani.dantotsu.BottomSheetDialogFragment import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.media.anime.AnimeSourceAdapter
import ani.dantotsu.databinding.BottomSheetSourceSearchBinding import ani.dantotsu.databinding.BottomSheetSourceSearchBinding
import ani.dantotsu.media.anime.AnimeSourceAdapter
import ani.dantotsu.media.manga.MangaSourceAdapter import ani.dantotsu.media.manga.MangaSourceAdapter
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.AnimeSources
@@ -38,7 +38,11 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
var id: Int? = null var id: Int? = null
var media: Media? = null var media: Media? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = BottomSheetSourceSearchBinding.inflate(inflater, container, false) _binding = BottomSheetSourceSearchBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }
@@ -47,7 +51,8 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight } binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
val scope = requireActivity().lifecycleScope val scope = requireActivity().lifecycleScope
val imm = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager val imm =
requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
model.getMedia().observe(viewLifecycleOwner) { model.getMedia().observe(viewLifecycleOwner) {
media = it media = it
if (media != null) { if (media != null) {
@@ -65,6 +70,7 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
anime = false anime = false
(if (media!!.isAdult) HMangaSources else MangaSources)[i!!] (if (media!!.isAdult) HMangaSources else MangaSources)[i!!]
} }
fun search() { fun search() {
binding.searchBarText.clearFocus() binding.searchBarText.clearFocus()
imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0) imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0)
@@ -86,6 +92,7 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
search() search()
true true
} }
else -> false else -> false
} }
} }
@@ -101,7 +108,11 @@ class SourceSearchDialogFragment : BottomSheetDialogFragment() {
else MangaSourceAdapter(j, model, i!!, media!!.id, this, scope) else MangaSourceAdapter(j, model, i!!, media!!.id, this, scope)
binding.searchRecyclerView.layoutManager = GridLayoutManager( binding.searchRecyclerView.layoutManager = GridLayoutManager(
requireActivity(), requireActivity(),
clamp(requireActivity().resources.displayMetrics.widthPixels / 124f.px, 1, 4) clamp(
requireActivity().resources.displayMetrics.widthPixels / 124f.px,
1,
4
)
) )
} }
} }

View File

@@ -12,9 +12,17 @@ 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 ani.dantotsu.* import ani.dantotsu.EmptyAdapter
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.databinding.ActivityStudioBinding import ani.dantotsu.databinding.ActivityStudioBinding
import ani.dantotsu.initActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.others.LangSet
import ani.dantotsu.others.getSerialized import ani.dantotsu.others.getSerialized
import ani.dantotsu.px
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -28,6 +36,8 @@ class StudioActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityStudioBinding.inflate(layoutInflater) binding = ActivityStudioBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)

View File

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

View File

@@ -5,8 +5,10 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.databinding.ItemTitleBinding import ani.dantotsu.databinding.ItemTitleBinding
class TitleAdapter(private val text: String) : RecyclerView.Adapter<TitleAdapter.TitleViewHolder>() { class TitleAdapter(private val text: String) :
inner class TitleViewHolder(val binding: ItemTitleBinding) : RecyclerView.ViewHolder(binding.root) RecyclerView.Adapter<TitleAdapter.TitleViewHolder>() {
inner class TitleViewHolder(val binding: ItemTitleBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TitleViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TitleViewHolder {
val binding = ItemTitleBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding = ItemTitleBinding.inflate(LayoutInflater.from(parent.context), parent, false)

View File

@@ -0,0 +1,76 @@
package ani.dantotsu.media.anime
import java.util.regex.Matcher
import java.util.regex.Pattern
class AnimeNameAdapter {
companion object {
const val episodeRegex =
"(episode|ep|e)[\\s:.\\-]*([\\d]+\\.?[\\d]*)[\\s:.\\-]*\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*"
const val failedEpisodeNumberRegex =
"(?<!part\\s)\\b(\\d+)\\b"
const val seasonRegex = "\\s+(season|s)[\\s:.\\-]*(\\d+)[\\s:.\\-]*"
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,29 +1,41 @@
package ani.dantotsu.media.anime package ani.dantotsu.media.anime
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
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 android.view.ViewGroup import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.ImageView import android.widget.ImageButton
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.startActivity
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.databinding.DialogLayoutBinding
import ani.dantotsu.databinding.ItemAnimeWatchBinding import ani.dantotsu.databinding.ItemAnimeWatchBinding
import ani.dantotsu.databinding.ItemChipBinding import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.SourceSearchDialogFragment import ani.dantotsu.media.SourceSearchDialogFragment
import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.others.webview.CookieCatcher
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.DynamicAnimeParser
import ani.dantotsu.parsers.WatchSources import ani.dantotsu.parsers.WatchSources
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
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.util.system.WebViewUtil
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class AnimeWatchAdapter( class AnimeWatchAdapter(
private val media: Media, private val media: Media,
private val fragment: AnimeWatchFragment, private val fragment: AnimeWatchFragment,
@@ -38,6 +50,9 @@ class AnimeWatchAdapter(
return ViewHolder(bind) return ViewHolder(bind)
} }
private var nestedDialog: AlertDialog? = null
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding = holder.binding val binding = holder.binding
@@ -53,22 +68,44 @@ class AnimeWatchAdapter(
} }
binding.animeSourceDubbed.isChecked = media.selected!!.preferDub binding.animeSourceDubbed.isChecked = media.selected!!.preferDub
binding.animeSourceDubbedText.text = if (media.selected!!.preferDub) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(R.string.subbed) binding.animeSourceDubbedText.text =
if (media.selected!!.preferDub) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(
R.string.subbed
)
//PreferDub //PreferDub
var changing = false var changing = false
binding.animeSourceDubbed.setOnCheckedChangeListener { _, isChecked -> binding.animeSourceDubbed.setOnCheckedChangeListener { _, isChecked ->
binding.animeSourceDubbedText.text = if (isChecked) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(R.string.subbed) binding.animeSourceDubbedText.text =
if (isChecked) currActivity()!!.getString(R.string.dubbed) else currActivity()!!.getString(
R.string.subbed
)
if (!changing) fragment.onDubClicked(isChecked) if (!changing) fragment.onDubClicked(isChecked)
} }
//Wrong Title //Wrong Title
binding.animeSourceSearch.setOnClickListener { binding.animeSourceSearch.setOnClickListener {
SourceSearchDialogFragment().show(fragment.requireActivity().supportFragmentManager, null) SourceSearchDialogFragment().show(
fragment.requireActivity().supportFragmentManager,
null
)
} }
val offline = if (!isOnline(binding.root.context) || currContext()?.getSharedPreferences(
"Dantotsu",
Context.MODE_PRIVATE
)
?.getBoolean("offlineMode", false) == true
) View.GONE else View.VISIBLE
binding.animeSourceNameContainer.visibility = offline
binding.animeSourceSettings.visibility = offline
binding.animeSourceSearch.visibility = offline
binding.animeSourceTitle.visibility = offline
//Source Selection //Source Selection
val source = media.selected!!.sourceIndex.let { if (it >= watchSources.names.size) 0 else it } var source =
media.selected!!.sourceIndex.let { if (it >= watchSources.names.size) 0 else it }
setLanguageList(media.selected!!.langIndex, source)
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) { if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
binding.animeSource.setText(watchSources.names[source]) binding.animeSource.setText(watchSources.names[source])
watchSources[source].apply { watchSources[source].apply {
@@ -80,7 +117,13 @@ class AnimeWatchAdapter(
} }
} }
binding.animeSource.setAdapter(ArrayAdapter(fragment.requireContext(), R.layout.item_dropdown, watchSources.names)) binding.animeSource.setAdapter(
ArrayAdapter(
fragment.requireContext(),
R.layout.item_dropdown,
watchSources.names
)
)
binding.animeSourceTitle.isSelected = true binding.animeSourceTitle.isSelected = true
binding.animeSource.setOnItemClickListener { _, _, i, _ -> binding.animeSource.setOnItemClickListener { _, _, i, _ ->
fragment.onSourceChange(i).apply { fragment.onSourceChange(i).apply {
@@ -89,13 +132,47 @@ class AnimeWatchAdapter(
changing = true changing = true
binding.animeSourceDubbed.isChecked = selectDub binding.animeSourceDubbed.isChecked = selectDub
changing = false changing = false
binding.animeSourceDubbedCont.visibility = if (isDubAvailableSeparately) View.VISIBLE else View.GONE binding.animeSourceDubbedCont.visibility =
if (isDubAvailableSeparately) View.VISIBLE else View.GONE
source = i
setLanguageList(0, i)
} }
subscribeButton(false) subscribeButton(false)
fragment.loadEpisodes(i) fragment.loadEpisodes(i, false)
} }
//Subscription binding.animeSourceLanguage.setOnItemClickListener { _, _, i, _ ->
// Check if 'extension' and 'selected' properties exist and are accessible
(watchSources[source] as? DynamicAnimeParser)?.let { ext ->
ext.sourceLanguage = i
fragment.onLangChange(i)
fragment.onSourceChange(media.selected!!.sourceIndex).apply {
binding.animeSourceTitle.text = showUserText
showUserTextListener =
{ MainScope().launch { binding.animeSourceTitle.text = it } }
changing = true
binding.animeSourceDubbed.isChecked = selectDub
changing = false
binding.animeSourceDubbedCont.visibility =
if (isDubAvailableSeparately) View.VISIBLE else View.GONE
setLanguageList(i, source)
}
subscribeButton(false)
fragment.loadEpisodes(media.selected!!.sourceIndex, true)
} ?: run {
}
}
//settings
binding.animeSourceSettings.setOnClickListener {
(watchSources[source] as? DynamicAnimeParser)?.let { ext ->
fragment.openSettings(ext.extension)
}
}
//Icons
//subscribe
subscribe = MediaDetailsActivity.PopImageButton( subscribe = MediaDetailsActivity.PopImageButton(
fragment.lifecycleScope, fragment.lifecycleScope,
binding.animeSourceSubscribe, binding.animeSourceSubscribe,
@@ -114,43 +191,99 @@ class AnimeWatchAdapter(
openSettings(fragment.requireContext(), getChannelId(true, media.id)) openSettings(fragment.requireContext(), getChannelId(true, media.id))
} }
//Icons //Nested Button
binding.animeNestedButton.setOnClickListener {
val dialogView =
LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null)
val dialogBinding = DialogLayoutBinding.bind(dialogView)
var refresh = false
var run = false
var reversed = media.selected!!.recyclerReversed var reversed = media.selected!!.recyclerReversed
var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.animeDefaultView var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.animeDefaultView
binding.animeSourceTop.rotation = if (reversed) -90f else 90f dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
binding.animeSourceTop.setOnClickListener { dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
dialogBinding.animeSourceTop.setOnClickListener {
reversed = !reversed reversed = !reversed
binding.animeSourceTop.rotation = if (reversed) -90f else 90f dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
fragment.onIconPressed(style, reversed) dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
run = true
} }
//Grids
var selected = when (style) { var selected = when (style) {
0 -> binding.animeSourceList 0 -> dialogBinding.animeSourceList
1 -> binding.animeSourceGrid 1 -> dialogBinding.animeSourceGrid
2 -> binding.animeSourceCompact 2 -> dialogBinding.animeSourceCompact
else -> binding.animeSourceList else -> dialogBinding.animeSourceList
}
when (style) {
0 -> dialogBinding.layoutText.text = "List"
1 -> dialogBinding.layoutText.text = "Grid"
2 -> dialogBinding.layoutText.text = "Compact"
else -> dialogBinding.animeSourceList
} }
selected.alpha = 1f selected.alpha = 1f
fun selected(it: ImageView) { fun selected(it: ImageButton) {
selected.alpha = 0.33f selected.alpha = 0.33f
selected = it selected = it
selected.alpha = 1f selected.alpha = 1f
} }
binding.animeSourceList.setOnClickListener { dialogBinding.animeSourceList.setOnClickListener {
selected(it as ImageView) selected(it as ImageButton)
style = 0 style = 0
fragment.onIconPressed(style, reversed) dialogBinding.layoutText.text = "List"
run = true
} }
binding.animeSourceGrid.setOnClickListener { dialogBinding.animeSourceGrid.setOnClickListener {
selected(it as ImageView) selected(it as ImageButton)
style = 1 style = 1
fragment.onIconPressed(style, reversed) dialogBinding.layoutText.text = "Grid"
run = true
} }
binding.animeSourceCompact.setOnClickListener { dialogBinding.animeSourceCompact.setOnClickListener {
selected(it as ImageView) selected(it as ImageButton)
style = 2 style = 2
fragment.onIconPressed(style, reversed) dialogBinding.layoutText.text = "Compact"
run = true
}
dialogBinding.animeWebviewContainer.setOnClickListener {
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
toast("WebView not installed")
}
//start CookieCatcher activity
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
val sourceAHH = watchSources[source] as? DynamicAnimeParser
val sourceHttp =
sourceAHH?.extension?.sources?.firstOrNull() as? AnimeHttpSource
val url = sourceHttp?.baseUrl
url?.let {
refresh = true
val intent = Intent(fragment.requireContext(), CookieCatcher::class.java)
.putExtra("url", url)
startActivity(fragment.requireContext(), intent, null)
}
}
} }
//hidden
dialogBinding.animeScanlatorContainer.visibility = View.GONE
dialogBinding.animeDownloadContainer.visibility = View.GONE
nestedDialog = AlertDialog.Builder(fragment.requireContext(), R.style.MyPopup)
.setTitle("Options")
.setView(dialogView)
.setPositiveButton("OK") { _, _ ->
if (run) fragment.onIconPressed(style, reversed)
if (refresh) fragment.loadEpisodes(source, true)
}
.setNegativeButton("Cancel") { _, _ ->
if (refresh) fragment.loadEpisodes(source, true)
}
.setOnCancelListener {
if (refresh) fragment.loadEpisodes(source, true)
}
.create()
nestedDialog?.show()
}
//Episode Handling //Episode Handling
handleEpisodes() handleEpisodes()
} }
@@ -169,13 +302,26 @@ class AnimeWatchAdapter(
for (position in arr.indices) { for (position in arr.indices) {
val last = if (position + 1 == arr.size) names.size else (limit * (position + 1)) val last = if (position + 1 == arr.size) names.size else (limit * (position + 1))
val chip = val chip =
ItemChipBinding.inflate(LayoutInflater.from(fragment.context), binding.animeSourceChipGroup, false).root ItemChipBinding.inflate(
LayoutInflater.from(fragment.context),
binding.animeSourceChipGroup,
false
).root
chip.isCheckable = true chip.isCheckable = true
fun selected() { fun selected() {
chip.isChecked = true chip.isChecked = true
binding.animeWatchChipScroll.smoothScrollTo((chip.left - screenWidth / 2) + (chip.width / 2), 0) binding.animeWatchChipScroll.smoothScrollTo(
(chip.left - screenWidth / 2) + (chip.width / 2),
0
)
} }
chip.text = "${names[limit * (position)]} - ${names[last - 1]}" chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
chip.setTextColor(
ContextCompat.getColorStateList(
fragment.requireContext(),
R.color.chip_text_color
)
)
chip.setOnClickListener { chip.setOnClickListener {
selected() selected()
@@ -188,7 +334,14 @@ class AnimeWatchAdapter(
} }
} }
if (select != null) if (select != null)
binding.animeWatchChipScroll.apply { post { scrollTo((select.left - screenWidth / 2) + (select.width / 2), 0) } } binding.animeWatchChipScroll.apply {
post {
scrollTo(
(select.left - screenWidth / 2) + (select.width / 2),
0
)
}
}
} }
} }
@@ -230,10 +383,15 @@ class AnimeWatchAdapter(
} }
} }
val ep = media.anime.episodes!![continueEp]!! val ep = media.anime.episodes!![continueEp]!!
binding.itemEpisodeImage.loadImage(ep.thumb ?: FileUrl[media.banner ?: media.cover], 0)
val cleanedTitle = ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) }
binding.itemEpisodeImage.loadImage(
ep.thumb ?: FileUrl[media.banner ?: media.cover], 0
)
if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE
binding.animeSourceContinueText.text = binding.animeSourceContinueText.text =
currActivity()!!.getString(R.string.continue_episode) + "${ep.number}${if (ep.filler) " - Filler" else ""}${if (ep.title != null) "\n${ep.title}" else ""}" currActivity()!!.getString(R.string.continue_episode) + "${ep.number}${if (ep.filler) " - Filler" else ""}${"\n$cleanedTitle"}"
binding.animeSourceContinue.setOnClickListener { binding.animeSourceContinue.setOnClickListener {
fragment.onEpisodeClick(continueEp) fragment.onEpisodeClick(continueEp)
} }
@@ -246,6 +404,7 @@ class AnimeWatchAdapter(
} else { } else {
binding.animeSourceContinue.visibility = View.GONE binding.animeSourceContinue.visibility = View.GONE
} }
binding.animeSourceProgressBar.visibility = View.GONE binding.animeSourceProgressBar.visibility = View.GONE
if (media.anime.episodes!!.isNotEmpty()) if (media.anime.episodes!!.isNotEmpty())
binding.animeSourceNotFound.visibility = View.GONE binding.animeSourceNotFound.visibility = View.GONE
@@ -260,9 +419,40 @@ class AnimeWatchAdapter(
} }
} }
private fun setLanguageList(lang: Int, source: Int) {
val binding = _binding
if (watchSources is AnimeSources) {
val parser = watchSources[source] as? DynamicAnimeParser
if (parser != null) {
(watchSources[source] as? DynamicAnimeParser)?.let { ext ->
ext.sourceLanguage = lang
}
try {
binding?.animeSourceLanguage?.setText(parser.extension.sources[lang].lang)
} catch (e: IndexOutOfBoundsException) {
binding?.animeSourceLanguage?.setText(
parser.extension.sources.firstOrNull()?.lang ?: "Unknown"
)
}
val adapter = ArrayAdapter(
fragment.requireContext(),
R.layout.item_dropdown,
parser.extension.sources.map { LanguageMapper.mapLanguageCodeToName(it.lang) }
)
val items = adapter.count
binding?.animeSourceLanguageContainer?.visibility =
if (items > 1) View.VISIBLE else View.GONE
binding?.animeSourceLanguage?.setAdapter(adapter)
}
}
}
override fun getItemCount(): Int = 1 override fun getItemCount(): Int = 1
inner class ViewHolder(val binding: ItemAnimeWatchBinding) : RecyclerView.ViewHolder(binding.root) { inner class ViewHolder(val binding: ItemAnimeWatchBinding) :
RecyclerView.ViewHolder(binding.root) {
init { init {
//Timer //Timer
countDown(media, binding.animeSourceContainer) countDown(media, binding.animeSourceContainer)

View File

@@ -1,36 +1,62 @@
package ani.dantotsu.media.anime package ani.dantotsu.media.anime
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils import androidx.core.math.MathUtils
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.DownloadService
import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.databinding.FragmentAnimeWatchBinding import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.anime.AnimeDownloaderService
import ani.dantotsu.download.video.ExoplayerDownloadService
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.parsers.AnimeParser import ani.dantotsu.parsers.AnimeParser
import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.HAnimeSources import ani.dantotsu.parsers.HAnimeSources
import ani.dantotsu.settings.PlayerSettings import ani.dantotsu.settings.PlayerSettings
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.settings.extensionprefs.AnimeSourcePreferencesFragment
import ani.dantotsu.subcriptions.Notifications import ani.dantotsu.subcriptions.Notifications
import ani.dantotsu.subcriptions.Notifications.Group.ANIME_GROUP import ani.dantotsu.subcriptions.Notifications.Group.ANIME_GROUP
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import ani.dantotsu.subcriptions.SubscriptionHelper import ani.dantotsu.subcriptions.SubscriptionHelper
import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.navigationrail.NavigationRailView
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.extension.anime.model.AnimeExtension
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.max import kotlin.math.max
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -50,6 +76,8 @@ class AnimeWatchFragment : Fragment() {
private lateinit var headerAdapter: AnimeWatchAdapter private lateinit var headerAdapter: AnimeWatchAdapter
private lateinit var episodeAdapter: EpisodeAdapter private lateinit var episodeAdapter: EpisodeAdapter
val downloadManager = Injekt.get<DownloadsManager>()
var screenWidth = 0f var screenWidth = 0f
private var progress = View.VISIBLE private var progress = View.VISIBLE
@@ -69,6 +97,21 @@ class AnimeWatchFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val intentFilter = IntentFilter().apply {
addAction(ACTION_DOWNLOAD_STARTED)
addAction(ACTION_DOWNLOAD_FINISHED)
addAction(ACTION_DOWNLOAD_FAILED)
addAction(ACTION_DOWNLOAD_PROGRESS)
}
ContextCompat.registerReceiver(
requireContext(),
downloadStatusReceiver,
intentFilter,
ContextCompat.RECEIVER_EXPORTED
)
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight) binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight)
screenWidth = resources.displayMetrics.widthPixels.dp screenWidth = resources.displayMetrics.widthPixels.dp
@@ -76,8 +119,10 @@ class AnimeWatchFragment : Fragment() {
maxGridSize = max(4, maxGridSize - (maxGridSize % 2)) maxGridSize = max(4, maxGridSize - (maxGridSize % 2))
playerSettings = playerSettings =
loadData("player_settings", toast = false) ?: PlayerSettings().apply { saveData("player_settings", this) } loadData("player_settings", toast = false)
uiSettings = loadData("ui_settings", toast = false) ?: UserInterfaceSettings().apply { saveData("ui_settings", this) } ?: PlayerSettings().apply { saveData("player_settings", this) }
uiSettings = loadData("ui_settings", toast = false)
?: UserInterfaceSettings().apply { saveData("ui_settings", this) }
val gridLayoutManager = GridLayoutManager(requireContext(), maxGridSize) val gridLayoutManager = GridLayoutManager(requireContext(), maxGridSize)
@@ -109,7 +154,8 @@ class AnimeWatchFragment : Fragment() {
media = it media = it
media.selected = model.loadSelected(media) media.selected = model.loadSelected(media)
subscribed = SubscriptionHelper.getSubscriptions(requireContext()).containsKey(media.id) subscribed =
SubscriptionHelper.getSubscriptions(requireContext()).containsKey(media.id)
style = media.selected!!.recyclerStyle style = media.selected!!.recyclerStyle
reverse = media.selected!!.recyclerReversed reverse = media.selected!!.recyclerReversed
@@ -120,10 +166,20 @@ class AnimeWatchFragment : Fragment() {
if (!loaded) { if (!loaded) {
model.watchSources = if (media.isAdult) HAnimeSources else AnimeSources model.watchSources = if (media.isAdult) HAnimeSources else AnimeSources
headerAdapter = AnimeWatchAdapter(it, this, model.watchSources!!) val offlineMode =
episodeAdapter = EpisodeAdapter(style ?: uiSettings.animeDefaultView, media, this) model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)
binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, episodeAdapter) headerAdapter = AnimeWatchAdapter(it, this, model.watchSources!!)
episodeAdapter =
EpisodeAdapter(
style ?: uiSettings.animeDefaultView,
media,
this,
offlineMode = offlineMode
)
binding.animeSourceRecycler.adapter =
ConcatAdapter(headerAdapter, episodeAdapter)
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
awaitAll( awaitAll(
@@ -145,15 +201,23 @@ class AnimeWatchFragment : Fragment() {
episodes.forEach { (i, episode) -> episodes.forEach { (i, episode) ->
if (media.anime?.fillerEpisodes != null) { if (media.anime?.fillerEpisodes != null) {
if (media.anime!!.fillerEpisodes!!.containsKey(i)) { if (media.anime!!.fillerEpisodes!!.containsKey(i)) {
episode.title = episode.title ?: media.anime!!.fillerEpisodes!![i]?.title episode.title =
episode.title ?: media.anime!!.fillerEpisodes!![i]?.title
episode.filler = media.anime!!.fillerEpisodes!![i]?.filler ?: false episode.filler = media.anime!!.fillerEpisodes!![i]?.filler ?: false
} }
} }
if (media.anime?.kitsuEpisodes != null) { if (media.anime?.kitsuEpisodes != null) {
if (media.anime!!.kitsuEpisodes!!.containsKey(i)) { if (media.anime!!.kitsuEpisodes!!.containsKey(i)) {
episode.desc = episode.desc ?: media.anime!!.kitsuEpisodes!![i]?.desc episode.desc =
episode.title = episode.title ?: media.anime!!.kitsuEpisodes!![i]?.title media.anime!!.kitsuEpisodes!![i]?.desc ?: episode.desc
episode.thumb = episode.thumb ?: media.anime!!.kitsuEpisodes!![i]?.thumb ?: FileUrl[media.cover] episode.title = if (AnimeNameAdapter.removeEpisodeNumberCompletely(
episode.title ?: ""
).isBlank()
) media.anime!!.kitsuEpisodes!![i]?.title
?: episode.title else episode.title
?: media.anime!!.kitsuEpisodes!![i]?.title ?: episode.title
episode.thumb = media.anime!!.kitsuEpisodes!![i]?.thumb
?: FileUrl[media.cover]
} }
} }
} }
@@ -214,17 +278,29 @@ class AnimeWatchFragment : Fragment() {
return model.watchSources?.get(i)!! return model.watchSources?.get(i)!!
} }
fun onLangChange(i: Int) {
val selected = model.loadSelected(media)
selected.langIndex = i
model.saveSelected(media.id, selected, requireActivity())
media.selected = selected
}
fun onDubClicked(checked: Boolean) { fun onDubClicked(checked: Boolean) {
val selected = model.loadSelected(media) val selected = model.loadSelected(media)
model.watchSources?.get(selected.sourceIndex)?.selectDub = checked model.watchSources?.get(selected.sourceIndex)?.selectDub = checked
selected.preferDub = checked selected.preferDub = checked
model.saveSelected(media.id, selected, requireActivity()) model.saveSelected(media.id, selected, requireActivity())
media.selected = selected media.selected = selected
lifecycleScope.launch(Dispatchers.IO) { model.forceLoadEpisode(media, selected.sourceIndex) } lifecycleScope.launch(Dispatchers.IO) {
model.forceLoadEpisode(
media,
selected.sourceIndex
)
}
} }
fun loadEpisodes(i: Int) { fun loadEpisodes(i: Int, invalidate: Boolean) {
lifecycleScope.launch(Dispatchers.IO) { model.loadEpisodes(media, i) } lifecycleScope.launch(Dispatchers.IO) { model.loadEpisodes(media, i, invalidate) }
} }
fun onIconPressed(viewType: Int, rev: Boolean) { fun onIconPressed(viewType: Int, rev: Boolean) {
@@ -263,22 +339,191 @@ class AnimeWatchFragment : Fragment() {
) )
} }
fun openSettings(pkg: AnimeExtension.Installed) {
val changeUIVisibility: (Boolean) -> Unit = { show ->
val activity = activity
if (activity is MediaDetailsActivity && isAdded) {
val visibility = if (show) View.VISIBLE else View.GONE
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).visibility = visibility
activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
try {
activity.findViewById<CustomBottomNavBar>(R.id.mediaTab).visibility = visibility
} catch (e: ClassCastException) {
activity.findViewById<NavigationRailView>(R.id.mediaTab).visibility = visibility
}
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
if (show) View.GONE else View.VISIBLE
}
}
var itemSelected = false
val allSettings = pkg.sources.filterIsInstance<ConfigurableAnimeSource>()
if (allSettings.isNotEmpty()) {
var selectedSetting = allSettings[0]
if (allSettings.size > 1) {
val names =
allSettings.map { LanguageMapper.mapLanguageCodeToName(it.lang) }.toTypedArray()
var selectedIndex = 0
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
.setTitle("Select a Source")
.setSingleChoiceItems(names, selectedIndex) { dialog, which ->
selectedIndex = which
selectedSetting = allSettings[selectedIndex]
itemSelected = true
dialog.dismiss()
// Move the fragment transaction here
requireActivity().runOnUiThread {
val fragment =
AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) {
changeUIVisibility(true)
loadEpisodes(media.selected!!.sourceIndex, true)
}
parentFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
.replace(R.id.fragmentExtensionsContainer, fragment)
.addToBackStack(null)
.commit()
}
}
.setOnDismissListener {
if (!itemSelected) {
changeUIVisibility(true)
}
}
.show()
dialog.window?.setDimAmount(0.8f)
} else {
// If there's only one setting, proceed with the fragment transaction
requireActivity().runOnUiThread {
val fragment =
AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) {
changeUIVisibility(true)
loadEpisodes(media.selected!!.sourceIndex, true)
}
parentFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
.replace(R.id.fragmentExtensionsContainer, fragment)
.addToBackStack(null)
.commit()
}
}
changeUIVisibility(false)
} else {
Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
.show()
}
}
fun onEpisodeClick(i: String) { fun onEpisodeClick(i: String) {
model.continueMedia = false model.continueMedia = false
model.saveSelected(media.id, media.selected!!, requireActivity()) model.saveSelected(media.id, media.selected!!, requireActivity())
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager) model.onEpisodeClick(media, i, requireActivity().supportFragmentManager)
} }
fun onAnimeEpisodeDownloadClick(i: String) {
model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true)
}
fun onAnimeEpisodeStopDownloadClick(i: String) {
val cancelIntent = Intent().apply {
action = AnimeDownloaderService.ACTION_CANCEL_DOWNLOAD
putExtra(
AnimeDownloaderService.EXTRA_TASK_NAME,
AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
)
}
requireContext().sendBroadcast(cancelIntent)
// Remove the download from the manager and update the UI
downloadManager.removeDownload(
DownloadedType(
media.mainName(),
i,
DownloadedType.Type.ANIME
)
)
episodeAdapter.purgeDownload(i)
}
@OptIn(UnstableApi::class)
fun onAnimeEpisodeRemoveDownloadClick(i: String) {
downloadManager.removeDownload(
DownloadedType(
media.mainName(),
i,
DownloadedType.Type.ANIME
)
)
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i)
val id = requireContext().getSharedPreferences(
ContextCompat.getString(requireContext(), R.string.anime_downloads),
Context.MODE_PRIVATE
).getString(
taskName,
""
) ?: ""
requireContext().getSharedPreferences(
ContextCompat.getString(requireContext(), R.string.anime_downloads),
Context.MODE_PRIVATE
).edit().remove(taskName).apply()
DownloadService.sendRemoveDownload(
requireContext(),
ExoplayerDownloadService::class.java,
id,
true
)
episodeAdapter.deleteDownload(i)
}
private val downloadStatusReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (!this@AnimeWatchFragment::episodeAdapter.isInitialized) return
when (intent.action) {
ACTION_DOWNLOAD_STARTED -> {
val chapterNumber = intent.getStringExtra(EXTRA_EPISODE_NUMBER)
chapterNumber?.let { episodeAdapter.startDownload(it) }
}
ACTION_DOWNLOAD_FINISHED -> {
val chapterNumber = intent.getStringExtra(EXTRA_EPISODE_NUMBER)
chapterNumber?.let { episodeAdapter.stopDownload(it) }
}
ACTION_DOWNLOAD_FAILED -> {
val chapterNumber = intent.getStringExtra(EXTRA_EPISODE_NUMBER)
chapterNumber?.let {
episodeAdapter.purgeDownload(it)
}
}
ACTION_DOWNLOAD_PROGRESS -> {
val chapterNumber = intent.getStringExtra(EXTRA_EPISODE_NUMBER)
val progress = intent.getIntExtra("progress", 0)
chapterNumber?.let {
episodeAdapter.updateDownloadProgress(it, progress)
}
}
}
}
}
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
private fun reload() { private fun reload() {
val selected = model.loadSelected(media) val selected = model.loadSelected(media)
//Find latest episode for subscription //Find latest episode for subscription
selected.latest = media.anime?.episodes?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f selected.latest =
selected.latest = media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest media.anime?.episodes?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f
selected.latest =
media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest
model.saveSelected(media.id, selected, requireActivity()) model.saveSelected(media.id, selected, requireActivity())
headerAdapter.handleEpisodes() headerAdapter.handleEpisodes()
val isDownloaded = model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)
episodeAdapter.offlineMode = isDownloaded
episodeAdapter.notifyItemRangeRemoved(0, episodeAdapter.arr.size) episodeAdapter.notifyItemRangeRemoved(0, episodeAdapter.arr.size)
var arr: ArrayList<Episode> = arrayListOf() var arr: ArrayList<Episode> = arrayListOf()
if (media.anime!!.episodes != null) { if (media.anime!!.episodes != null) {
@@ -293,11 +538,20 @@ class AnimeWatchFragment : Fragment() {
episodeAdapter.arr = arr episodeAdapter.arr = arr
episodeAdapter.updateType(style ?: uiSettings.animeDefaultView) episodeAdapter.updateType(style ?: uiSettings.animeDefaultView)
episodeAdapter.notifyItemRangeInserted(0, arr.size) episodeAdapter.notifyItemRangeInserted(0, arr.size)
for (download in downloadManager.animeDownloadedTypes) {
if (download.title == media.mainName()) {
episodeAdapter.stopDownload(download.chapter)
}
}
} }
override fun onDestroy() { override fun onDestroy() {
model.watchSources?.flushText() model.watchSources?.flushText()
super.onDestroy() super.onDestroy()
try {
requireContext().unregisterReceiver(downloadStatusReceiver)
} catch (_: IllegalArgumentException) {
}
} }
var state: Parcelable? = null var state: Parcelable? = null
@@ -312,4 +566,12 @@ class AnimeWatchFragment : Fragment() {
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState() state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState()
} }
companion object {
const val ACTION_DOWNLOAD_STARTED = "ani.dantotsu.ACTION_DOWNLOAD_STARTED"
const val ACTION_DOWNLOAD_FINISHED = "ani.dantotsu.ACTION_DOWNLOAD_FINISHED"
const val ACTION_DOWNLOAD_FAILED = "ani.dantotsu.ACTION_DOWNLOAD_FAILED"
const val ACTION_DOWNLOAD_PROGRESS = "ani.dantotsu.ACTION_DOWNLOAD_PROGRESS"
const val EXTRA_EPISODE_NUMBER = "extra_episode_number"
}
} }

View File

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

View File

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

View File

@@ -1,19 +1,33 @@
package ani.dantotsu.media.anime package ani.dantotsu.media.anime
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.LinearInterpolator
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.annotation.OptIn
import androidx.core.content.ContextCompat
import androidx.lifecycle.coroutineScope
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.DownloadIndex
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.connections.updateProgress import ani.dantotsu.connections.updateProgress
import ani.dantotsu.databinding.ItemEpisodeCompactBinding import ani.dantotsu.databinding.ItemEpisodeCompactBinding
import ani.dantotsu.databinding.ItemEpisodeGridBinding import ani.dantotsu.databinding.ItemEpisodeGridBinding
import ani.dantotsu.databinding.ItemEpisodeListBinding import ani.dantotsu.databinding.ItemEpisodeListBinding
import ani.dantotsu.download.anime.AnimeDownloaderService
import ani.dantotsu.download.video.Helper
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.GlideUrl
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.math.ln
import kotlin.math.pow
fun handleProgress(cont: LinearLayout, bar: View, empty: View, mediaId: Int, ep: String) { fun handleProgress(cont: LinearLayout, bar: View, empty: View, mediaId: Int, ep: String) {
val curr = loadData<Long>("${mediaId}_${ep}") val curr = loadData<Long>("${mediaId}_${ep}")
@@ -32,17 +46,42 @@ fun handleProgress(cont: LinearLayout, bar: View, empty: View, mediaId: Int, ep:
} }
} }
@OptIn(UnstableApi::class)
class EpisodeAdapter( class EpisodeAdapter(
private var type: Int, private var type: Int,
private val media: Media, private val media: Media,
private val fragment: AnimeWatchFragment, private val fragment: AnimeWatchFragment,
var arr: List<Episode> = arrayListOf() var arr: List<Episode> = arrayListOf(),
var offlineMode: Boolean
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private lateinit var index: DownloadIndex
init {
if (offlineMode) {
index = Helper.downloadManager(fragment.requireContext()).downloadIndex
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return (when (viewType) { return (when (viewType) {
0 -> EpisodeListViewHolder(ItemEpisodeListBinding.inflate(LayoutInflater.from(parent.context), parent, false)) 0 -> EpisodeListViewHolder(
1 -> EpisodeGridViewHolder(ItemEpisodeGridBinding.inflate(LayoutInflater.from(parent.context), parent, false)) ItemEpisodeListBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
1 -> EpisodeGridViewHolder(
ItemEpisodeGridBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
2 -> EpisodeCompactViewHolder( 2 -> EpisodeCompactViewHolder(
ItemEpisodeCompactBinding.inflate( ItemEpisodeCompactBinding.inflate(
LayoutInflater.from(parent.context), LayoutInflater.from(parent.context),
@@ -50,6 +89,7 @@ class EpisodeAdapter(
false false
) )
) )
else -> throw IllegalArgumentException() else -> throw IllegalArgumentException()
}) })
} }
@@ -61,18 +101,23 @@ class EpisodeAdapter(
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val ep = arr[position] val ep = arr[position]
val title = val title = if (!ep.title.isNullOrEmpty() && ep.title != "null") {
"${if (!ep.title.isNullOrEmpty() && ep.title != "null") "" else currContext()!!.getString(R.string.episode_singular)} ${if (!ep.title.isNullOrEmpty() && ep.title != "null") ep.title else ep.number}" ep.title?.let { AnimeNameAdapter.removeEpisodeNumber(it) }
} else {
ep.number
} ?: ""
when (holder) { when (holder) {
is EpisodeListViewHolder -> { is EpisodeListViewHolder -> {
val binding = holder.binding val binding = holder.binding
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings) setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
val thumb = ep.thumb?.let { if(it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null } val thumb =
Glide.with(binding.itemEpisodeImage).load(thumb?:media.cover).override(400,0).into(binding.itemEpisodeImage) ep.thumb?.let { if (it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null }
Glide.with(binding.itemEpisodeImage).load(thumb ?: media.cover).override(400, 0)
.into(binding.itemEpisodeImage)
binding.itemEpisodeNumber.text = ep.number binding.itemEpisodeNumber.text = ep.number
binding.itemEpisodeTitle.text = title binding.itemEpisodeTitle.text = if (ep.number == title) "Episode $title" else title
if (ep.filler) { if (ep.filler) {
binding.itemEpisodeFiller.visibility = View.VISIBLE binding.itemEpisodeFiller.visibility = View.VISIBLE
@@ -81,8 +126,10 @@ 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 = if (ep.desc != null && ep.desc?.trim(' ') != "") View.VISIBLE else View.GONE binding.itemEpisodeDesc.visibility =
if (ep.desc != null && ep.desc?.trim(' ') != "") View.VISIBLE else View.GONE
binding.itemEpisodeDesc.text = ep.desc ?: "" binding.itemEpisodeDesc.text = ep.desc ?: ""
holder.bind(ep.number, ep.downloadProgress , ep.desc)
if (media.userProgress != null) { if (media.userProgress != null) {
if ((ep.number.toFloatOrNull() ?: 9999f) <= media.userProgress!!.toFloat()) { if ((ep.number.toFloatOrNull() ?: 9999f) <= media.userProgress!!.toFloat()) {
@@ -114,8 +161,10 @@ class EpisodeAdapter(
val binding = holder.binding val binding = holder.binding
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings) setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
val thumb = ep.thumb?.let { if(it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null } val thumb =
Glide.with(binding.itemEpisodeImage).load(thumb?:media.cover).override(400,0).into(binding.itemEpisodeImage) ep.thumb?.let { if (it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null }
Glide.with(binding.itemEpisodeImage).load(thumb ?: media.cover).override(400, 0)
.into(binding.itemEpisodeImage)
binding.itemEpisodeNumber.text = ep.number binding.itemEpisodeNumber.text = ep.number
binding.itemEpisodeTitle.text = title binding.itemEpisodeTitle.text = title
@@ -155,7 +204,8 @@ class EpisodeAdapter(
val binding = holder.binding val binding = holder.binding
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings) setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
binding.itemEpisodeNumber.text = ep.number binding.itemEpisodeNumber.text = ep.number
binding.itemEpisodeFillerView.visibility = if (ep.filler) View.VISIBLE else View.GONE binding.itemEpisodeFillerView.visibility =
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
@@ -180,7 +230,82 @@ class EpisodeAdapter(
override fun getItemCount(): Int = arr.size override fun getItemCount(): Int = arr.size
inner class EpisodeCompactViewHolder(val binding: ItemEpisodeCompactBinding) : RecyclerView.ViewHolder(binding.root) { private val activeDownloads = mutableSetOf<String>()
private val downloadedEpisodes = mutableSetOf<String>()
fun startDownload(episodeNumber: String) {
activeDownloads.add(episodeNumber)
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == episodeNumber }
if (position != -1) {
notifyItemChanged(position)
}
}
@OptIn(UnstableApi::class)
fun stopDownload(episodeNumber: String) {
activeDownloads.remove(episodeNumber)
downloadedEpisodes.add(episodeNumber)
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == episodeNumber }
if (position != -1) {
val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(
media.mainName(),
episodeNumber
)
val id = fragment.requireContext().getSharedPreferences(
ContextCompat.getString(fragment.requireContext(), R.string.anime_downloads),
Context.MODE_PRIVATE
).getString(
taskName,
""
) ?: ""
val size = try {
val download = index.getDownload(id)
bytesToHuman(download?.bytesDownloaded ?: 0)
} catch (e: Exception) {
null
}
arr[position].downloadProgress = "Downloaded" + if (size != null) ": ($size)" else ""
notifyItemChanged(position)
}
}
fun deleteDownload(episodeNumber: String) {
downloadedEpisodes.remove(episodeNumber)
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == episodeNumber }
if (position != -1) {
arr[position].downloadProgress = null
notifyItemChanged(position)
}
}
fun purgeDownload(episodeNumber: String) {
activeDownloads.remove(episodeNumber)
downloadedEpisodes.remove(episodeNumber)
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == episodeNumber }
if (position != -1) {
arr[position].downloadProgress = "Failed"
notifyItemChanged(position)
}
}
fun updateDownloadProgress(episodeNumber: String, progress: Int) {
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == episodeNumber }
if (position != -1) {
arr[position].downloadProgress = "Downloading: $progress%"
notifyItemChanged(position)
}
}
inner class EpisodeCompactViewHolder(val binding: ItemEpisodeCompactBinding) :
RecyclerView.ViewHolder(binding.root) {
init { init {
itemView.setOnClickListener { itemView.setOnClickListener {
if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0) if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0)
@@ -189,7 +314,8 @@ class EpisodeAdapter(
} }
} }
inner class EpisodeGridViewHolder(val binding: ItemEpisodeGridBinding) : RecyclerView.ViewHolder(binding.root) { inner class EpisodeGridViewHolder(val binding: ItemEpisodeGridBinding) :
RecyclerView.ViewHolder(binding.root) {
init { init {
itemView.setOnClickListener { itemView.setOnClickListener {
if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0) if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0)
@@ -198,12 +324,38 @@ class EpisodeAdapter(
} }
} }
inner class EpisodeListViewHolder(val binding: ItemEpisodeListBinding) : RecyclerView.ViewHolder(binding.root) { inner class EpisodeListViewHolder(val binding: ItemEpisodeListBinding) :
RecyclerView.ViewHolder(binding.root) {
private val activeCoroutines = mutableSetOf<String>()
init { init {
itemView.setOnClickListener { itemView.setOnClickListener {
if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0) if (bindingAdapterPosition < arr.size && bindingAdapterPosition >= 0)
fragment.onEpisodeClick(arr[bindingAdapterPosition].number) fragment.onEpisodeClick(arr[bindingAdapterPosition].number)
} }
binding.itemDownload.setOnClickListener {
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size) {
val episodeNumber = arr[bindingAdapterPosition].number
if (activeDownloads.contains(episodeNumber)) {
fragment.onAnimeEpisodeStopDownloadClick(episodeNumber)
return@setOnClickListener
} else if (downloadedEpisodes.contains(episodeNumber)) {
val builder = AlertDialog.Builder(currContext(), R.style.MyPopup)
builder.setTitle("Delete Episode")
builder.setMessage("Are you sure you want to delete Episode ${episodeNumber}?")
builder.setPositiveButton("Yes") { _, _ ->
fragment.onAnimeEpisodeRemoveDownloadClick(episodeNumber)
}
builder.setNegativeButton("No") { _, _ ->
}
val dialog = builder.show()
dialog.window?.setDimAmount(0.8f)
return@setOnClickListener
} else {
fragment.onAnimeEpisodeDownloadClick(episodeNumber)
}
}
}
binding.itemEpisodeDesc.setOnClickListener { binding.itemEpisodeDesc.setOnClickListener {
if (binding.itemEpisodeDesc.maxLines == 3) if (binding.itemEpisodeDesc.maxLines == 3)
binding.itemEpisodeDesc.maxLines = 100 binding.itemEpisodeDesc.maxLines = 100
@@ -211,11 +363,77 @@ class EpisodeAdapter(
binding.itemEpisodeDesc.maxLines = 3 binding.itemEpisodeDesc.maxLines = 3
} }
} }
fun bind(episodeNumber: String, progress: String?, desc: String?) {
if (progress != null) {
binding.itemEpisodeDesc.visibility = View.GONE
binding.itemDownloadStatus.visibility = View.VISIBLE
binding.itemDownloadStatus.text = progress
} else {
binding.itemDownloadStatus.visibility = View.GONE
binding.itemDownloadStatus.text = ""
}
if (activeDownloads.contains(episodeNumber)) {
// Show spinner
binding.itemDownload.setImageResource(R.drawable.ic_sync)
startOrContinueRotation(episodeNumber)
binding.itemEpisodeDesc.visibility = View.GONE
} else if (downloadedEpisodes.contains(episodeNumber)) {
binding.itemEpisodeDesc.visibility = View.GONE
binding.itemDownloadStatus.visibility = View.VISIBLE
// Show checkmark
binding.itemDownload.setImageResource(R.drawable.ic_circle_check)
//binding.itemDownload.setColorFilter(typedValue2.data) //TODO: colors go to wrong places
binding.itemDownload.postDelayed({
binding.itemDownload.setImageResource(R.drawable.ic_round_delete_24)
binding.itemDownload.rotation = 0f
//binding.itemDownload.setColorFilter(typedValue2.data)
}, 1000)
} else {
binding.itemDownloadStatus.visibility = View.GONE
binding.itemEpisodeDesc.visibility = if (desc != null && desc.trim(' ') != "") View.VISIBLE else View.GONE
// Show download icon
binding.itemDownload.setImageResource(R.drawable.ic_circle_add)
binding.itemDownload.rotation = 0f
}
}
private fun startOrContinueRotation(episodeNumber: String) {
if (!isRotationCoroutineRunningFor(episodeNumber)) {
val scope = fragment.lifecycle.coroutineScope
scope.launch {
// Add chapter number to active coroutines set
activeCoroutines.add(episodeNumber)
while (activeDownloads.contains(episodeNumber)) {
binding.itemDownload.animate().rotationBy(360f).setDuration(1000)
.setInterpolator(
LinearInterpolator()
).start()
delay(1000)
}
// Remove chapter number from active coroutines set
activeCoroutines.remove(episodeNumber)
}
}
}
private fun isRotationCoroutineRunningFor(episodeNumber: String): Boolean {
return episodeNumber in activeCoroutines
}
} }
fun updateType(t: Int) { fun updateType(t: Int) {
type = t type = t
} }
private fun bytesToHuman(bytes: Long): String? {
if (bytes < 0) return null
val unit = 1000
if (bytes < unit) return "$bytes B"
val exp = (Math.log(bytes.toDouble()) / ln(unit.toDouble())).toInt()
val pre = ("KMGTPE")[exp - 1]
return String.format("%.1f %sB", bytes / unit.toDouble().pow(exp.toDouble()), pre)
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,13 @@
package ani.dantotsu.media.anime package ani.dantotsu.media.anime
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.TypedValue
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@@ -18,17 +21,21 @@ import ani.dantotsu.*
import ani.dantotsu.databinding.BottomSheetSelectorBinding import ani.dantotsu.databinding.BottomSheetSelectorBinding
import ani.dantotsu.databinding.ItemStreamBinding import ani.dantotsu.databinding.ItemStreamBinding
import ani.dantotsu.databinding.ItemUrlBinding import ani.dantotsu.databinding.ItemUrlBinding
import ani.dantotsu.download.video.Helper
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.others.Download.download import ani.dantotsu.others.Download.download
import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.VideoExtractor import ani.dantotsu.parsers.VideoExtractor
import ani.dantotsu.parsers.VideoType import ani.dantotsu.parsers.VideoType
import com.google.firebase.crashlytics.FirebaseCrashlytics
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.text.DecimalFormat import java.text.DecimalFormat
class SelectorDialogFragment : BottomSheetDialogFragment() { class SelectorDialogFragment : BottomSheetDialogFragment() {
private var _binding: BottomSheetSelectorBinding? = null private var _binding: BottomSheetSelectorBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
@@ -40,6 +47,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
private var makeDefault = false private var makeDefault = false
private var selected: String? = null private var selected: String? = null
private var launch: Boolean? = null private var launch: Boolean? = null
private var isDownloadMenu: Boolean? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -47,11 +55,22 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
selected = it.getString("server") selected = it.getString("server")
launch = it.getBoolean("launch", true) launch = it.getBoolean("launch", true)
prevEpisode = it.getString("prev") prevEpisode = it.getString("prev")
isDownloadMenu = it.getBoolean("isDownload")
} }
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = BottomSheetSelectorBinding.inflate(inflater, container, false) _binding = BottomSheetSelectorBinding.inflate(inflater, container, false)
val window = dialog?.window
window?.statusBarColor = Color.TRANSPARENT
val typedValue = TypedValue()
val theme = requireContext().theme
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
window?.navigationBarColor = typedValue.data
return binding.root return binding.root
} }
@@ -64,8 +83,11 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
val ep = media?.anime?.episodes?.get(media?.anime?.selectedEpisode) val ep = media?.anime?.episodes?.get(media?.anime?.selectedEpisode)
episode = ep episode = ep
if (ep != null) { if (ep != null) {
if (isDownloadMenu == true) {
binding.selectorMakeDefault.visibility = View.GONE
}
if (selected != null) { if (selected != null && isDownloadMenu == false) {
binding.selectorListContainer.visibility = View.GONE binding.selectorListContainer.visibility = View.GONE
binding.selectorAutoListContainer.visibility = View.VISIBLE binding.selectorAutoListContainer.visibility = View.VISIBLE
binding.selectorAutoText.text = selected binding.selectorAutoText.text = selected
@@ -82,10 +104,18 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
} }
fun load() { fun load() {
val size = ep.extractors?.find { it.server.name == selected }?.videos?.size val size =
if (model.watchSources!!.isDownloadedSource(media!!.selected!!.sourceIndex)) {
ep.extractors?.firstOrNull()?.videos?.size
} else {
ep.extractors?.find { it.server.name == selected }?.videos?.size
}
if (size != null && size >= media!!.selected!!.video) { if (size != null && size >= media!!.selected!!.video) {
media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedExtractor = selected media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedExtractor =
media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedVideo = media!!.selected!!.video selected
media!!.anime!!.episodes?.get(media!!.anime!!.selectedEpisode!!)?.selectedVideo =
media!!.selected!!.video
startExoplayer(media!!) startExoplayer(media!!)
} else fail() } else fail()
} }
@@ -106,8 +136,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
}) fail() }) fail()
} }
} else load() } else load()
} } else {
else {
binding.selectorRecyclerView.updateLayoutParams<ViewGroup.MarginLayoutParams> { binding.selectorRecyclerView.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight bottomMargin = navBarHeight
} }
@@ -120,18 +149,28 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
saveData("make_default", makeDefault) saveData("make_default", makeDefault)
} }
binding.selectorRecyclerView.layoutManager = binding.selectorRecyclerView.layoutManager =
LinearLayoutManager(requireActivity(), LinearLayoutManager.VERTICAL, false) LinearLayoutManager(
requireActivity(),
LinearLayoutManager.VERTICAL,
false
)
val adapter = ExtractorAdapter() val adapter = ExtractorAdapter()
binding.selectorRecyclerView.adapter = adapter binding.selectorRecyclerView.adapter = adapter
if (!ep.allStreams) { if (!ep.allStreams) {
ep.extractorCallback = { ep.extractorCallback = {
scope.launch { scope.launch {
adapter.add(it) adapter.add(it)
if (model.watchSources!!.isDownloadedSource(media?.selected!!.sourceIndex)) {
adapter.performClick(0)
}
} }
} }
model.getEpisode().observe(this) { model.getEpisode().observe(this) {
if (it != null) { if (it != null) {
media!!.anime?.episodes?.set(media!!.anime?.selectedEpisode!!, ep) media!!.anime?.episodes?.set(
media!!.anime?.selectedEpisode!!,
ep
)
} }
} }
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
@@ -143,6 +182,9 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
} else { } else {
media!!.anime?.episodes?.set(media!!.anime?.selectedEpisode!!, ep) media!!.anime?.episodes?.set(media!!.anime?.selectedEpisode!!, ep)
adapter.addAll(ep.extractors) adapter.addAll(ep.extractors)
if (model.watchSources!!.isDownloadedSource(media?.selected!!.sourceIndex)) {
adapter.performClick(0)
}
binding.selectorProgressBar.visibility = View.GONE binding.selectorProgressBar.visibility = View.GONE
} }
} }
@@ -158,14 +200,17 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
prevEpisode = null prevEpisode = null
dismiss() dismiss()
if (launch!!) { if (launch!! || model.watchSources!!.isDownloadedSource(media.selected!!.sourceIndex)) {
stopAddingToList() stopAddingToList()
val intent = Intent(activity, ExoplayerView::class.java) val intent = Intent(activity, ExoplayerView::class.java)
ExoplayerView.media = media ExoplayerView.media = media
ExoplayerView.initialized = true ExoplayerView.initialized = true
startActivity(intent) startActivity(intent)
} else { } else {
model.setEpisode(media.anime!!.episodes!![media.anime.selectedEpisode!!]!!, "startExo no launch") model.setEpisode(
media.anime!!.episodes!![media.anime.selectedEpisode!!]!!,
"startExo no launch"
)
} }
} }
@@ -176,14 +221,22 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
} }
} }
private inner class ExtractorAdapter : RecyclerView.Adapter<ExtractorAdapter.StreamViewHolder>() { private inner class ExtractorAdapter :
RecyclerView.Adapter<ExtractorAdapter.StreamViewHolder>() {
val links = mutableListOf<VideoExtractor>() val links = mutableListOf<VideoExtractor>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StreamViewHolder = override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StreamViewHolder =
StreamViewHolder(ItemStreamBinding.inflate(LayoutInflater.from(parent.context), parent, false)) StreamViewHolder(
ItemStreamBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) { override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
val extractor = links[position] val extractor = links[position]
holder.binding.streamName.text = extractor.server.name holder.binding.streamName.text = ""//extractor.server.name
holder.binding.streamName.visibility = View.GONE
holder.binding.streamRecyclerView.layoutManager = LinearLayoutManager(requireContext()) holder.binding.streamRecyclerView.layoutManager = LinearLayoutManager(requireContext())
holder.binding.streamRecyclerView.adapter = VideoAdapter(extractor) holder.binding.streamRecyclerView.adapter = VideoAdapter(extractor)
@@ -204,55 +257,168 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
notifyItemRangeInserted(0, extractors.size) notifyItemRangeInserted(0, extractors.size)
} }
private inner class StreamViewHolder(val binding: ItemStreamBinding) : RecyclerView.ViewHolder(binding.root) fun performClick(position: Int) {
try { //bandaid fix for crash
val extractor = links[position]
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedExtractor =
extractor.server.name
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedVideo = 0
startExoplayer(media!!)
} catch (e: Exception) {
FirebaseCrashlytics.getInstance().recordException(e)
}
} }
private inner class VideoAdapter(private val extractor : VideoExtractor) : RecyclerView.Adapter<VideoAdapter.UrlViewHolder>() { private inner class StreamViewHolder(val binding: ItemStreamBinding) :
RecyclerView.ViewHolder(binding.root)
}
private inner class VideoAdapter(private val extractor: VideoExtractor) :
RecyclerView.Adapter<VideoAdapter.UrlViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UrlViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UrlViewHolder {
return UrlViewHolder(ItemUrlBinding.inflate(LayoutInflater.from(parent.context), parent, false)) return UrlViewHolder(
ItemUrlBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
} }
@SuppressLint("SetTextI18n") @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]
binding.urlQuality.text = if(video.quality!=null) "${video.quality}p" else "Default Quality" if (isDownloadMenu == true) {
binding.urlNote.text = video.extraNote ?: ""
binding.urlNote.visibility = if (video.extraNote != null) View.VISIBLE else View.GONE
binding.urlDownload.visibility = View.VISIBLE binding.urlDownload.visibility = View.VISIBLE
} else {
binding.urlDownload.visibility = View.GONE
}
binding.urlDownload.setSafeOnClickListener { binding.urlDownload.setSafeOnClickListener {
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor = extractor.server.name media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor =
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedVideo = position extractor.server.name
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedVideo =
position
binding.urlDownload.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) binding.urlDownload.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
val episode = media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!
val selectedVideo =
if (extractor.videos.size > episode.selectedVideo) extractor.videos[episode.selectedVideo] else null
val subtitles = extractor.subtitles
val subtitleNames = subtitles.map { it.language }
var subtitleToDownload: Subtitle? = null
if (subtitles.isNotEmpty()) {
val alertDialog = AlertDialog.Builder(context, R.style.MyPopup)
.setTitle("Download Subtitle")
.setSingleChoiceItems(
subtitleNames.toTypedArray(),
-1
) { dialog, which ->
subtitleToDownload = subtitles[which]
}
.setPositiveButton("Download") { _, _ ->
dialog?.dismiss()
if (selectedVideo != null) {
Helper.startAnimeDownloadService(
currActivity()!!,
media!!.mainName(),
episode.number,
selectedVideo,
subtitleToDownload,
media,
episode.thumb?.url ?: media!!.banner ?: media!!.cover
)
} else {
snackString("No Video Selected")
}
}
.setNegativeButton("Skip") { dialog, _ ->
subtitleToDownload = null
if (selectedVideo != null) {
Helper.startAnimeDownloadService(
currActivity()!!,
media!!.mainName(),
episode.number,
selectedVideo,
subtitleToDownload,
media,
episode.thumb?.url ?: media!!.banner ?: media!!.cover
)
} else {
snackString("No Video Selected")
}
dialog.dismiss()
}
.setNeutralButton("Cancel") { dialog, _ ->
subtitleToDownload = null
dialog.dismiss()
}
.show()
alertDialog.window?.setDimAmount(0.8f)
} else {
if (selectedVideo != null) {
Helper.startAnimeDownloadService(
requireActivity(),
media!!.mainName(),
episode.number,
selectedVideo,
subtitleToDownload,
media,
episode.thumb?.url ?: media!!.banner ?: media!!.cover
)
} else {
snackString("No Video Selected")
}
}
dismiss()
}
binding.urlDownload.setOnLongClickListener {
binding.urlDownload.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
if ((loadData<Int>("settings_download_manager") ?: 0) != 0) {
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedExtractor =
extractor.server.name
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!.selectedVideo =
position
download( download(
requireActivity(), requireActivity(),
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!, media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!!,
media!!.userPreferredName media!!.userPreferredName
) )
dismiss() } else {
snackString("No Download Manager Selected")
}
true
} }
if (video.format == VideoType.CONTAINER) { if (video.format == VideoType.CONTAINER) {
binding.urlSize.visibility = if (video.size != null) View.VISIBLE else View.GONE binding.urlSize.visibility = if (video.size != null) View.VISIBLE else View.GONE
binding.urlSize.text = binding.urlSize.text =
(if (video.extraNote != null) " : " else "") + DecimalFormat("#.##").format(video.size ?: 0).toString() + " MB" // if video size is null or 0, show "Unknown Size" else show the size in MB
} (if (video.extraNote != null) " : " else "") + (if (video.size == 0.0) "Unknown Size" else (DecimalFormat(
else { "#.##"
binding.urlQuality.text = "Multi Quality" ).format(video.size ?: 0).toString() + " MB"))
if ((loadData<Int>("settings_download_manager") ?: 0) == 0) {
binding.urlDownload.visibility = View.GONE
}
} }
binding.urlNote.visibility = View.VISIBLE
binding.urlNote.text = video.format.name
binding.urlQuality.text = extractor.server.name
} }
override fun getItemCount(): Int = extractor.videos.size override fun getItemCount(): Int = extractor.videos.size
private inner class UrlViewHolder(val binding: ItemUrlBinding) : RecyclerView.ViewHolder(binding.root) { private inner class UrlViewHolder(val binding: ItemUrlBinding) :
RecyclerView.ViewHolder(binding.root) {
init { init {
itemView.setSafeOnClickListener { itemView.setSafeOnClickListener {
if (isDownloadMenu == true) {
binding.urlDownload.performClick()
return@setSafeOnClickListener
}
tryWith(true) { tryWith(true) {
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedExtractor = extractor.server.name media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedExtractor =
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedVideo = bindingAdapterPosition extractor.server.name
media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]?.selectedVideo =
bindingAdapterPosition
if (makeDefault) { if (makeDefault) {
media!!.selected!!.server = extractor.server.name media!!.selected!!.server = extractor.server.name
media!!.selected!!.video = bindingAdapterPosition media!!.selected!!.video = bindingAdapterPosition
@@ -276,12 +442,18 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
} }
companion object { companion object {
fun newInstance(server: String? = null, la: Boolean = true, prev: String? = null): SelectorDialogFragment = fun newInstance(
server: String? = null,
la: Boolean = true,
prev: String? = null,
isDownload: Boolean
): SelectorDialogFragment =
SelectorDialogFragment().apply { SelectorDialogFragment().apply {
arguments = Bundle().apply { arguments = Bundle().apply {
putString("server", server) putString("server", server)
putBoolean("launch", la) putBoolean("launch", la)
putString("prev", prev) putString("prev", prev)
putBoolean("isDownload", isDownload)
} }
} }
} }

View File

@@ -6,7 +6,9 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.OptIn
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.BottomSheetDialogFragment import ani.dantotsu.BottomSheetDialogFragment
@@ -24,7 +26,11 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
val model: MediaDetailsViewModel by activityViewModels() val model: MediaDetailsViewModel by activityViewModels()
private lateinit var episode: Episode private lateinit var episode: Episode
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = BottomSheetSubtitlesBinding.inflate(inflater, container, false) _binding = BottomSheetSubtitlesBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }
@@ -34,18 +40,29 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() {
model.getMedia().observe(viewLifecycleOwner) { media -> model.getMedia().observe(viewLifecycleOwner) { media ->
episode = media?.anime?.episodes?.get(media.anime.selectedEpisode) ?: return@observe episode = media?.anime?.episodes?.get(media.anime.selectedEpisode) ?: return@observe
val currentExtractor = episode.extractors?.find { it.server.name == episode.selectedExtractor } ?: return@observe val currentExtractor =
episode.extractors?.find { it.server.name == episode.selectedExtractor }
?: return@observe
binding.subtitlesRecycler.layoutManager = LinearLayoutManager(requireContext()) binding.subtitlesRecycler.layoutManager = LinearLayoutManager(requireContext())
binding.subtitlesRecycler.adapter = SubtitleAdapter(currentExtractor.subtitles) binding.subtitlesRecycler.adapter = SubtitleAdapter(currentExtractor.subtitles)
} }
} }
inner class SubtitleAdapter(val subtitles: List<Subtitle>) : RecyclerView.Adapter<SubtitleAdapter.StreamViewHolder>() { inner class SubtitleAdapter(val subtitles: List<Subtitle>) :
inner class StreamViewHolder(val binding: ItemSubtitleTextBinding) : RecyclerView.ViewHolder(binding.root) RecyclerView.Adapter<SubtitleAdapter.StreamViewHolder>() {
inner class StreamViewHolder(val binding: ItemSubtitleTextBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StreamViewHolder = override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StreamViewHolder =
StreamViewHolder(ItemSubtitleTextBinding.inflate(LayoutInflater.from(parent.context), parent, false)) StreamViewHolder(
ItemSubtitleTextBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
@OptIn(UnstableApi::class)
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) { override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
val binding = holder.binding val binding = holder.binding
if (position == 0) { if (position == 0) {

View File

@@ -14,7 +14,10 @@ object VideoCache {
val databaseProvider = StandaloneDatabaseProvider(context) val databaseProvider = StandaloneDatabaseProvider(context)
if (simpleCache == null) if (simpleCache == null)
simpleCache = SimpleCache( simpleCache = SimpleCache(
File(context.cacheDir, "exoplayer").also { it.deleteOnExit() }, // Ensures always fresh file File(
context.cacheDir,
"exoplayer"
).also { it.deleteOnExit() }, // Ensures always fresh file
LeastRecentlyUsedCacheEvictor(300L * 1024L * 1024L), LeastRecentlyUsedCacheEvictor(300L * 1024L * 1024L),
databaseProvider databaseProvider
) )

View File

@@ -10,6 +10,8 @@ import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.MediaStore import android.provider.MediaStore
import android.util.LruCache import android.util.LruCache
import ani.dantotsu.logger
import ani.dantotsu.snackString
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -21,43 +23,58 @@ data class ImageData(
val page: Page, val page: Page,
val source: HttpSource val source: HttpSource
) { ) {
suspend fun fetchAndProcessImage(page: Page, httpSource: HttpSource, context: Context): Bitmap? { suspend fun fetchAndProcessImage(
page: Page,
httpSource: HttpSource,
context: Context
): Bitmap? {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
try { try {
// Fetch the image // Fetch the image
val response = httpSource.getImage(page) val response = httpSource.getImage(page)
println("Response: ${response.code}") logger("Response: ${response.code}")
println("Response: ${response.message}") logger("Response: ${response.message}")
// Convert the Response to an InputStream // Convert the Response to an InputStream
val inputStream = response.body?.byteStream() val inputStream = response.body.byteStream()
// Convert InputStream to Bitmap // Convert InputStream to Bitmap
val bitmap = BitmapFactory.decodeStream(inputStream) val bitmap = BitmapFactory.decodeStream(inputStream)
inputStream?.close() inputStream.close()
saveImage(bitmap, context.contentResolver, page.imageUrl!!, Bitmap.CompressFormat.JPEG, 100) //saveImage(bitmap, context.contentResolver, page.imageUrl!!, Bitmap.CompressFormat.JPEG, 100)
return@withContext bitmap return@withContext bitmap
} catch (e: Exception) { } catch (e: Exception) {
// Handle any exceptions // Handle any exceptions
println("An error occurred: ${e.message}") logger("An error occurred: ${e.message}")
snackString("An error occurred: ${e.message}")
return@withContext null return@withContext null
} }
} }
} }
} }
fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String, format: Bitmap.CompressFormat, quality: Int) { fun saveImage(
bitmap: Bitmap,
contentResolver: ContentResolver,
filename: String,
format: Bitmap.CompressFormat,
quality: Int
) {
try { try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val contentValues = ContentValues().apply { val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, filename) put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
put(MediaStore.MediaColumns.MIME_TYPE, "image/${format.name.lowercase()}") put(MediaStore.MediaColumns.MIME_TYPE, "image/${format.name.lowercase()}")
put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/Manga") put(
MediaStore.MediaColumns.RELATIVE_PATH,
"${Environment.DIRECTORY_DOWNLOADS}/Dantotsu/Manga"
)
} }
val uri: Uri? = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) val uri: Uri? =
contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
uri?.let { uri?.let {
contentResolver.openOutputStream(it)?.use { os -> contentResolver.openOutputStream(it)?.use { os ->
@@ -65,7 +82,8 @@ fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String
} }
} }
} else { } else {
val directory = File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Anime") val directory =
File("${Environment.getExternalStorageDirectory()}${File.separator}Dantotsu${File.separator}Manga")
if (!directory.exists()) { if (!directory.exists()) {
directory.mkdirs() directory.mkdirs()
} }
@@ -81,7 +99,7 @@ fun saveImage(bitmap: Bitmap, contentResolver: ContentResolver, filename: String
} }
} }
class MangaCache() { class MangaCache {
private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024 / 2).toInt() private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024 / 2).toInt()
private val cache = LruCache<String, ImageData>(maxMemory) private val cache = LruCache<String, ImageData>(maxMemory)

View File

@@ -11,9 +11,18 @@ data class MangaChapter(
var link: String, var link: String,
var title: String? = null, var title: String? = null,
var description: String? = null, var description: String? = null,
var sChapter: SChapter var sChapter: SChapter,
val scanlator: String? = null,
var progress: String? = ""
) : Serializable { ) : Serializable {
constructor(chapter: MangaChapter) : this(chapter.number, chapter.link, chapter.title, chapter.description, chapter.sChapter) constructor(chapter: MangaChapter) : this(
chapter.number,
chapter.link,
chapter.title,
chapter.description,
chapter.sChapter,
chapter.scanlator
)
private val images = mutableListOf<MangaImage>() private val images = mutableListOf<MangaImage>()
fun images(): List<MangaImage> = images fun images(): List<MangaImage> = images

View File

@@ -1,16 +1,23 @@
package ani.dantotsu.media.manga package ani.dantotsu.media.manga
import android.app.AlertDialog
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.LinearInterpolator
import android.widget.NumberPicker
import androidx.lifecycle.coroutineScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.connections.updateProgress
import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemChapterListBinding import ani.dantotsu.databinding.ItemChapterListBinding
import ani.dantotsu.databinding.ItemEpisodeCompactBinding import ani.dantotsu.databinding.ItemEpisodeCompactBinding
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.setAnimation import ani.dantotsu.setAnimation
import ani.dantotsu.connections.updateProgress import kotlinx.coroutines.delay
import java.util.regex.Matcher import kotlinx.coroutines.launch
import java.util.regex.Pattern
class MangaChapterAdapter( class MangaChapterAdapter(
private var type: Int, private var type: Int,
@@ -28,7 +35,15 @@ class MangaChapterAdapter(
false false
) )
) )
0 -> ChapterListViewHolder(ItemChapterListBinding.inflate(LayoutInflater.from(parent.context), parent, false))
0 -> ChapterListViewHolder(
ItemChapterListBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
else -> throw IllegalArgumentException() else -> throw IllegalArgumentException()
} }
} }
@@ -39,7 +54,8 @@ class MangaChapterAdapter(
override fun getItemCount(): Int = arr.size override fun getItemCount(): Int = arr.size
inner class ChapterCompactViewHolder(val binding: ItemEpisodeCompactBinding) : RecyclerView.ViewHolder(binding.root) { inner class ChapterCompactViewHolder(val binding: ItemEpisodeCompactBinding) :
RecyclerView.ViewHolder(binding.root) {
init { init {
itemView.setOnClickListener { itemView.setOnClickListener {
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size) if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size)
@@ -48,12 +64,196 @@ class MangaChapterAdapter(
} }
} }
inner class ChapterListViewHolder(val binding: ItemChapterListBinding) : RecyclerView.ViewHolder(binding.root) { private val activeDownloads = mutableSetOf<String>()
private val downloadedChapters = mutableSetOf<String>()
fun startDownload(chapterNumber: String) {
activeDownloads.add(chapterNumber)
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == chapterNumber }
if (position != -1) {
notifyItemChanged(position)
}
}
fun stopDownload(chapterNumber: String) {
activeDownloads.remove(chapterNumber)
downloadedChapters.add(chapterNumber)
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == chapterNumber }
if (position != -1) {
arr[position].progress = "Downloaded"
notifyItemChanged(position)
}
}
fun deleteDownload(chapterNumber: String) {
downloadedChapters.remove(chapterNumber)
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == chapterNumber }
if (position != -1) {
arr[position].progress = ""
notifyItemChanged(position)
}
}
fun purgeDownload(chapterNumber: String) {
activeDownloads.remove(chapterNumber)
downloadedChapters.remove(chapterNumber)
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == chapterNumber }
if (position != -1) {
arr[position].progress = ""
notifyItemChanged(position)
}
}
fun updateDownloadProgress(chapterNumber: String, progress: Int) {
// Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == chapterNumber }
if (position != -1) {
arr[position].progress = "Downloading: ${progress}%"
notifyItemChanged(position)
}
}
fun downloadNChaptersFrom(position: Int, n: Int) {
//download next n chapters
if (position < 0 || position >= arr.size) return
for (i in 0..<n) {
if (position + i < arr.size) {
val chapterNumber = arr[position + i].number
if (activeDownloads.contains(chapterNumber)) {
//do nothing
continue
} else if (downloadedChapters.contains(chapterNumber)) {
//do nothing
continue
} else {
fragment.onMangaChapterDownloadClick(chapterNumber)
startDownload(chapterNumber)
}
}
}
}
inner class ChapterListViewHolder(val binding: ItemChapterListBinding) :
RecyclerView.ViewHolder(binding.root) {
private val activeCoroutines = mutableSetOf<String>()
private val typedValue1 = TypedValue()
private val typedValue2 = TypedValue()
fun bind(chapterNumber: String, progress: String?) {
if (progress != null) {
binding.itemChapterTitle.visibility = View.VISIBLE
binding.itemChapterTitle.text = "$progress"
} else {
binding.itemChapterTitle.visibility = View.GONE
binding.itemChapterTitle.text = ""
}
if (activeDownloads.contains(chapterNumber)) {
// Show spinner
binding.itemDownload.setImageResource(R.drawable.ic_sync)
startOrContinueRotation(chapterNumber)
} else if (downloadedChapters.contains(chapterNumber)) {
// Show checkmark
binding.itemDownload.setImageResource(R.drawable.ic_circle_check)
//binding.itemDownload.setColorFilter(typedValue2.data) //TODO: colors go to wrong places
binding.itemDownload.postDelayed({
binding.itemDownload.setImageResource(R.drawable.ic_round_delete_24)
binding.itemDownload.rotation = 0f
//binding.itemDownload.setColorFilter(typedValue2.data)
}, 1000)
} else {
// Show download icon
binding.itemDownload.setImageResource(R.drawable.ic_circle_add)
binding.itemDownload.rotation = 0f
}
}
private fun startOrContinueRotation(chapterNumber: String) {
if (!isRotationCoroutineRunningFor(chapterNumber)) {
val scope = fragment.lifecycle.coroutineScope
scope.launch {
// Add chapter number to active coroutines set
activeCoroutines.add(chapterNumber)
while (activeDownloads.contains(chapterNumber)) {
binding.itemDownload.animate().rotationBy(360f).setDuration(1000)
.setInterpolator(
LinearInterpolator()
).start()
delay(1000)
}
// Remove chapter number from active coroutines set
activeCoroutines.remove(chapterNumber)
}
}
}
private fun isRotationCoroutineRunningFor(chapterNumber: String): Boolean {
return chapterNumber in activeCoroutines
}
init { init {
val theme = currContext()?.theme
theme?.resolveAttribute(
com.google.android.material.R.attr.colorError,
typedValue1,
true
)
theme?.resolveAttribute(
com.google.android.material.R.attr.colorPrimary,
typedValue2,
true
)
itemView.setOnClickListener { itemView.setOnClickListener {
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size) if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size)
fragment.onMangaChapterClick(arr[bindingAdapterPosition].number) fragment.onMangaChapterClick(arr[bindingAdapterPosition].number)
} }
binding.itemDownload.setOnClickListener {
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size) {
val chapterNumber = arr[bindingAdapterPosition].number
if (activeDownloads.contains(chapterNumber)) {
fragment.onMangaChapterStopDownloadClick(chapterNumber)
return@setOnClickListener
} else if (downloadedChapters.contains(chapterNumber)) {
val builder = AlertDialog.Builder(currContext(), R.style.MyPopup)
builder.setTitle("Delete Chapter")
builder.setMessage("Are you sure you want to delete ${chapterNumber}?")
builder.setPositiveButton("Yes") { _, _ ->
fragment.onMangaChapterRemoveDownloadClick(chapterNumber)
}
builder.setNegativeButton("No") { _, _ ->
}
val dialog = builder.show()
dialog.window?.setDimAmount(0.8f)
return@setOnClickListener
} else {
fragment.onMangaChapterDownloadClick(chapterNumber)
startDownload(chapterNumber)
}
}
}
binding.itemDownload.setOnLongClickListener {
//Alert dialog asking for the number of chapters to download
val alertDialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
alertDialog.setTitle("Multi Chapter Downloader")
alertDialog.setMessage("Enter the number of chapters to download")
val input = NumberPicker(currContext())
input.minValue = 1
input.maxValue = itemCount - bindingAdapterPosition
input.value = 1
alertDialog.setView(input)
alertDialog.setPositiveButton("OK") { _, _ ->
downloadNChaptersFrom(bindingAdapterPosition, input.value)
}
alertDialog.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() }
val dialog = alertDialog.show()
dialog.window?.setDimAmount(0.8f)
true
}
} }
} }
@@ -63,44 +263,50 @@ class MangaChapterAdapter(
val binding = holder.binding val binding = holder.binding
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings) setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
val ep = arr[position] val ep = arr[position]
binding.itemEpisodeNumber.text = ep.number val parsedNumber = MangaNameAdapter.findChapterNumber(ep.number)?.toInt()
binding.itemEpisodeNumber.text = parsedNumber?.toString() ?: ep.number
if (media.userProgress != null) { if (media.userProgress != null) {
if ((MangaNameAdapter.findChapterNumber(ep.number) ?: 9999f) <= media.userProgress!!.toFloat()) if ((MangaNameAdapter.findChapterNumber(ep.number)
?: 9999f) <= media.userProgress!!.toFloat()
)
binding.itemEpisodeViewedCover.visibility = View.VISIBLE binding.itemEpisodeViewedCover.visibility = View.VISIBLE
else { else {
binding.itemEpisodeViewedCover.visibility = View.GONE binding.itemEpisodeViewedCover.visibility = View.GONE
binding.itemEpisodeCont.setOnLongClickListener { binding.itemEpisodeCont.setOnLongClickListener {
updateProgress(media, MangaNameAdapter.findChapterNumber(ep.number).toString()) updateProgress(
media,
MangaNameAdapter.findChapterNumber(ep.number).toString()
)
true true
} }
} }
} }
} }
is ChapterListViewHolder -> { is ChapterListViewHolder -> {
val binding = holder.binding val binding = holder.binding
val ep = arr[position] val ep = arr[position]
holder.bind(ep.number, ep.progress)
setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings) setAnimation(fragment.requireContext(), holder.binding.root, fragment.uiSettings)
binding.itemChapterNumber.text = ep.number binding.itemChapterNumber.text = ep.number
if (!ep.title.isNullOrEmpty()) { if (ep.progress.isNullOrEmpty()) {
binding.itemChapterTitle.text = ep.title binding.itemChapterTitle.visibility = View.GONE
binding.itemChapterTitle.setOnLongClickListener { } else binding.itemChapterTitle.visibility = View.VISIBLE
binding.itemChapterTitle.maxLines.apply {
binding.itemChapterTitle.maxLines = if (this == 1) 3 else 1
}
true
}
binding.itemChapterTitle.visibility = View.VISIBLE
} else binding.itemChapterTitle.visibility = View.GONE
if (media.userProgress != null) { if (media.userProgress != null) {
if ((MangaNameAdapter.findChapterNumber(ep.number) ?: 9999f) <= media.userProgress!!.toFloat()) { if ((MangaNameAdapter.findChapterNumber(ep.number)
?: 9999f) <= media.userProgress!!.toFloat()
) {
binding.itemEpisodeViewedCover.visibility = View.VISIBLE binding.itemEpisodeViewedCover.visibility = View.VISIBLE
binding.itemEpisodeViewed.visibility = View.VISIBLE binding.itemEpisodeViewed.visibility = View.VISIBLE
} else { } else {
binding.itemEpisodeViewedCover.visibility = View.GONE binding.itemEpisodeViewedCover.visibility = View.GONE
binding.itemEpisodeViewed.visibility = View.GONE binding.itemEpisodeViewed.visibility = View.GONE
binding.root.setOnLongClickListener { binding.root.setOnLongClickListener {
updateProgress(media, MangaNameAdapter.findChapterNumber(ep.number).toString()) updateProgress(
media,
MangaNameAdapter.findChapterNumber(ep.number).toString()
)
true true
} }
} }

View File

@@ -5,16 +5,25 @@ import java.util.regex.Pattern
class MangaNameAdapter { class MangaNameAdapter {
companion object { companion object {
const val chapterRegex = "(chapter|chap|ch|c)[\\s:.\\-]*([\\d]+\\.?[\\d]*)[\\s:.\\-]*"
const val filedChapterNumberRegex = "(?<!part\\s)\\b(\\d+)\\b"
fun findChapterNumber(text: String): Float? { fun findChapterNumber(text: String): Float? {
val regex = "(chapter|chap|ch|c)[\\s:.\\-]*([\\d]+\\.?[\\d]*)" val pattern: Pattern = Pattern.compile(chapterRegex, Pattern.CASE_INSENSITIVE)
val pattern: Pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE)
val matcher: Matcher = pattern.matcher(text) val matcher: Matcher = pattern.matcher(text)
return if (matcher.find()) { return if (matcher.find()) {
matcher.group(2)?.toFloat() matcher.group(2)?.toFloat()
} else {
val failedChapterNumberPattern: Pattern =
Pattern.compile(filedChapterNumberRegex, Pattern.CASE_INSENSITIVE)
val failedChapterNumberMatcher: Matcher =
failedChapterNumberPattern.matcher(text)
if (failedChapterNumberMatcher.find()) {
failedChapterNumberMatcher.group(1)?.toFloat()
} else { } else {
null null
} }
} }
} }
} }
}

View File

@@ -1,28 +1,42 @@
package ani.dantotsu.media.manga package ani.dantotsu.media.manga
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.ImageView import android.widget.CheckBox
import android.widget.ImageButton
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.NumberPicker
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.media.anime.handleProgress import ani.dantotsu.databinding.DialogLayoutBinding
import ani.dantotsu.databinding.ItemAnimeWatchBinding import ani.dantotsu.databinding.ItemAnimeWatchBinding
import ani.dantotsu.databinding.ItemChipBinding import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.SourceSearchDialogFragment import ani.dantotsu.media.SourceSearchDialogFragment
import ani.dantotsu.media.anime.handleProgress
import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.others.webview.CookieCatcher
import ani.dantotsu.parsers.DynamicMangaParser
import ani.dantotsu.parsers.MangaReadSources import ani.dantotsu.parsers.MangaReadSources
import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.subcriptions.Notifications.Companion.openSettings import ani.dantotsu.subcriptions.Notifications.Companion.openSettings
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.system.WebViewUtil
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class MangaReadAdapter( class MangaReadAdapter(
private val media: Media, private val media: Media,
private val fragment: MangaReadFragment, private val fragment: MangaReadFragment,
@@ -31,12 +45,17 @@ class MangaReadAdapter(
var subscribe: MediaDetailsActivity.PopImageButton? = null var subscribe: MediaDetailsActivity.PopImageButton? = null
private var _binding: ItemAnimeWatchBinding? = null private var _binding: ItemAnimeWatchBinding? = null
val hiddenScanlators = mutableListOf<String>()
var scanlatorSelectionListener: ScanlatorSelectionListener? = null
var options = listOf<String>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val bind = ItemAnimeWatchBinding.inflate(LayoutInflater.from(parent.context), parent, false) val bind = ItemAnimeWatchBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(bind) return ViewHolder(bind)
} }
private var nestedDialog: AlertDialog? = null
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding = holder.binding val binding = holder.binding
@@ -45,31 +64,79 @@ class MangaReadAdapter(
//Wrong Title //Wrong Title
binding.animeSourceSearch.setOnClickListener { binding.animeSourceSearch.setOnClickListener {
SourceSearchDialogFragment().show(fragment.requireActivity().supportFragmentManager, null) SourceSearchDialogFragment().show(
fragment.requireActivity().supportFragmentManager,
null
)
} }
val offline = if (!isOnline(binding.root.context) || currContext()?.getSharedPreferences(
"Dantotsu",
Context.MODE_PRIVATE
)
?.getBoolean("offlineMode", false) == true
) View.GONE else View.VISIBLE
binding.animeSourceNameContainer.visibility = offline
binding.animeSourceSettings.visibility = offline
binding.animeSourceSearch.visibility = offline
binding.animeSourceTitle.visibility = offline
//Source Selection //Source Selection
val source = media.selected!!.sourceIndex.let { if (it >= mangaReadSources.names.size) 0 else it } var source =
media.selected!!.sourceIndex.let { if (it >= mangaReadSources.names.size) 0 else it }
setLanguageList(media.selected!!.langIndex, source)
if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) { if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) {
binding.animeSource.setText(mangaReadSources.names[source]) binding.animeSource.setText(mangaReadSources.names[source])
mangaReadSources[source].apply { mangaReadSources[source].apply {
binding.animeSourceTitle.text = showUserText binding.animeSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } } showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
} }
} }
binding.animeSource.setAdapter(ArrayAdapter(fragment.requireContext(), R.layout.item_dropdown, mangaReadSources.names)) binding.animeSource.setAdapter(
ArrayAdapter(
fragment.requireContext(),
R.layout.item_dropdown,
mangaReadSources.names
)
)
binding.animeSourceTitle.isSelected = true binding.animeSourceTitle.isSelected = true
binding.animeSource.setOnItemClickListener { _, _, i, _ -> binding.animeSource.setOnItemClickListener { _, _, i, _ ->
fragment.onSourceChange(i).apply { fragment.onSourceChange(i).apply {
binding.animeSourceTitle.text = showUserText binding.animeSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } } showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
source = i
setLanguageList(0, i)
} }
subscribeButton(false) subscribeButton(false)
fragment.loadChapters(i) //invalidate if it's the last source
val invalidate = i == mangaReadSources.names.size - 1
fragment.loadChapters(i, invalidate)
} }
//Subscription binding.animeSourceLanguage.setOnItemClickListener { _, _, i, _ ->
// Check if 'extension' and 'selected' properties exist and are accessible
(mangaReadSources[source] as? DynamicMangaParser)?.let { ext ->
ext.sourceLanguage = i
fragment.onLangChange(i)
fragment.onSourceChange(media.selected!!.sourceIndex).apply {
binding.animeSourceTitle.text = showUserText
showUserTextListener =
{ MainScope().launch { binding.animeSourceTitle.text = it } }
setLanguageList(i, source)
}
subscribeButton(false)
fragment.loadChapters(media.selected!!.sourceIndex, true)
} ?: run {
}
}
//settings
binding.animeSourceSettings.setOnClickListener {
(mangaReadSources[source] as? DynamicMangaParser)?.let { ext ->
fragment.openSettings(ext.extension)
}
}
//Grids
subscribe = MediaDetailsActivity.PopImageButton( subscribe = MediaDetailsActivity.PopImageButton(
fragment.lifecycleScope, fragment.lifecycleScope,
binding.animeSourceSubscribe, binding.animeSourceSubscribe,
@@ -88,38 +155,156 @@ class MangaReadAdapter(
openSettings(fragment.requireContext(), getChannelId(true, media.id)) openSettings(fragment.requireContext(), getChannelId(true, media.id))
} }
//Icons binding.animeNestedButton.setOnClickListener {
binding.animeSourceGrid.visibility = View.GONE
val dialogView =
LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null)
val dialogBinding = DialogLayoutBinding.bind(dialogView)
var refresh = false
var run = false
var reversed = media.selected!!.recyclerReversed var reversed = media.selected!!.recyclerReversed
var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.mangaDefaultView var style = media.selected!!.recyclerStyle ?: fragment.uiSettings.mangaDefaultView
binding.animeSourceTop.rotation = if (reversed) -90f else 90f dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
binding.animeSourceTop.setOnClickListener { dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
dialogBinding.animeSourceTop.setOnClickListener {
reversed = !reversed reversed = !reversed
binding.animeSourceTop.rotation = if (reversed) -90f else 90f dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
fragment.onIconPressed(style, reversed) dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
run = true
} }
//Grids
dialogBinding.animeSourceGrid.visibility = View.GONE
var selected = when (style) { var selected = when (style) {
0 -> binding.animeSourceList 0 -> dialogBinding.animeSourceList
1 -> binding.animeSourceCompact 1 -> dialogBinding.animeSourceCompact
else -> binding.animeSourceList else -> dialogBinding.animeSourceList
}
when (style) {
0 -> dialogBinding.layoutText.text = "List"
1 -> dialogBinding.layoutText.text = "Compact"
else -> dialogBinding.animeSourceList
} }
selected.alpha = 1f selected.alpha = 1f
fun selected(it: ImageView) { fun selected(it: ImageButton) {
selected.alpha = 0.33f selected.alpha = 0.33f
selected = it selected = it
selected.alpha = 1f selected.alpha = 1f
} }
binding.animeSourceList.setOnClickListener { dialogBinding.animeSourceList.setOnClickListener {
selected(it as ImageView) selected(it as ImageButton)
style = 0 style = 0
fragment.onIconPressed(style, reversed) dialogBinding.layoutText.text = "List"
run = true
} }
binding.animeSourceCompact.setOnClickListener { dialogBinding.animeSourceCompact.setOnClickListener {
selected(it as ImageView) selected(it as ImageButton)
style = 1 style = 1
fragment.onIconPressed(style, reversed) dialogBinding.layoutText.text = "Compact"
run = true
}
dialogBinding.animeWebviewContainer.setOnClickListener {
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
toast("WebView not installed")
}
//start CookieCatcher activity
if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) {
val sourceAHH = mangaReadSources[source] as? DynamicMangaParser
val sourceHttp = sourceAHH?.extension?.sources?.firstOrNull() as? HttpSource
val url = sourceHttp?.baseUrl
url?.let {
refresh = true
val intent = Intent(fragment.requireContext(), CookieCatcher::class.java)
.putExtra("url", url)
ContextCompat.startActivity(fragment.requireContext(), intent, null)
}
}
} }
//Multi download
dialogBinding.downloadNo.text = "0"
dialogBinding.animeDownloadTop.setOnClickListener {
//Alert dialog asking for the number of chapters to download
val alertDialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
alertDialog.setTitle("Multi Chapter Downloader")
alertDialog.setMessage("Enter the number of chapters to download")
val input = NumberPicker(currContext())
input.minValue = 1
input.maxValue = 20
input.value = 1
alertDialog.setView(input)
alertDialog.setPositiveButton("OK") { _, _ ->
dialogBinding.downloadNo.text = "${input.value}"
}
alertDialog.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() }
val dialog = alertDialog.show()
dialog.window?.setDimAmount(0.8f)
}
//Scanlator
dialogBinding.animeScanlatorContainer.visibility =
if (options.count() > 1) View.VISIBLE else View.GONE
dialogBinding.scanlatorNo.text = "${options.count()}"
dialogBinding.animeScanlatorTop.setOnClickListener {
val dialogView2 =
LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null)
val checkboxContainer =
dialogView2.findViewById<LinearLayout>(R.id.checkboxContainer)
// Dynamically add checkboxes
options.forEach { option ->
val checkBox = CheckBox(currContext()).apply {
text = option
}
//set checked if it's already selected
if (media.selected!!.scanlators != null) {
checkBox.isChecked = media.selected!!.scanlators?.contains(option) != true
scanlatorSelectionListener?.onScanlatorsSelected()
} else {
checkBox.isChecked = true
}
checkboxContainer.addView(checkBox)
}
// Create AlertDialog
val dialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
.setView(dialogView2)
.setPositiveButton("OK") { _, _ ->
//add unchecked to hidden
hiddenScanlators.clear()
for (i in 0 until checkboxContainer.childCount) {
val checkBox = checkboxContainer.getChildAt(i) as CheckBox
if (!checkBox.isChecked) {
hiddenScanlators.add(checkBox.text.toString())
}
}
fragment.onScanlatorChange(hiddenScanlators)
scanlatorSelectionListener?.onScanlatorsSelected()
}
.setNegativeButton("Cancel", null)
.show()
dialog.window?.setDimAmount(0.8f)
}
nestedDialog = AlertDialog.Builder(fragment.requireContext(), R.style.MyPopup)
.setTitle("Options")
.setView(dialogView)
.setPositiveButton("OK") { _, _ ->
if (run) fragment.onIconPressed(style, reversed)
if (dialogBinding.downloadNo.text != "0") {
fragment.multiDownload(dialogBinding.downloadNo.text.toString().toInt())
}
if (refresh) fragment.loadChapters(source, true)
}
.setNegativeButton("Cancel") { _, _ ->
if (refresh) fragment.loadChapters(source, true)
}
.setOnCancelListener {
if (refresh) fragment.loadChapters(source, true)
}
.create()
nestedDialog?.show()
}
//Chapter Handling //Chapter Handling
handleChapters() handleChapters()
} }
@@ -138,13 +323,40 @@ class MangaReadAdapter(
for (position in arr.indices) { for (position in arr.indices) {
val last = if (position + 1 == arr.size) names.size else (limit * (position + 1)) val last = if (position + 1 == arr.size) names.size else (limit * (position + 1))
val chip = val chip =
ItemChipBinding.inflate(LayoutInflater.from(fragment.context), binding.animeSourceChipGroup, false).root ItemChipBinding.inflate(
LayoutInflater.from(fragment.context),
binding.animeSourceChipGroup,
false
).root
chip.isCheckable = true chip.isCheckable = true
fun selected() { fun selected() {
chip.isChecked = true chip.isChecked = true
binding.animeWatchChipScroll.smoothScrollTo((chip.left - screenWidth / 2) + (chip.width / 2), 0) binding.animeWatchChipScroll.smoothScrollTo(
(chip.left - screenWidth / 2) + (chip.width / 2),
0
)
} }
chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
val startChapter = MangaNameAdapter.findChapterNumber(names[limit * (position)])
val endChapter = MangaNameAdapter.findChapterNumber(names[last - 1])
val startChapterString = if (startChapter != null) {
"Ch.$startChapter"
} else {
names[limit * (position)]
}
val endChapterString = if (endChapter != null) {
"Ch.$endChapter"
} else {
names[last - 1]
}
//chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
chip.text = "$startChapterString - $endChapterString"
chip.setTextColor(
ContextCompat.getColorStateList(
fragment.requireContext(),
R.color.chip_text_color
)
)
chip.setOnClickListener { chip.setOnClickListener {
selected() selected()
@@ -157,7 +369,14 @@ class MangaReadAdapter(
} }
} }
if (select != null) if (select != null)
binding.animeWatchChipScroll.apply { post { scrollTo((select.left - screenWidth / 2) + (select.width / 2), 0) } } binding.animeWatchChipScroll.apply {
post {
scrollTo(
(select.left - screenWidth / 2) + (select.width / 2),
0
)
}
}
} }
} }
@@ -167,6 +386,7 @@ class MangaReadAdapter(
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun handleChapters() { fun handleChapters() {
val binding = _binding val binding = _binding
if (binding != null) { if (binding != null) {
if (media.manga?.chapters != null) { if (media.manga?.chapters != null) {
@@ -174,7 +394,15 @@ class MangaReadAdapter(
val anilistEp = (media.userProgress ?: 0).plus(1) val anilistEp = (media.userProgress ?: 0).plus(1)
val appEp = loadData<String>("${media.id}_current_chp")?.toIntOrNull() ?: 1 val appEp = loadData<String>("${media.id}_current_chp")?.toIntOrNull() ?: 1
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString() var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
if (chapters.contains(continueEp)) { val filteredChapters = chapters.filter { chapterKey ->
val chapter = media.manga.chapters!![chapterKey]!!
chapter.scanlator !in hiddenScanlators
}
val formattedChapters = filteredChapters.map {
MangaNameAdapter.findChapterNumber(it)?.toInt()?.toString()
}
if (formattedChapters.contains(continueEp)) {
continueEp = chapters[formattedChapters.indexOf(continueEp)]
binding.animeSourceContinue.visibility = View.VISIBLE binding.animeSourceContinue.visibility = View.VISIBLE
handleProgress( handleProgress(
binding.itemEpisodeProgressCont, binding.itemEpisodeProgressCont,
@@ -220,7 +448,42 @@ class MangaReadAdapter(
} }
} }
private fun setLanguageList(lang: Int, source: Int) {
val binding = _binding
if (mangaReadSources is MangaSources) {
val parser = mangaReadSources[source] as? DynamicMangaParser
if (parser != null) {
(mangaReadSources[source] as? DynamicMangaParser)?.let { ext ->
ext.sourceLanguage = lang
}
try {
binding?.animeSourceLanguage?.setText(parser.extension.sources[lang].lang)
} catch (e: IndexOutOfBoundsException) {
binding?.animeSourceLanguage?.setText(
parser.extension.sources.firstOrNull()?.lang ?: "Unknown"
)
}
val adapter = ArrayAdapter(
fragment.requireContext(),
R.layout.item_dropdown,
parser.extension.sources.map { LanguageMapper.mapLanguageCodeToName(it.lang) }
)
val items = adapter.count
binding?.animeSourceLanguageContainer?.visibility =
if (items > 1) View.VISIBLE else View.GONE
binding?.animeSourceLanguage?.setAdapter(adapter)
}
}
}
override fun getItemCount(): Int = 1 override fun getItemCount(): Int = 1
inner class ViewHolder(val binding: ItemAnimeWatchBinding) : RecyclerView.ViewHolder(binding.root) inner class ViewHolder(val binding: ItemAnimeWatchBinding) :
RecyclerView.ViewHolder(binding.root)
}
interface ScanlatorSelectionListener {
fun onScanlatorsSelected()
} }

View File

@@ -1,11 +1,24 @@
package ani.dantotsu.media.manga package ani.dantotsu.media.manga
import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.Toast
import androidx.cardview.widget.CardView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils.clamp import androidx.core.math.MathUtils.clamp
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@@ -13,27 +26,44 @@ import androidx.fragment.app.activityViewModels
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.viewpager2.widget.ViewPager2
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.databinding.FragmentAnimeWatchBinding import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.manga.MangaDownloaderService
import ani.dantotsu.download.manga.MangaServiceDataSingleton
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.manga.mangareader.ChapterLoaderDialog
import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.parsers.DynamicMangaParser
import ani.dantotsu.parsers.HMangaSources import ani.dantotsu.parsers.HMangaSources
import ani.dantotsu.parsers.MangaParser import ani.dantotsu.parsers.MangaParser
import ani.dantotsu.parsers.MangaSources import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment
import ani.dantotsu.subcriptions.Notifications import ani.dantotsu.subcriptions.Notifications
import ani.dantotsu.subcriptions.Notifications.Group.MANGA_GROUP import ani.dantotsu.subcriptions.Notifications.Group.MANGA_GROUP
import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId import ani.dantotsu.subcriptions.Subscription.Companion.getChannelId
import ani.dantotsu.subcriptions.SubscriptionHelper import ani.dantotsu.subcriptions.SubscriptionHelper
import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription import ani.dantotsu.subcriptions.SubscriptionHelper.Companion.saveSubscription
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.navigationrail.NavigationRailView
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.source.ConfigurableSource
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.max import kotlin.math.max
import kotlin.math.roundToInt import kotlin.math.roundToInt
open class MangaReadFragment : Fragment() { open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
private var _binding: FragmentAnimeWatchBinding? = null private var _binding: FragmentAnimeWatchBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private val model: MediaDetailsViewModel by activityViewModels() private val model: MediaDetailsViewModel by activityViewModels()
@@ -48,13 +78,16 @@ open class MangaReadFragment : Fragment() {
private lateinit var headerAdapter: MangaReadAdapter private lateinit var headerAdapter: MangaReadAdapter
private lateinit var chapterAdapter: MangaChapterAdapter private lateinit var chapterAdapter: MangaChapterAdapter
val downloadManager = Injekt.get<DownloadsManager>()
var screenWidth = 0f var screenWidth = 0f
private var progress = View.VISIBLE private var progress = View.VISIBLE
var continueEp: Boolean = false var continueEp: Boolean = false
var loaded = false var loaded = false
val uiSettings = loadData("ui_settings", toast = false) ?: UserInterfaceSettings().apply { saveData("ui_settings", this) } val uiSettings = loadData("ui_settings", toast = false)
?: UserInterfaceSettings().apply { saveData("ui_settings", this) }
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@@ -67,10 +100,23 @@ open class MangaReadFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val intentFilter = IntentFilter().apply {
addAction(ACTION_DOWNLOAD_STARTED)
addAction(ACTION_DOWNLOAD_FINISHED)
addAction(ACTION_DOWNLOAD_FAILED)
addAction(ACTION_DOWNLOAD_PROGRESS)
}
ContextCompat.registerReceiver(
requireContext(),
downloadStatusReceiver,
intentFilter,
ContextCompat.RECEIVER_EXPORTED
)
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight) binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight)
screenWidth = resources.displayMetrics.widthPixels.dp screenWidth = resources.displayMetrics.widthPixels.dp
var maxGridSize = (screenWidth / 100f).roundToInt() var maxGridSize = (screenWidth / 100f).roundToInt()
maxGridSize = max(4, maxGridSize - (maxGridSize % 2)) maxGridSize = max(4, maxGridSize - (maxGridSize % 2))
@@ -107,7 +153,8 @@ open class MangaReadFragment : Fragment() {
if (media.format == "MANGA" || media.format == "ONE SHOT") { if (media.format == "MANGA" || media.format == "ONE SHOT") {
media.selected = model.loadSelected(media) media.selected = model.loadSelected(media)
subscribed = SubscriptionHelper.getSubscriptions(requireContext()).containsKey(media.id) subscribed =
SubscriptionHelper.getSubscriptions(requireContext()).containsKey(media.id)
style = media.selected!!.recyclerStyle style = media.selected!!.recyclerStyle
reverse = media.selected!!.recyclerReversed reverse = media.selected!!.recyclerReversed
@@ -116,9 +163,16 @@ open class MangaReadFragment : Fragment() {
model.mangaReadSources = if (media.isAdult) HMangaSources else MangaSources model.mangaReadSources = if (media.isAdult) HMangaSources else MangaSources
headerAdapter = MangaReadAdapter(it, this, model.mangaReadSources!!) headerAdapter = MangaReadAdapter(it, this, model.mangaReadSources!!)
chapterAdapter = MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this) headerAdapter.scanlatorSelectionListener = this
chapterAdapter =
MangaChapterAdapter(style ?: uiSettings.mangaDefaultView, media, this)
binding.animeSourceRecycler.adapter = ConcatAdapter(headerAdapter, chapterAdapter) for (download in downloadManager.mangaDownloadedTypes) {
chapterAdapter.stopDownload(download.chapter)
}
binding.animeSourceRecycler.adapter =
ConcatAdapter(headerAdapter, chapterAdapter)
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
model.loadMangaChapters(media, media.selected!!.sourceIndex) model.loadMangaChapters(media, media.selected!!.sourceIndex)
@@ -129,19 +183,59 @@ open class MangaReadFragment : Fragment() {
} }
} else { } else {
binding.animeNotSupported.visibility = View.VISIBLE binding.animeNotSupported.visibility = View.VISIBLE
binding.animeNotSupported.text = getString(R.string.not_supported, media.format ?: "") binding.animeNotSupported.text =
getString(R.string.not_supported, media.format ?: "")
} }
} }
} }
model.getMangaChapters().observe(viewLifecycleOwner) { loadedChapters -> model.getMangaChapters().observe(viewLifecycleOwner) { _ ->
updateChapters()
}
}
override fun onScanlatorsSelected() {
updateChapters()
}
fun multiDownload(n: Int) {
//get last viewed chapter
val selected = media.userProgress
val chapters = media.manga?.chapters?.values?.toList()
//filter by selected language
val progressChapterIndex = (chapters?.indexOfFirst {
MangaNameAdapter.findChapterNumber(it.number)?.toInt() == selected
} ?: 0) + 1
if (progressChapterIndex < 0 || n < 1 || chapters == null) return
// Calculate the end index
val endIndex = minOf(progressChapterIndex + n, chapters.size)
//make sure there are enough chapters
val chaptersToDownload = chapters.subList(progressChapterIndex, endIndex)
for (chapter in chaptersToDownload) {
onMangaChapterDownloadClick(chapter.title!!)
}
}
private fun updateChapters() {
val loadedChapters = model.getMangaChapters().value
if (loadedChapters != null) { if (loadedChapters != null) {
val chapters = loadedChapters[media.selected!!.sourceIndex] val chapters = loadedChapters[media.selected!!.sourceIndex]
if (chapters != null) { if (chapters != null) {
media.manga?.chapters = chapters headerAdapter.options = getScanlators(chapters)
val filteredChapters = chapters.filterNot { (_, chapter) ->
chapter.scanlator in headerAdapter.hiddenScanlators
}
media.manga?.chapters = filteredChapters.toMutableMap()
//CHIP GROUP //CHIP GROUP
val total = chapters.size val total = filteredChapters.size
val divisions = total.toDouble() / 10 val divisions = total.toDouble() / 10
start = 0 start = 0
end = null end = null
@@ -152,7 +246,7 @@ open class MangaReadFragment : Fragment() {
} }
headerAdapter.clearChips() headerAdapter.clearChips()
if (total > limit) { if (total > limit) {
val arr = chapters.keys.toTypedArray() val arr = filteredChapters.keys.toTypedArray()
val stored = ceil((total).toDouble() / limit).toInt() val stored = ceil((total).toDouble() / limit).toInt()
val position = clamp(media.selected!!.chip, 0, stored - 1) val position = clamp(media.selected!!.chip, 0, stored - 1)
val last = if (position + 1 == stored) total else (limit * (position + 1)) val last = if (position + 1 == stored) total else (limit * (position + 1))
@@ -171,6 +265,16 @@ open class MangaReadFragment : Fragment() {
} }
} }
} }
private fun getScanlators(chap: MutableMap<String, MangaChapter>?): List<String> {
val scanlators = mutableListOf<String>()
if (chap != null) {
val chapters = chap.values
for (chapter in chapters) {
scanlators.add(chapter.scanlator ?: "Unknown")
}
}
return scanlators.distinct()
} }
fun onSourceChange(i: Int): MangaParser { fun onSourceChange(i: Int): MangaParser {
@@ -185,8 +289,22 @@ open class MangaReadFragment : Fragment() {
return model.mangaReadSources?.get(i)!! return model.mangaReadSources?.get(i)!!
} }
fun loadChapters(i: Int) { fun onLangChange(i: Int) {
lifecycleScope.launch(Dispatchers.IO) { model.loadMangaChapters(media, i) } val selected = model.loadSelected(media)
selected.langIndex = i
model.saveSelected(media.id, selected, requireActivity())
media.selected = selected
}
fun onScanlatorChange(list: List<String>) {
val selected = model.loadSelected(media)
selected.scanlators = list
model.saveSelected(media.id, selected, requireActivity())
media.selected = selected
}
fun loadChapters(i: Int, invalidate: Boolean) {
lifecycleScope.launch(Dispatchers.IO) { model.loadMangaChapters(media, i, invalidate) }
} }
fun onIconPressed(viewType: Int, rev: Boolean) { fun onIconPressed(viewType: Int, rev: Boolean) {
@@ -225,22 +343,220 @@ open class MangaReadFragment : Fragment() {
) )
} }
fun openSettings(pkg: MangaExtension.Installed) {
val changeUIVisibility: (Boolean) -> Unit = { show ->
val activity = activity
if (activity is MediaDetailsActivity && isAdded) {
val visibility = if (show) View.VISIBLE else View.GONE
activity.findViewById<AppBarLayout>(R.id.mediaAppBar).visibility = visibility
activity.findViewById<ViewPager2>(R.id.mediaViewPager).visibility = visibility
activity.findViewById<CardView>(R.id.mediaCover).visibility = visibility
activity.findViewById<CardView>(R.id.mediaClose).visibility = visibility
try {
activity.findViewById<CustomBottomNavBar>(R.id.mediaTab).visibility = visibility
} catch (e: ClassCastException) {
activity.findViewById<NavigationRailView>(R.id.mediaTab).visibility = visibility
}
activity.findViewById<FrameLayout>(R.id.fragmentExtensionsContainer).visibility =
if (show) View.GONE else View.VISIBLE
}
}
var itemSelected = false
val allSettings = pkg.sources.filterIsInstance<ConfigurableSource>()
if (allSettings.isNotEmpty()) {
var selectedSetting = allSettings[0]
if (allSettings.size > 1) {
val names =
allSettings.map { LanguageMapper.mapLanguageCodeToName(it.lang) }.toTypedArray()
var selectedIndex = 0
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
.setTitle("Select a Source")
.setSingleChoiceItems(names, selectedIndex) { dialog, which ->
selectedIndex = which
selectedSetting = allSettings[selectedIndex]
itemSelected = true
dialog.dismiss()
// Move the fragment transaction here
val fragment =
MangaSourcePreferencesFragment().getInstance(selectedSetting.id) {
changeUIVisibility(true)
loadChapters(media.selected!!.sourceIndex, true)
}
parentFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
.replace(R.id.fragmentExtensionsContainer, fragment)
.addToBackStack(null)
.commit()
}
.setOnDismissListener {
if (!itemSelected) {
changeUIVisibility(true)
}
}
.show()
dialog.window?.setDimAmount(0.8f)
} else {
// If there's only one setting, proceed with the fragment transaction
val fragment = MangaSourcePreferencesFragment().getInstance(selectedSetting.id) {
changeUIVisibility(true)
loadChapters(media.selected!!.sourceIndex, true)
}
parentFragmentManager.beginTransaction()
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
.replace(R.id.fragmentExtensionsContainer, fragment)
.addToBackStack(null)
.commit()
}
changeUIVisibility(false)
} else {
Toast.makeText(requireContext(), "Source is not configurable", Toast.LENGTH_SHORT)
.show()
}
}
fun onMangaChapterClick(i: String) { fun onMangaChapterClick(i: String) {
model.continueMedia = false model.continueMedia = false
media.manga?.chapters?.get(i)?.let { media.manga?.chapters?.get(i)?.let {
media.manga?.selectedChapter = i media.manga?.selectedChapter = i
model.saveSelected(media.id, media.selected!!, requireActivity()) model.saveSelected(media.id, media.selected!!, requireActivity())
ChapterLoaderDialog.newInstance(it, true).show(requireActivity().supportFragmentManager, "dialog") ChapterLoaderDialog.newInstance(it, true)
.show(requireActivity().supportFragmentManager, "dialog")
} }
} }
fun onMangaChapterDownloadClick(i: String) {
if (!isNotificationPermissionGranted()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ActivityCompat.requestPermissions(
requireActivity(),
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
1
)
}
}
model.continueMedia = false
media.manga?.chapters?.get(i)?.let { chapter ->
val parser =
model.mangaReadSources?.get(media.selected!!.sourceIndex) as? DynamicMangaParser
parser?.let {
CoroutineScope(Dispatchers.IO).launch {
val images = parser.imageList("", chapter.sChapter)
// Create a download task
val downloadTask = MangaDownloaderService.DownloadTask(
title = media.mainName(),
chapter = chapter.title!!,
imageData = images,
sourceMedia = media,
retries = 2,
simultaneousDownloads = 2
)
MangaServiceDataSingleton.downloadQueue.offer(downloadTask)
// If the service is not already running, start it
if (!MangaServiceDataSingleton.isServiceRunning) {
val intent = Intent(context, MangaDownloaderService::class.java)
withContext(Dispatchers.Main) {
ContextCompat.startForegroundService(requireContext(), intent)
}
MangaServiceDataSingleton.isServiceRunning = true
}
// Inform the adapter that the download has started
withContext(Dispatchers.Main) {
chapterAdapter.startDownload(i)
}
}
}
}
}
private fun isNotificationPermissionGranted(): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
return ActivityCompat.checkSelfPermission(
requireContext(),
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
}
return true
}
fun onMangaChapterRemoveDownloadClick(i: String) {
downloadManager.removeDownload(
DownloadedType(
media.mainName(),
i,
DownloadedType.Type.MANGA
)
)
chapterAdapter.deleteDownload(i)
}
fun onMangaChapterStopDownloadClick(i: String) {
val cancelIntent = Intent().apply {
action = MangaDownloaderService.ACTION_CANCEL_DOWNLOAD
putExtra(MangaDownloaderService.EXTRA_CHAPTER, i)
}
requireContext().sendBroadcast(cancelIntent)
// Remove the download from the manager and update the UI
downloadManager.removeDownload(
DownloadedType(
media.mainName(),
i,
DownloadedType.Type.MANGA
)
)
chapterAdapter.purgeDownload(i)
}
private val downloadStatusReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (!this@MangaReadFragment::chapterAdapter.isInitialized) return
when (intent.action) {
ACTION_DOWNLOAD_STARTED -> {
val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER)
chapterNumber?.let { chapterAdapter.startDownload(it) }
}
ACTION_DOWNLOAD_FINISHED -> {
val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER)
chapterNumber?.let { chapterAdapter.stopDownload(it) }
}
ACTION_DOWNLOAD_FAILED -> {
val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER)
chapterNumber?.let {
chapterAdapter.purgeDownload(it)
}
}
ACTION_DOWNLOAD_PROGRESS -> {
val chapterNumber = intent.getStringExtra(EXTRA_CHAPTER_NUMBER)
val progress = intent.getIntExtra("progress", 0)
chapterNumber?.let {
chapterAdapter.updateDownloadProgress(it, progress)
}
}
}
}
}
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
private fun reload() { private fun reload() {
val selected = model.loadSelected(media) val selected = model.loadSelected(media)
//Find latest chapter for subscription //Find latest chapter for subscription
selected.latest = media.manga?.chapters?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f selected.latest =
selected.latest = media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest media.manga?.chapters?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f
selected.latest =
media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest
model.saveSelected(media.id, selected, requireActivity()) model.saveSelected(media.id, selected, requireActivity())
headerAdapter.handleChapters() headerAdapter.handleChapters()
@@ -249,7 +565,8 @@ open class MangaReadFragment : Fragment() {
if (media.manga!!.chapters != null) { if (media.manga!!.chapters != null) {
val end = if (end != null && end!! < media.manga!!.chapters!!.size) end else null val end = if (end != null && end!! < media.manga!!.chapters!!.size) end else null
arr.addAll( arr.addAll(
media.manga!!.chapters!!.values.toList().slice(start..(end ?: (media.manga!!.chapters!!.size - 1))) media.manga!!.chapters!!.values.toList()
.slice(start..(end ?: (media.manga!!.chapters!!.size - 1)))
) )
if (reverse) if (reverse)
arr = (arr.reversed() as? ArrayList<MangaChapter>) ?: arr arr = (arr.reversed() as? ArrayList<MangaChapter>) ?: arr
@@ -262,6 +579,7 @@ open class MangaReadFragment : Fragment() {
override fun onDestroy() { override fun onDestroy() {
model.mangaReadSources?.flushText() model.mangaReadSources?.flushText()
super.onDestroy() super.onDestroy()
requireContext().unregisterReceiver(downloadStatusReceiver)
} }
private var state: Parcelable? = null private var state: Parcelable? = null
@@ -275,4 +593,12 @@ open class MangaReadFragment : Fragment() {
super.onPause() super.onPause()
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState() state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState()
} }
companion object {
const val ACTION_DOWNLOAD_STARTED = "ani.dantotsu.ACTION_DOWNLOAD_STARTED"
const val ACTION_DOWNLOAD_FINISHED = "ani.dantotsu.ACTION_DOWNLOAD_FINISHED"
const val ACTION_DOWNLOAD_FAILED = "ani.dantotsu.ACTION_DOWNLOAD_FAILED"
const val ACTION_DOWNLOAD_PROGRESS = "ani.dantotsu.ACTION_DOWNLOAD_PROGRESS"
const val EXTRA_CHAPTER_NUMBER = "extra_chapter_number"
}
} }

View File

@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
import android.net.Uri
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
@@ -13,8 +14,8 @@ import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.media.manga.MangaCache
import ani.dantotsu.media.manga.MangaChapter import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.parsers.DynamicMangaParser
import ani.dantotsu.settings.CurrentReaderSettings import ani.dantotsu.settings.CurrentReaderSettings
import com.alexvasilkov.gestures.views.GestureFrameLayout import com.alexvasilkov.gestures.views.GestureFrameLayout
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
@@ -22,13 +23,11 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import ani.dantotsu.media.manga.MangaCache
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.File
abstract class BaseImageAdapter( abstract class BaseImageAdapter(
val activity: MangaReaderActivity, val activity: MangaReaderActivity,
@@ -90,7 +89,8 @@ abstract class BaseImageAdapter(
} }
} else { } else {
val detector = GestureDetectorCompat(view.context, object : GesturesListener() { val detector = GestureDetectorCompat(view.context, object : GesturesListener() {
override fun onSingleClick(event: MotionEvent) = activity.handleController() override fun onSingleClick(event: MotionEvent) =
activity.handleController(event = event)
}) })
view.findViewById<View>(R.id.imgProgCover).apply { view.findViewById<View>(R.id.imgProgCover).apply {
setOnTouchListener { _, event -> setOnTouchListener { _, event ->
@@ -113,13 +113,19 @@ abstract class BaseImageAdapter(
activity.lifecycleScope.launch { loadImage(holder.bindingAdapterPosition, view) } activity.lifecycleScope.launch { loadImage(holder.bindingAdapterPosition, view) }
} }
abstract fun isZoomed(): Boolean
abstract fun setZoom(zoom: Float)
abstract suspend fun loadImage(position: Int, parent: View): Boolean abstract suspend fun loadImage(position: Int, parent: View): Boolean
companion object { companion object {
/*suspend fun Context.loadBitmap(link: FileUrl, transforms: List<BitmapTransformation>): Bitmap? { suspend fun Context.loadBitmap_old(
link: FileUrl,
transforms: List<BitmapTransformation>
): Bitmap? { //still used in some places
return tryWithSuspend { return tryWithSuspend {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
Glide.with(this@loadBitmap) Glide.with(this@loadBitmap_old)
.asBitmap() .asBitmap()
.let { .let {
if (link.url.startsWith("file://")) { if (link.url.startsWith("file://")) {
@@ -133,8 +139,7 @@ abstract class BaseImageAdapter(
.let { .let {
if (transforms.isNotEmpty()) { if (transforms.isNotEmpty()) {
it.transform(*transforms.toTypedArray()) it.transform(*transforms.toTypedArray())
} } else {
else {
it it
} }
} }
@@ -142,26 +147,31 @@ abstract class BaseImageAdapter(
.get() .get()
} }
} }
}*/ }
suspend fun Context.loadBitmap(link: FileUrl, transforms: List<BitmapTransformation>): Bitmap? { suspend fun Context.loadBitmap(
link: FileUrl,
transforms: List<BitmapTransformation>
): Bitmap? {
return tryWithSuspend { return tryWithSuspend {
val mangaCache = uy.kohesive.injekt.Injekt.get<MangaCache>() val mangaCache = uy.kohesive.injekt.Injekt.get<MangaCache>()
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
Glide.with(this@loadBitmap) Glide.with(this@loadBitmap)
.asBitmap() .asBitmap()
.let { .let {
if (link.url.startsWith("file://")) { val fileUri = Uri.fromFile(File(link.url)).toString()
it.load(link.url) val localFile = File(link.url)
if (localFile.exists()) {
it.load(localFile.absoluteFile)
.skipMemoryCache(true) .skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
} else { } else {
println("bitmap from cache")
println(link.url)
println(mangaCache.get(link.url))
println("cache size: ${mangaCache.size()}")
mangaCache.get(link.url)?.let { imageData -> mangaCache.get(link.url)?.let { imageData ->
val bitmap = imageData.fetchAndProcessImage(imageData.page, imageData.source, context = this@loadBitmap) val bitmap = imageData.fetchAndProcessImage(
imageData.page,
imageData.source,
context = this@loadBitmap
)
it.load(bitmap) it.load(bitmap)
.skipMemoryCache(true) .skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)

View File

@@ -1,7 +1,9 @@
package ani.dantotsu.media.manga.mangareader package ani.dantotsu.media.manga.mangareader
import android.content.Intent import android.content.Intent
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
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
@@ -12,9 +14,9 @@ import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.currActivity import ani.dantotsu.currActivity
import ani.dantotsu.databinding.BottomSheetSelectorBinding import ani.dantotsu.databinding.BottomSheetSelectorBinding
import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.media.MediaDetailsViewModel
import ani.dantotsu.media.MediaSingleton import ani.dantotsu.media.MediaSingleton
import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.others.getSerialized import ani.dantotsu.others.getSerialized
import ani.dantotsu.tryWith import ani.dantotsu.tryWith
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -45,13 +47,21 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() {
loaded = true loaded = true
binding.selectorAutoText.text = chp.title binding.selectorAutoText.text = chp.title
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
if(model.loadMangaChapterImages(chp, m.selected!!)) { if (model.loadMangaChapterImages(
chp,
m.selected!!,
m.mainName()
)
) {
val activity = currActivity() val activity = currActivity()
activity?.runOnUiThread { activity?.runOnUiThread {
tryWith { dismiss() } tryWith { dismiss() }
if (launch) { if (launch) {
MediaSingleton.media = m MediaSingleton.media = m
val intent = Intent(activity, MangaReaderActivity::class.java)//.apply { putExtra("media", m) } val intent = Intent(
activity,
MangaReaderActivity::class.java
)//.apply { putExtra("media", m) }
activity.startActivity(intent) activity.startActivity(intent)
} }
} }
@@ -61,8 +71,18 @@ class ChapterLoaderDialog : BottomSheetDialogFragment() {
} }
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = BottomSheetSelectorBinding.inflate(inflater, container, false) _binding = BottomSheetSelectorBinding.inflate(inflater, container, false)
val window = dialog?.window
window?.statusBarColor = Color.TRANSPARENT
val typedValue = TypedValue()
val theme = requireContext().theme
theme.resolveAttribute(com.google.android.material.R.attr.colorSurface, typedValue, true)
window?.navigationBarColor = typedValue.data
return binding.root return binding.root
} }

View File

@@ -47,7 +47,8 @@ open class ImageAdapter(
} }
override suspend fun loadImage(position: Int, parent: View): Boolean { override suspend fun loadImage(position: Int, parent: View): Boolean {
val imageView = parent.findViewById<SubsamplingScaleImageView>(R.id.imgProgImageNoGestures) ?: return false val imageView = parent.findViewById<SubsamplingScaleImageView>(R.id.imgProgImageNoGestures)
?: return false
val progress = parent.findViewById<View>(R.id.imgProgProgress) ?: return false val progress = parent.findViewById<View>(R.id.imgProgProgress) ?: return false
imageView.recycle() imageView.recycle()
imageView.visibility = View.GONE imageView.visibility = View.GONE
@@ -60,10 +61,12 @@ open class ImageAdapter(
if (settings.layout != PAGED) if (settings.layout != PAGED)
parent.updateLayoutParams { parent.updateLayoutParams {
if (settings.direction != LEFT_TO_RIGHT && settings.direction != RIGHT_TO_LEFT) { if (settings.direction != LEFT_TO_RIGHT && settings.direction != RIGHT_TO_LEFT) {
sHeight = if (settings.wrapImages) bitmap.height else (sWidth * bitmap.height * 1f / bitmap.width).toInt() sHeight =
if (settings.wrapImages) bitmap.height else (sWidth * bitmap.height * 1f / bitmap.width).toInt()
height = sHeight height = sHeight
} else { } else {
sWidth = if (settings.wrapImages) bitmap.width else (sHeight * bitmap.width * 1f / bitmap.height).toInt() sWidth =
if (settings.wrapImages) bitmap.width else (sHeight * bitmap.width * 1f / bitmap.height).toInt()
width = sWidth width = sWidth
} }
} }
@@ -73,7 +76,8 @@ open class ImageAdapter(
val parentArea = sWidth * sHeight * 1f val parentArea = sWidth * sHeight * 1f
val bitmapArea = bitmap.width * bitmap.height * 1f val bitmapArea = bitmap.width * bitmap.height * 1f
val scale = if (parentArea < bitmapArea) (bitmapArea / parentArea) else (parentArea / bitmapArea) val scale =
if (parentArea < bitmapArea) (bitmapArea / parentArea) else (parentArea / bitmapArea)
imageView.maxScale = scale * 1.1f imageView.maxScale = scale * 1.1f
imageView.minScale = scale imageView.minScale = scale
@@ -87,4 +91,16 @@ open class ImageAdapter(
} }
override fun getItemCount(): Int = images.size override fun getItemCount(): Int = images.size
override fun isZoomed(): Boolean {
val imageView =
activity.findViewById<SubsamplingScaleImageView>(R.id.imgProgImageNoGestures)
return imageView.scale > imageView.minScale
}
override fun setZoom(zoom: Float) {
val imageView =
activity.findViewById<SubsamplingScaleImageView>(R.id.imgProgImageNoGestures)
imageView.setScaleAndCenter(zoom, imageView.center)
}
} }

View File

@@ -3,7 +3,10 @@ package ani.dantotsu.media.manga.mangareader
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@@ -11,6 +14,7 @@ import android.view.*
import android.view.KeyEvent.* import android.view.KeyEvent.*
import android.view.animation.OvershootInterpolator import android.view.animation.OvershootInterpolator
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.CheckBox
import androidx.activity.addCallback import androidx.activity.addCallback
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@@ -25,6 +29,8 @@ import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.* import ani.dantotsu.*
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.discord.Discord import ani.dantotsu.connections.discord.Discord
import ani.dantotsu.connections.discord.DiscordService
import ani.dantotsu.connections.discord.DiscordServiceRunningSingleton
import ani.dantotsu.connections.discord.RPC import ani.dantotsu.connections.discord.RPC
import ani.dantotsu.connections.updateProgress import ani.dantotsu.connections.updateProgress
import ani.dantotsu.databinding.ActivityMangaReaderBinding import ani.dantotsu.databinding.ActivityMangaReaderBinding
@@ -35,7 +41,7 @@ import ani.dantotsu.media.manga.MangaCache
import ani.dantotsu.media.manga.MangaChapter import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.media.manga.MangaNameAdapter import ani.dantotsu.media.manga.MangaNameAdapter
import ani.dantotsu.others.ImageViewDialog import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.others.getSerialized import ani.dantotsu.others.LangSet
import ani.dantotsu.parsers.HMangaSources import ani.dantotsu.parsers.HMangaSources
import ani.dantotsu.parsers.MangaImage import ani.dantotsu.parsers.MangaImage
import ani.dantotsu.parsers.MangaSources import ani.dantotsu.parsers.MangaSources
@@ -45,6 +51,7 @@ import ani.dantotsu.settings.CurrentReaderSettings.DualPageModes.*
import ani.dantotsu.settings.CurrentReaderSettings.Layouts.* import ani.dantotsu.settings.CurrentReaderSettings.Layouts.*
import ani.dantotsu.settings.ReaderSettings import ani.dantotsu.settings.ReaderSettings
import ani.dantotsu.settings.UserInterfaceSettings import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.themes.ThemeManager
import com.alexvasilkov.gestures.views.GestureFrameLayout import com.alexvasilkov.gestures.views.GestureFrameLayout
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
@@ -52,8 +59,6 @@ import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase import com.google.firebase.ktx.Firebase
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@@ -78,7 +83,8 @@ class MangaReaderActivity : AppCompatActivity() {
private var isContVisible = false private var isContVisible = false
private var showProgressDialog = true private var showProgressDialog = true
private var progressDialog: AlertDialog.Builder? = null
//private var progressDialog: AlertDialog.Builder? = null
private var maxChapterPage = 0L private var maxChapterPage = 0L
private var currentChapterPage = 0L private var currentChapterPage = 0L
@@ -99,7 +105,10 @@ class MangaReaderActivity : AppCompatActivity() {
val displayCutout = window.decorView.rootWindowInsets.displayCutout val displayCutout = window.decorView.rootWindowInsets.displayCutout
if (displayCutout != null) { if (displayCutout != null) {
if (displayCutout.boundingRects.size > 0) { if (displayCutout.boundingRects.size > 0) {
notchHeight = min(displayCutout.boundingRects[0].width(), displayCutout.boundingRects[0].height()) notchHeight = min(
displayCutout.boundingRects[0].width(),
displayCutout.boundingRects[0].height()
)
checkNotch() checkNotch()
} }
} }
@@ -119,12 +128,18 @@ class MangaReaderActivity : AppCompatActivity() {
override fun onDestroy() { override fun onDestroy() {
mangaCache.clear() mangaCache.clear()
rpc?.close() if (DiscordServiceRunningSingleton.running) {
DiscordServiceRunningSingleton.running = false
val stopIntent = Intent(this, DiscordService::class.java)
stopService(stopIntent)
}
super.onDestroy() super.onDestroy()
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityMangaReaderBinding.inflate(layoutInflater) binding = ActivityMangaReaderBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
@@ -136,8 +151,14 @@ class MangaReaderActivity : AppCompatActivity() {
progress { finish() } progress { finish() }
} }
settings = loadData("reader_settings", this) ?: ReaderSettings().apply { saveData("reader_settings", this) } settings = loadData("reader_settings", this)
uiSettings = loadData("ui_settings", this) ?: UserInterfaceSettings().apply { saveData("ui_settings", this) } ?: ReaderSettings().apply { saveData("reader_settings", this) }
uiSettings = loadData("ui_settings", this) ?: UserInterfaceSettings().apply {
saveData(
"ui_settings",
this
)
}
controllerDuration = (uiSettings.animationSpeed * 200).toLong() controllerDuration = (uiSettings.animationSpeed * 200).toLong()
hideBars() hideBars()
@@ -162,9 +183,11 @@ class MangaReaderActivity : AppCompatActivity() {
if (fromUser) { if (fromUser) {
sliding = true sliding = true
if (settings.default.layout != PAGED) if (settings.default.layout != PAGED)
binding.mangaReaderRecycler.scrollToPosition((value.toInt() - 1) / (dualPage { 2 } ?: 1)) binding.mangaReaderRecycler.scrollToPosition((value.toInt() - 1) / (dualPage { 2 }
?: 1))
else else
binding.mangaReaderPager.currentItem = (value.toInt() - 1) / (dualPage { 2 } ?: 1) binding.mangaReaderPager.currentItem =
(value.toInt() - 1) / (dualPage { 2 } ?: 1)
pageSliderHide() pageSliderHide()
} }
} }
@@ -197,7 +220,7 @@ class MangaReaderActivity : AppCompatActivity() {
val mangaSources = MangaSources val mangaSources = MangaSources
val scope = lifecycleScope val scope = lifecycleScope
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
mangaSources.init(Injekt.get<MangaExtensionManager>().installedExtensionsFlow) mangaSources.init(Injekt.get<MangaExtensionManager>().installedExtensionsFlow, this@MangaReaderActivity)
} }
model.mangaReadSources = mangaSources model.mangaReadSources = mangaSources
} else { } else {
@@ -213,7 +236,12 @@ class MangaReaderActivity : AppCompatActivity() {
logError(e) logError(e)
} }
} }
binding.mangaReaderSource.text = model.mangaReadSources!!.names[media.selected!!.sourceIndex] //check that index is not out of bounds (crash fix)
if (media.selected!!.sourceIndex >= model.mangaReadSources!!.names.size) {
media.selected!!.sourceIndex = 0
}
binding.mangaReaderSource.text =
model.mangaReadSources!!.names[media.selected!!.sourceIndex]
binding.mangaReaderTitle.text = media.userPreferredName binding.mangaReaderTitle.text = media.userPreferredName
@@ -226,34 +254,30 @@ class MangaReaderActivity : AppCompatActivity() {
chaptersTitleArr.add("${if (!chapter.title.isNullOrEmpty() && chapter.title != "null") "" else "Chapter "}${chapter.number}${if (!chapter.title.isNullOrEmpty() && chapter.title != "null") " : " + chapter.title else ""}") chaptersTitleArr.add("${if (!chapter.title.isNullOrEmpty() && chapter.title != "null") "" else "Chapter "}${chapter.number}${if (!chapter.title.isNullOrEmpty() && chapter.title != "null") " : " + chapter.title else ""}")
} }
showProgressDialog = if (settings.askIndividual) loadData<Boolean>("${media.id}_progressDialog") != true else false showProgressDialog =
progressDialog = if (settings.askIndividual) loadData<Boolean>("${media.id}_progressDialog")
if (showProgressDialog && Anilist.userid != null && if (media.isAdult) settings.updateForH else true) ?: true else false
AlertDialog.Builder(this, R.style.DialogTheme).setTitle(getString(R.string.title_update_progress)).apply {
setMultiChoiceItems(
arrayOf(getString(R.string.dont_ask_again, media.userPreferredName)),
booleanArrayOf(false)
) { _, _, isChecked ->
if (isChecked) progressDialog = null
saveData("${media.id}_progressDialog", isChecked)
showProgressDialog = isChecked
}
setOnCancelListener { hideBars() }
}
else null
//Chapter Change //Chapter Change
fun change(index: Int) { fun change(index: Int) {
mangaCache.clear() mangaCache.clear()
saveData("${media.id}_${chaptersArr[currentChapterIndex]}", currentChapterPage, this) saveData("${media.id}_${chaptersArr[currentChapterIndex]}", currentChapterPage, this)
ChapterLoaderDialog.newInstance(chapters[chaptersArr[index]]!!).show(supportFragmentManager, "dialog") ChapterLoaderDialog.newInstance(chapters[chaptersArr[index]]!!)
.show(supportFragmentManager, "dialog")
} }
//ChapterSelector //ChapterSelector
binding.mangaReaderChapterSelect.adapter = NoPaddingArrayAdapter(this, R.layout.item_dropdown, chaptersTitleArr) binding.mangaReaderChapterSelect.adapter =
NoPaddingArrayAdapter(this, R.layout.item_dropdown, chaptersTitleArr)
binding.mangaReaderChapterSelect.setSelection(currentChapterIndex) binding.mangaReaderChapterSelect.setSelection(currentChapterIndex)
binding.mangaReaderChapterSelect.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { binding.mangaReaderChapterSelect.onItemSelectedListener =
override fun onItemSelected(p0: AdapterView<*>?, p1: View?, position: Int, p3: Long) { object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
p0: AdapterView<*>?,
p1: View?,
position: Int,
p3: Long
) {
if (position != currentChapterIndex) change(position) if (position != currentChapterIndex) change(position)
} }
@@ -289,27 +313,57 @@ class MangaReaderActivity : AppCompatActivity() {
saveData("${media.id}_current_chp", chap.number, this) saveData("${media.id}_current_chp", chap.number, this)
currentChapterIndex = chaptersArr.indexOf(chap.number) currentChapterIndex = chaptersArr.indexOf(chap.number)
binding.mangaReaderChapterSelect.setSelection(currentChapterIndex) binding.mangaReaderChapterSelect.setSelection(currentChapterIndex)
binding.mangaReaderNextChap.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: "" binding.mangaReaderNextChap.text =
binding.mangaReaderPrevChap.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: "" chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: ""
binding.mangaReaderPrevChap.text =
chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: ""
applySettings() applySettings()
rpc?.close() val context = this
rpc = Discord.defaultRPC() val incognito = context.getSharedPreferences("Dantotsu", 0)
rpc?.send { ?.getBoolean("incognito", false) ?: false
type = RPC.Type.WATCHING if (isOnline(context) && Discord.token != null && !incognito) {
activityName = media.userPreferredName lifecycleScope.launch {
details = chap.title?.takeIf { it.isNotEmpty() } ?: getString(R.string.chapter_num, chap.number) val presence = RPC.createPresence(
state = "${chap.number}/${media.manga?.totalChapters ?: "??"}" RPC.Companion.RPCData(
media.cover?.let { cover -> applicationId = Discord.application_Id,
largeImage = RPC.Link(media.userPreferredName, cover) type = RPC.Type.WATCHING,
activityName = media.userPreferredName,
details = chap.title?.takeIf { it.isNotEmpty() }
?: getString(R.string.chapter_num, chap.number),
state = "${chap.number}/${media.manga?.totalChapters ?: "??"}",
largeImage = media.cover?.let { cover ->
RPC.Link(media.userPreferredName, cover)
},
smallImage = RPC.Link(
"Dantotsu",
Discord.small_Image
),
buttons = mutableListOf(
RPC.Link(getString(R.string.view_manga), media.shareLink ?: ""),
RPC.Link(
"Stream on Dantotsu",
"https://github.com/rebelonion/Dantotsu/"
)
)
)
)
val intent = Intent(context, DiscordService::class.java).apply {
putExtra("presence", presence)
} }
media.shareLink?.let { link -> DiscordServiceRunningSingleton.running = true
buttons.add(0, RPC.Link(getString(R.string.view_manga), link)) startService(intent)
} }
} }
} }
} }
scope.launch(Dispatchers.IO) { model.loadMangaChapterImages(chapter, media.selected!!) } scope.launch(Dispatchers.IO) {
model.loadMangaChapterImages(
chapter,
media.selected!!,
media.mainName()
)
}
} }
private val snapHelper = PagerSnapHelper() private val snapHelper = PagerSnapHelper()
@@ -322,6 +376,7 @@ class MangaReaderActivity : AppCompatActivity() {
if (orientation == Configuration.ORIENTATION_LANDSCAPE) callback.invoke() if (orientation == Configuration.ORIENTATION_LANDSCAPE) callback.invoke()
else null else null
} }
Force -> callback.invoke() Force -> callback.invoke()
} }
} }
@@ -353,7 +408,8 @@ class MangaReaderActivity : AppCompatActivity() {
maxChapterPage = chapImages.size.toLong() maxChapterPage = chapImages.size.toLong()
saveData("${media.id}_${chapter.number}_max", maxChapterPage) saveData("${media.id}_${chapter.number}_max", maxChapterPage)
imageAdapter = dualPage { DualPageAdapter(this, chapter) } ?: ImageAdapter(this, chapter) imageAdapter =
dualPage { DualPageAdapter(this, chapter) } ?: ImageAdapter(this, chapter)
if (chapImages.size > 1) { if (chapImages.size > 1) {
binding.mangaReaderSlider.apply { binding.mangaReaderSlider.apply {
@@ -374,8 +430,10 @@ class MangaReaderActivity : AppCompatActivity() {
if ((settings.default.direction == TOP_TO_BOTTOM || settings.default.direction == BOTTOM_TO_TOP)) { if ((settings.default.direction == TOP_TO_BOTTOM || settings.default.direction == BOTTOM_TO_TOP)) {
binding.mangaReaderSwipy.vertical = true binding.mangaReaderSwipy.vertical = true
if (settings.default.direction == TOP_TO_BOTTOM) { if (settings.default.direction == TOP_TO_BOTTOM) {
binding.BottomSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: getString(R.string.no_chapter) binding.BottomSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1)
binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: getString(R.string.no_chapter) ?: getString(R.string.no_chapter)
binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1)
?: getString(R.string.no_chapter)
binding.mangaReaderSwipy.onTopSwiped = { binding.mangaReaderSwipy.onTopSwiped = {
binding.mangaReaderPreviousChapter.performClick() binding.mangaReaderPreviousChapter.performClick()
} }
@@ -383,8 +441,10 @@ class MangaReaderActivity : AppCompatActivity() {
binding.mangaReaderNextChapter.performClick() binding.mangaReaderNextChapter.performClick()
} }
} else { } else {
binding.BottomSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: getString(R.string.no_chapter) binding.BottomSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1)
binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: getString(R.string.no_chapter) ?: getString(R.string.no_chapter)
binding.TopSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1)
?: getString(R.string.no_chapter)
binding.mangaReaderSwipy.onTopSwiped = { binding.mangaReaderSwipy.onTopSwiped = {
binding.mangaReaderNextChapter.performClick() binding.mangaReaderNextChapter.performClick()
} }
@@ -407,8 +467,10 @@ class MangaReaderActivity : AppCompatActivity() {
} else { } else {
binding.mangaReaderSwipy.vertical = false binding.mangaReaderSwipy.vertical = false
if (settings.default.direction == RIGHT_TO_LEFT) { if (settings.default.direction == RIGHT_TO_LEFT) {
binding.LeftSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: getString(R.string.no_chapter) binding.LeftSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1)
binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: getString(R.string.no_chapter) ?: getString(R.string.no_chapter)
binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1)
?: getString(R.string.no_chapter)
binding.mangaReaderSwipy.onLeftSwiped = { binding.mangaReaderSwipy.onLeftSwiped = {
binding.mangaReaderNextChapter.performClick() binding.mangaReaderNextChapter.performClick()
} }
@@ -416,8 +478,10 @@ class MangaReaderActivity : AppCompatActivity() {
binding.mangaReaderPreviousChapter.performClick() binding.mangaReaderPreviousChapter.performClick()
} }
} else { } else {
binding.LeftSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1) ?: getString(R.string.no_chapter) binding.LeftSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex - 1)
binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1) ?: getString(R.string.no_chapter) ?: getString(R.string.no_chapter)
binding.RightSwipeText.text = chaptersTitleArr.getOrNull(currentChapterIndex + 1)
?: getString(R.string.no_chapter)
binding.mangaReaderSwipy.onLeftSwiped = { binding.mangaReaderSwipy.onLeftSwiped = {
binding.mangaReaderPreviousChapter.performClick() binding.mangaReaderPreviousChapter.performClick()
} }
@@ -442,7 +506,8 @@ class MangaReaderActivity : AppCompatActivity() {
if (settings.default.layout != PAGED) { if (settings.default.layout != PAGED) {
binding.mangaReaderRecyclerContainer.visibility = View.VISIBLE binding.mangaReaderRecyclerContainer.visibility = View.VISIBLE
binding.mangaReaderRecyclerContainer.controller.settings.isRotationEnabled = settings.default.rotation binding.mangaReaderRecyclerContainer.controller.settings.isRotationEnabled =
settings.default.rotation
val detector = GestureDetectorCompat(this, object : GesturesListener() { val detector = GestureDetectorCompat(this, object : GesturesListener() {
override fun onLongPress(e: MotionEvent) { override fun onLongPress(e: MotionEvent) {
@@ -450,18 +515,31 @@ class MangaReaderActivity : AppCompatActivity() {
child ?: return@let false child ?: return@let false
val pos = binding.mangaReaderRecycler.getChildAdapterPosition(child) val pos = binding.mangaReaderRecycler.getChildAdapterPosition(child)
val callback: (ImageViewDialog) -> Unit = { dialog -> val callback: (ImageViewDialog) -> Unit = { dialog ->
lifecycleScope.launch { imageAdapter?.loadImage(pos, child as GestureFrameLayout) } lifecycleScope.launch {
binding.mangaReaderRecycler.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) imageAdapter?.loadImage(
pos,
child as GestureFrameLayout
)
}
binding.mangaReaderRecycler.performHapticFeedback(
HapticFeedbackConstants.LONG_PRESS
)
dialog.dismiss() dialog.dismiss()
} }
dualPage { dualPage {
val page = chapter.dualPages().getOrNull(pos) ?: return@dualPage false val page =
chapter.dualPages().getOrNull(pos) ?: return@dualPage false
val nextPage = page.second val nextPage = page.second
if (settings.default.direction != LEFT_TO_RIGHT && nextPage != null) if (settings.default.direction != LEFT_TO_RIGHT && nextPage != null)
onImageLongClicked(pos * 2, nextPage, page.first, callback) onImageLongClicked(pos * 2, nextPage, page.first, callback)
else else
onImageLongClicked(pos * 2, page.first, nextPage, callback) onImageLongClicked(pos * 2, page.first, nextPage, callback)
} ?: onImageLongClicked(pos, chapImages.getOrNull(pos) ?: return@let false, null, callback) } ?: onImageLongClicked(
pos,
chapImages.getOrNull(pos) ?: return@let false,
null,
callback
)
} }
) binding.mangaReaderRecycler.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) ) binding.mangaReaderRecycler.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
super.onLongPress(e) super.onLongPress(e)
@@ -503,12 +581,16 @@ class MangaReaderActivity : AppCompatActivity() {
&& (!v.canScrollVertically(-1) || !v.canScrollVertically(1))) && (!v.canScrollVertically(-1) || !v.canScrollVertically(1)))
|| ||
((direction == LEFT_TO_RIGHT || direction == RIGHT_TO_LEFT) ((direction == LEFT_TO_RIGHT || direction == RIGHT_TO_LEFT)
&& (!v.canScrollHorizontally(-1) || !v.canScrollHorizontally(1))) && (!v.canScrollHorizontally(-1) || !v.canScrollHorizontally(
1
)))
) { ) {
handleController(true) handleController(true)
} else handleController(false) } else handleController(false)
} }
updatePageNumber(manager.findLastVisibleItemPosition().toLong() * (dualPage { 2 } ?: 1) + 1) updatePageNumber(
manager.findLastVisibleItemPosition().toLong() * (dualPage { 2 }
?: 1) + 1)
super.onScrolled(v, dx, dy) super.onScrolled(v, dx, dy)
} }
}) })
@@ -579,6 +661,7 @@ class MangaReaderActivity : AppCompatActivity() {
true true
} else false } else false
} }
KEYCODE_VOLUME_DOWN, KEYCODE_DPAD_DOWN, KEYCODE_PAGE_DOWN -> { KEYCODE_VOLUME_DOWN, KEYCODE_DPAD_DOWN, KEYCODE_PAGE_DOWN -> {
if (event.keyCode == KEYCODE_VOLUME_DOWN) if (event.keyCode == KEYCODE_VOLUME_DOWN)
if (!settings.default.volumeButtons) if (!settings.default.volumeButtons)
@@ -588,6 +671,7 @@ class MangaReaderActivity : AppCompatActivity() {
true true
} else false } else false
} }
else -> { else -> {
super.dispatchKeyEvent(event) super.dispatchKeyEvent(event)
} }
@@ -620,8 +704,60 @@ class MangaReaderActivity : AppCompatActivity() {
goneTimer.schedule(timerTask, controllerDuration) goneTimer.schedule(timerTask, controllerDuration)
} }
fun handleController(shouldShow: Boolean? = null) { enum class pressPos {
LEFT, RIGHT, CENTER
}
fun handleController(shouldShow: Boolean? = null, event: MotionEvent? = null) {
var pressLocation = pressPos.CENTER
if (!sliding) { if (!sliding) {
if (event != null && settings.default.layout == PAGED) {
if (event.action != MotionEvent.ACTION_UP) return
val x = event.rawX.toInt()
val y = event.rawY.toInt()
val screenWidth = Resources.getSystem().displayMetrics.widthPixels
//if in the 1st 1/5th of the screen width, left and lower than 1/5th of the screen height, left
if (screenWidth / 5 in (x + 1)..<y) {
pressLocation = if (settings.default.direction == RIGHT_TO_LEFT) {
pressPos.RIGHT
} else {
pressPos.LEFT
}
}
//if in the last 1/5th of the screen width, right and lower than 1/5th of the screen height, right
else if (x > screenWidth - screenWidth / 5 && y > screenWidth / 5) {
pressLocation = if (settings.default.direction == RIGHT_TO_LEFT) {
pressPos.LEFT
} else {
pressPos.RIGHT
}
}
}
// if pressLocation is left or right go to previous or next page (paged mode only)
if (pressLocation == pressPos.LEFT) {
if (binding.mangaReaderPager.currentItem > 0) {
//if the current images zoomed in, go back to normal before going to previous page
if (imageAdapter?.isZoomed() == true) {
imageAdapter?.setZoom(1f)
}
binding.mangaReaderPager.currentItem -= 1
return
}
} else if (pressLocation == pressPos.RIGHT) {
if (binding.mangaReaderPager.currentItem < maxChapterPage - 1) {
//if the current images zoomed in, go back to normal before going to next page
if (imageAdapter?.isZoomed() == true) {
imageAdapter?.setZoom(1f)
}
//if right to left, go to previous page
binding.mangaReaderPager.currentItem += 1
return
}
}
if (!settings.showSystemBars) { if (!settings.showSystemBars) {
hideBars() hideBars()
checkNotch() checkNotch()
@@ -662,8 +798,14 @@ class MangaReaderActivity : AppCompatActivity() {
isContVisible = false isContVisible = false
if (!isAnimating) { if (!isAnimating) {
isAnimating = true isAnimating = true
ObjectAnimator.ofFloat(binding.mangaReaderCont, "alpha", 1f, 0f).setDuration(controllerDuration).start() ObjectAnimator.ofFloat(binding.mangaReaderCont, "alpha", 1f, 0f)
ObjectAnimator.ofFloat(binding.mangaReaderBottomLayout, "translationY", 0f, 128f) .setDuration(controllerDuration).start()
ObjectAnimator.ofFloat(
binding.mangaReaderBottomLayout,
"translationY",
0f,
128f
)
.apply { interpolator = overshoot;duration = controllerDuration;start() } .apply { interpolator = overshoot;duration = controllerDuration;start() }
ObjectAnimator.ofFloat(binding.mangaReaderTopLayout, "translationY", 0f, -128f) ObjectAnimator.ofFloat(binding.mangaReaderTopLayout, "translationY", 0f, -128f)
.apply { interpolator = overshoot;duration = controllerDuration;start() } .apply { interpolator = overshoot;duration = controllerDuration;start() }
@@ -672,7 +814,8 @@ class MangaReaderActivity : AppCompatActivity() {
} else { } else {
isContVisible = true isContVisible = true
binding.mangaReaderCont.visibility = View.VISIBLE binding.mangaReaderCont.visibility = View.VISIBLE
ObjectAnimator.ofFloat(binding.mangaReaderCont, "alpha", 0f, 1f).setDuration(controllerDuration).start() ObjectAnimator.ofFloat(binding.mangaReaderCont, "alpha", 0f, 1f)
.setDuration(controllerDuration).start()
ObjectAnimator.ofFloat(binding.mangaReaderTopLayout, "translationY", -128f, 0f) ObjectAnimator.ofFloat(binding.mangaReaderTopLayout, "translationY", -128f, 0f)
.apply { interpolator = overshoot;duration = controllerDuration;start() } .apply { interpolator = overshoot;duration = controllerDuration;start() }
ObjectAnimator.ofFloat(binding.mangaReaderBottomLayout, "translationY", 128f, 0f) ObjectAnimator.ofFloat(binding.mangaReaderBottomLayout, "translationY", 128f, 0f)
@@ -698,6 +841,7 @@ class MangaReaderActivity : AppCompatActivity() {
model.loadMangaChapterImages( model.loadMangaChapterImages(
chapters[chaptersArr.getOrNull(currentChapterIndex + 1) ?: return@launch]!!, chapters[chaptersArr.getOrNull(currentChapterIndex + 1) ?: return@launch]!!,
media.selected!!, media.selected!!,
media.mainName(),
false false
) )
loading = false loading = false
@@ -706,23 +850,55 @@ class MangaReaderActivity : AppCompatActivity() {
private fun progress(runnable: Runnable) { private fun progress(runnable: Runnable) {
if (maxChapterPage - currentChapterPage <= 1 && Anilist.userid != null) { if (maxChapterPage - currentChapterPage <= 1 && Anilist.userid != null) {
if (showProgressDialog && progressDialog != null) { showProgressDialog =
progressDialog?.setCancelable(false) if (settings.askIndividual) loadData<Boolean>("${media.id}_progressDialog")
?.setPositiveButton(getString(R.string.yes)) { dialog, _ -> ?: true else false
if (showProgressDialog) {
val dialogView = layoutInflater.inflate(R.layout.item_custom_dialog, null)
val checkbox = dialogView.findViewById<CheckBox>(R.id.dialog_checkbox)
checkbox.text = getString(R.string.dont_ask_again, media.userPreferredName)
checkbox.setOnCheckedChangeListener { _, isChecked ->
saveData("${media.id}_progressDialog", !isChecked)
showProgressDialog = !isChecked
}
val incognito =
currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("incognito", false) ?: false
AlertDialog.Builder(this, R.style.MyPopup)
.setTitle(getString(R.string.title_update_progress))
.apply {
if (incognito) {
setMessage(getString(R.string.incognito_will_not_update))
}
}
.setView(dialogView)
.setCancelable(false)
.setPositiveButton(getString(R.string.yes)) { dialog, _ ->
saveData("${media.id}_save_progress", true) saveData("${media.id}_save_progress", true)
updateProgress(media, MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!).toString()) updateProgress(
media,
MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
.toString()
)
dialog.dismiss() dialog.dismiss()
runnable.run() runnable.run()
} }
?.setNegativeButton(getString(R.string.no)) { dialog, _ -> .setNegativeButton(getString(R.string.no)) { dialog, _ ->
saveData("${media.id}_save_progress", false) saveData("${media.id}_save_progress", false)
dialog.dismiss() dialog.dismiss()
runnable.run() runnable.run()
} }
progressDialog?.show() .setOnCancelListener { hideBars() }
.create()
.show()
} else { } else {
if (loadData<Boolean>("${media.id}_save_progress") != false && if (media.isAdult) settings.updateForH else true) if (loadData<Boolean>("${media.id}_save_progress") != false && if (media.isAdult) settings.updateForH else true)
updateProgress(media, MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!).toString()) updateProgress(
media,
MangaNameAdapter.findChapterNumber(media.manga!!.selectedChapter!!)
.toString()
)
runnable.run() runnable.run()
} }
} else { } else {

View File

@@ -10,7 +10,8 @@ import kotlin.math.max
class PreloadLinearLayoutManager(context: Context, orientation: Int, reverseLayout: Boolean) : class PreloadLinearLayoutManager(context: Context, orientation: Int, reverseLayout: Boolean) :
LinearLayoutManager(context, orientation, reverseLayout) { LinearLayoutManager(context, orientation, reverseLayout) {
private val mOrientationHelper: OrientationHelper = OrientationHelper.createOrientationHelper(this, orientation) private val mOrientationHelper: OrientationHelper =
OrientationHelper.createOrientationHelper(this, orientation)
/** /**
* As [LinearLayoutManager.collectAdjacentPrefetchPositions] will prefetch one view for us, * As [LinearLayoutManager.collectAdjacentPrefetchPositions] will prefetch one view for us,
@@ -37,7 +38,8 @@ class PreloadLinearLayoutManager(context: Context, orientation: Int, reverseLayo
val currentPosition: Int = getPosition(child ?: return) + layoutDirection val currentPosition: Int = getPosition(child ?: return) + layoutDirection
if (layoutDirection == 1) { if (layoutDirection == 1) {
val scrollingOffset = (mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.endAfterPadding) val scrollingOffset =
(mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.endAfterPadding)
((currentPosition + 1) until (currentPosition + preloadItemCount + 1)).forEach { ((currentPosition + 1) until (currentPosition + preloadItemCount + 1)).forEach {
if (it >= 0 && it < state.itemCount) { if (it >= 0 && it < state.itemCount) {
layoutPrefetchRegistry.addPosition(it, max(0, scrollingOffset)) layoutPrefetchRegistry.addPosition(it, max(0, scrollingOffset))

View File

@@ -14,7 +14,11 @@ class ReaderSettingsDialogFragment : BottomSheetDialogFragment() {
private var _binding: BottomSheetCurrentReaderSettingsBinding? = null private var _binding: BottomSheetCurrentReaderSettingsBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = BottomSheetCurrentReaderSettingsBinding.inflate(inflater, container, false) _binding = BottomSheetCurrentReaderSettingsBinding.inflate(inflater, container, false)
return binding.root return binding.root
} }
@@ -24,11 +28,14 @@ class ReaderSettingsDialogFragment : BottomSheetDialogFragment() {
val activity = requireActivity() as MangaReaderActivity val activity = requireActivity() as MangaReaderActivity
val settings = activity.settings.default val settings = activity.settings.default
binding.readerDirectionText.text = resources.getStringArray(R.array.manga_directions)[settings.direction.ordinal] binding.readerDirectionText.text =
resources.getStringArray(R.array.manga_directions)[settings.direction.ordinal]
binding.readerDirection.rotation = 90f * (settings.direction.ordinal) binding.readerDirection.rotation = 90f * (settings.direction.ordinal)
binding.readerDirection.setOnClickListener { binding.readerDirection.setOnClickListener {
settings.direction = Directions[settings.direction.ordinal + 1] ?: Directions.TOP_TO_BOTTOM settings.direction =
binding.readerDirectionText.text = resources.getStringArray(R.array.manga_directions)[settings.direction.ordinal] Directions[settings.direction.ordinal + 1] ?: Directions.TOP_TO_BOTTOM
binding.readerDirectionText.text =
resources.getStringArray(R.array.manga_directions)[settings.direction.ordinal]
binding.readerDirection.rotation = 90f * (settings.direction.ordinal) binding.readerDirection.rotation = 90f * (settings.direction.ordinal)
activity.applySettings() activity.applySettings()
} }
@@ -56,7 +63,8 @@ class ReaderSettingsDialogFragment : BottomSheetDialogFragment() {
activity.applySettings() activity.applySettings()
} }
binding.readerLayoutText.text = resources.getStringArray(R.array.manga_layouts)[settings.layout.ordinal] binding.readerLayoutText.text =
resources.getStringArray(R.array.manga_layouts)[settings.layout.ordinal]
var selected = list[settings.layout.ordinal] var selected = list[settings.layout.ordinal]
selected.alpha = 1f selected.alpha = 1f
@@ -65,8 +73,10 @@ class ReaderSettingsDialogFragment : BottomSheetDialogFragment() {
selected.alpha = 0.33f selected.alpha = 0.33f
selected = imageButton selected = imageButton
selected.alpha = 1f selected.alpha = 1f
settings.layout = CurrentReaderSettings.Layouts[index]?:CurrentReaderSettings.Layouts.CONTINUOUS settings.layout =
binding.readerLayoutText.text = resources.getStringArray(R.array.manga_layouts)[settings.layout.ordinal] CurrentReaderSettings.Layouts[index] ?: CurrentReaderSettings.Layouts.CONTINUOUS
binding.readerLayoutText.text =
resources.getStringArray(R.array.manga_layouts)[settings.layout.ordinal]
activity.applySettings() activity.applySettings()
paddingAvailable(settings.layout.ordinal != 0) paddingAvailable(settings.layout.ordinal != 0)
} }
@@ -87,7 +97,8 @@ class ReaderSettingsDialogFragment : BottomSheetDialogFragment() {
selectedDual.alpha = 0.33f selectedDual.alpha = 0.33f
selectedDual = imageButton selectedDual = imageButton
selectedDual.alpha = 1f selectedDual.alpha = 1f
settings.dualPageMode = CurrentReaderSettings.DualPageModes[index] ?: CurrentReaderSettings.DualPageModes.Automatic settings.dualPageMode = CurrentReaderSettings.DualPageModes[index]
?: CurrentReaderSettings.DualPageModes.Automatic
binding.readerDualPageText.text = settings.dualPageMode.toString() binding.readerDualPageText.text = settings.dualPageMode.toString()
activity.applySettings() activity.applySettings()
} }

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