Compare commits

...

567 Commits

Author SHA1 Message Date
rebelonion
7fdd8d5d6e fix: no need for update post 2024-05-07 05:40:08 -05:00
rebelonion
fda68a7ca2 fix: lang codes not found 2024-05-06 22:31:21 -05:00
rebelonion
40c2989b34 chore: version bump 2024-05-06 21:36:37 -05:00
rebelonion
636a56fb7f feat: multi stream audio support 2024-05-06 21:30:26 -05:00
aayush262
abcf9fcbef feat: activity ui tweaks 2024-05-06 20:21:04 +05:30
rebelonion
b187cf06be fix: optimization # 2 2024-05-05 20:03:07 -05:00
rebelonion
14f29d09df fix: optimization # 1 2024-05-05 18:57:03 -05:00
aayush262
390c709f5d fix: not un-hiding item 2024-05-05 01:41:31 +05:30
aayush262
7aa0951cdf feat: long click "continue Watching" to see hidden items 2024-05-05 00:45:46 +05:30
aayush262
aae80f6493 fix: something 2024-05-05 00:00:13 +05:30
aayush262
f86086cc7a fix: only save when clicked save 2024-05-04 01:05:55 +05:30
aayush262
425ca158a3 feat: hide media from home screen 2024-05-04 00:52:01 +05:30
aayush262
7bdc7c1719 feat: moved social bellow synonyms 2024-05-03 23:17:43 +05:30
rebelonion
126fe75c46 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-05-03 09:31:15 -05:00
rebelonion
dc19694d68 fix: check for empty uri 2024-05-03 09:31:12 -05:00
rebelonion
95c0b574b0 fix media initialization check 2024-05-03 09:22:52 -05:00
rebelonion
a1b9f90500 fix: switch auto off 2024-05-03 09:19:35 -05:00
aayush262
d5be21882e fix: use binding in CrashActivity.kt 2024-05-02 15:24:39 +05:30
aayush262
e9551be62d feat(Media List view): switch between grid and list view 2024-05-02 14:34:49 +05:30
rebelonion
3a88656e21 feat: share as file option for crash 2024-05-01 21:35:20 -05:00
rebelonion
97ff591b62 fix: switch visibility 2024-05-01 21:19:29 -05:00
rebelonion
a3e1cc45b3 fix: scanlator | language selection 2024-05-01 21:08:33 -05:00
rebelonion
deda67a070 fix: more network stuffs 2024-05-01 20:12:05 -05:00
rebelonion
e32bfa0cfa fix: network safety 2024-05-01 19:45:37 -05:00
rebelonion
f03af6856a Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-05-01 14:47:29 -05:00
rebelonion
8a0224e6b0 feat: crash report | various small fixes 2024-05-01 14:45:08 -05:00
aayush262
71870ff235 fix: banner crash 2024-05-02 00:39:27 +05:30
rebelonion
31c509f88c fix: decouple animator for stories 2024-05-01 12:43:04 -05:00
Sadwhy
4362dd94c1 Very small UI change (#384)
* Change Text gap

* Use center instead of top and word limit

* Max width for desc and center instead of top

* Changed some strings

* fix
2024-05-01 12:34:56 -05:00
rebelonion
9b132e9cb6 fix: hide image search for manga 2024-04-30 19:53:07 -05:00
rebelonion
1431027cf3 fix: smol changes 2024-04-30 19:39:19 -05:00
rebelonion
2d2f058d27 fix: downloaded next not working 2024-04-30 19:07:47 -05:00
rebelonion
85835b5c2e fix: ffmpeg not downloading all tracks 2024-04-30 17:57:55 -05:00
aayush262
08c3806d0d fix: padding fic the Final 2024-05-01 01:44:43 +05:30
aayush262
74fcd581b0 fix: strings 2024-05-01 01:29:46 +05:30
aayush262
0ea1ec1c35 fix: title height 2024-05-01 01:11:41 +05:30
aayush262
42330032c8 feat: list view for home page media 2024-05-01 00:57:58 +05:30
aayush262
fdb7f45a3d chore: cleanup pt 3 2024-04-30 20:27:09 +05:30
aayush262
390ce9a022 Merge remote-tracking branch 'origin/dev' into dev 2024-04-30 20:00:43 +05:30
rebelonion
95a9d289c9 fix: null media.users 2024-04-29 22:30:35 -05:00
rebelonion
69fb86d015 feat: special UE handler 2024-04-29 22:12:33 -05:00
rebelonion
4fc96b77e3 fix: some context issues 2024-04-29 22:08:38 -05:00
rebelonion
040b0845de fix: download title mismatch 2024-04-29 18:53:11 -05:00
rebelonion
eca38070cb fix: notification choosing wrong source 2024-04-29 17:51:41 -05:00
rebelonion
e9a60eafb6 fix: collections separate 2024-04-29 17:25:51 -05:00
rebelonion
0967721897 fix: too much combining 2024-04-29 16:15:40 -05:00
rebelonion
ed0e06d1af fix: more combining shit 2024-04-29 16:07:52 -05:00
rebelonion
a7589a0296 fix: combine some shit 2024-04-29 15:50:05 -05:00
aayush262
e30124d342 Merge remote-tracking branch 'origin/dev' into dev 2024-04-30 00:41:50 +05:30
rebelonion
140737bb40 fix: next button 2024-04-29 14:06:40 -05:00
rebelonion
8b582a9d32 fix: task system cleanup 2024-04-29 14:03:26 -05:00
aayush262
0a0da65f7c chore: cleanup pt 3 2024-04-30 00:26:30 +05:30
aayush262
ea48809d07 chore: cleanup 2024-04-29 03:04:51 +05:30
aayush262
a573fbdc89 chore: cleanup 2024-04-28 15:23:19 +05:30
aayush262
c947dbdb70 feat(social): only save last 100 activity ids 2024-04-28 03:30:14 +05:30
aayush262
90b9b7bef3 feat(social): filter activity only with in 3days 2024-04-28 01:40:17 +05:30
aayush262
133354a22d feat(social): continue from where you left 2024-04-27 22:07:09 +05:30
aayush262
73ef5f4bbc feat(social): text activity 2024-04-27 19:50:59 +05:30
aayush262
140dd2e0c3 fix(social): activity name 2024-04-27 17:54:49 +05:30
aayush262
90e611dc4f fix(social): crash when clicking story 2024-04-27 17:22:00 +05:30
aayush262
7ecdbfd42f fix(social): thick bar 2024-04-27 09:48:50 +05:30
aayush262
f4c95b6cc0 feat(social): mark alr watched 2024-04-27 01:20:40 +05:30
aayush262
da456d3067 feat(social): like button fix 2024-04-26 19:57:55 +05:30
aayush262
856deb7755 feat: activity view 2024-04-26 03:27:04 +05:30
aayush262
55ad8dccad chore: cleanup 2024-04-23 00:38:33 +05:30
aayush262
e81773f2b5 fix: reading in manga instead of watching 2024-04-22 21:38:46 +05:30
aayush262
c5a03c4455 Merge remote-tracking branch 'origin/dev' into dev 2024-04-21 21:50:41 +05:30
aayush262
d2127f92a1 fix: double setting page in theme settings 2024-04-21 21:46:36 +05:30
rebelonion
870cb751a4 fix: duplicate download 2024-04-21 08:06:23 -05:00
rebelonion
4ffe9d7505 fix: novel loading 2024-04-21 07:36:23 -05:00
rebelonion
513b937e59 fix: some sorting problems 2024-04-21 06:41:51 -05:00
rebelonion
6113a10556 fix: update spinner 2024-04-21 06:21:10 -05:00
rebelonion
233f4bfb48 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-04-21 06:06:01 -05:00
aayush262
3fd01d582a fix: forgot to remove todo strings 2024-04-21 16:22:02 +05:30
aayush262
00758af458 feat: desc for every setting 2024-04-21 16:02:21 +05:30
rebelonion
4477e3a0e1 fix: view clickable after hidden
https://stackoverflow.com/questions/4728908/android-view-with-view-gone-still-receives-ontouch-and-onclick
2024-04-21 04:50:55 -05:00
rebelonion
e475cc5c01 fix: novel extension installing 2024-04-21 04:31:24 -05:00
rebelonion
3622d91886 fix: allow deprecated media to be played 2024-04-21 02:58:17 -05:00
rebelonion
3c46c21a25 feat: downloading extensions 2024-04-19 11:24:03 -05:00
Sadwhy
44178b2de2 Why use decapitated actions? (#373) 2024-04-19 10:52:12 -05:00
rebel onion
13f5d0978d Update Crowdin configuration file 2024-04-19 10:22:05 -05:00
rebelonion
70a50ece43 chore: cleanup pt2 2024-04-19 06:13:14 -05:00
rebelonion
24147e746a chore: code cleanup 2024-04-19 06:03:40 -05:00
rebelonion
386e02a564 fix: exoplayer initialization 2024-04-19 05:40:43 -05:00
rebelonion
865b96a219 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-04-19 05:25:01 -05:00
rebel onion
dd38bb156b Update Crowdin configuration file 2024-04-19 05:07:19 -05:00
rebelonion
72c07b7d7a fix: app updater single apk 2024-04-19 04:33:36 -05:00
rebelonion
3f19cadffc fix: workflow universal 2024-04-19 04:16:54 -05:00
rebel onion
670d16bd8e Addons (#368)
* feat: (wip) torrent

credit to kuukiyomi

* fix: extensions -> addons

* fix: unified loader

* feat: (wip) modularity

* fix: addon ui

* feat: addon install/uninstall

---------

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

* Theme

* Extension, common, notification and anime

* manga and about

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

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

* fix: improve results and logging

* fix: not everything is a title

Fixed the book title style of capitalization

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

* Fix: the other half of the file

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

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

* fix: no need to overengineer it

* feat: add cache to bitmap download

dfgdfg

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

* fix: allow Material You icons to load

* fix: remove unused preference item

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

* fix: stop overriding user settings

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

* fix: prevent leaving landscape behind

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

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

* fix: send headers to ffmpeg

ffmpeg can be a real bitch to work with

* fix: offline page for new download system

* feat: novel to new system | load freezing

* chore: clean manifest

* fix: notification incrementing

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

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

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

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

* fix: also populate the forks page

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

* Updated Icons (#13)

* update

* update beta.yml

* nicer icons

* update view_list_24.xml

* revert changes

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

* feat(discord): added haptics

* fine...

* fix(strings): my genius is frightening

* feat: add option to only show the first button

* feat: discord rpc menu

* feat(link): add button preview back

* feat(accounts): redirect on avatar click

---------

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

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

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

* feat: status filter fully functional

* chore: upgrade AGP to 8.3.1

* fix: splitted status list and cleaned up

* fix(search): underscore

* feat: attempt to add backend for countryOfOrigin filter

* fix: countryOfOrigin query and gradle

* feat: source filter fully functional

* fix(source): underscore

* feat: swap source with status

* fix: add searchSource to reset fun

* fix: clear underline after reopening bottom sheet

* chore: remove unnecessary declaration

* feat: add global to countryOfOrigin dropdown

* feat: floating cancel and apply button

* fix: added searchStatus and searchYear back to manga filter

* feat: desperate attempt for manga year filter

* feat(sortBy): added new releases item

* fix: year filter

---------

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

* feat(discord): added haptics

* fine...

* fix(strings): my genius is frightening

* feat: add option to only show the first button

* feat: discord rpc menu

* feat(link): add button preview back

---------

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

* fix: offset for split TextView values

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

* feat: extension launch from notice

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

* fix: thought he was slick

hiding in plain sight

* fix: it's not THAT important

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

* feat: mirror app color option

* fix: the minimum size cut off

* feat: make the stat widget decent

* fix: prevent bleeding edges

* fix: PREVENT BLEEDING EDGES!

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

* fix: clean up a sloppy fix

* chore: merge name adapters

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

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

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

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

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

* feat: improve app restart process

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

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

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

* fix: use the convenience method

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

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

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

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

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

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

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

* Resolved Merge Conflicts and Removed Unnecessary Imports

* Resolved Merge Conflicts

* Resolved Merge Conflicts

* Resolved Merge Conflicts

* Resolved problems

* Fixed a little mistake

* Made Requested Changes

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

* fix: clean up the overlapping decor

* feat: match theme color with navbar

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

* feat: hide navigation bar until swiped

* fix: limit announcements to official

* feat: keep navigation visible for back

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

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

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

* feet: Attached strings

* feet(fix)

---------

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

* feat: scanlation mass tick

* fix: togglebutton on scanlation scrollview

* fix: fix ImageButton padding + overlay

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

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

added oriax in picker + applier

* Update colors.xml

Added new color seed

* Update themes.xml

Added Oriax Lightmode

* Update themes.xml

Added Oriax Darkmode + OLED
2024-02-17 22:46:54 -06:00
aayush262
1fe50d2cca feat: reply in comments(WIP) 2024-02-17 12:29:39 +05:30
aayush262
420c0348f9 fix: mod cant ban themself now 2024-02-17 12:25:18 +05:30
aayush262
9be81aa4a9 fix: paddings of badges 2024-02-16 19:49:19 +05:30
aayush262
64c8f4225c fix: paddings in comments 2024-02-16 16:37:31 +05:30
rebelonion
a7c9604c43 fix: first comment message appear every time 2024-02-15 18:45:57 -06:00
rebelonion
68cc81e56c fix: correct config import 2024-02-15 18:39:46 -06:00
rebelonion
c9a64b1638 feat: server-side auth 2024-02-15 18:28:03 -06:00
rebelonion
ee7cff0fea feat: remember comment sort order 2024-02-15 14:17:45 -06:00
rebelonion
4c35f9a0cf fix: don't show 404 if no comments 2024-02-15 13:32:05 -06:00
rebelonion
9d9c4f026d fix: better error message 2024-02-15 13:24:34 -06:00
rebelonion
b4c7ea5f26 fix: round image on comment bar 2024-02-15 13:01:04 -06:00
rebelonion
093bee94c6 fix: update timestamp without reloading the page 2024-02-15 13:00:08 -06:00
rebelonion
fb99429dd7 fix: better attempt to get anilist username 2024-02-15 12:53:06 -06:00
rebelonion
a73c4cd678 feat: comments targeted at database 2024-02-15 12:44:52 -06:00
aayush262
1694a1cb24 feat: comments sorter popup 2024-02-15 15:25:18 +05:30
rebelonion
aaf9bdd00c feat: creating, deleting comments | markdown, spoiler comments 2024-02-14 06:41:24 -06:00
aayush262
129adc5825 chore: upgrade download-artifact-v3 -> v4 2024-02-13 22:39:04 +05:30
aayush262
07e7456ed8 Merge remote-tracking branch 'origin/dev' into dev 2024-02-13 13:36:31 +05:30
aayush262
7168e08587 feat: something idr 2024-02-13 13:35:46 +05:30
rebel onion
efb3b27317 Merge pull request #195 from Sadwhy/patch-6
A well recognized font
2024-02-12 17:34:33 -06:00
Sadwhy
2c3247c194 fixed unfortunate licence issues 2024-02-12 22:24:18 +06:00
aayush262
d37ebf8cdd fix: typo 2024-02-12 21:44:54 +05:30
aayush262
a73b049fd4 feat: moved "Play on youtube" to info page 2024-02-12 21:44:22 +05:30
aayush262
b3de2f805f wip: "send comments" interface 2024-02-12 21:43:29 +05:30
Sadwhy
0bec4f4d61 Mojangles font 2024-02-12 14:38:27 +06:00
aayush262
4be4a0968d Merge remote-tracking branch 'origin/dev' into dev 2024-02-12 01:45:16 +05:30
aayush262
97b957a0ab wip: UI for comments 2024-02-12 01:44:36 +05:30
Sadwhy
a22083dfcd fix: Updated discord links 2024-02-11 23:00:16 +05:30
rebel onion
9dbc3db1b8 Merge pull request #188 from Sadwhy/patch-6
Offline padding and faq update
2024-02-11 05:16:27 -06:00
Sadwhy
7af71ba217 Faq Rewrite P.1 2024-02-11 16:59:48 +06:00
rebelonion
80f3523f2e Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-02-11 04:16:23 -06:00
rebelonion
0afad1d9ae feat: comment authorization 2024-02-11 04:16:22 -06:00
aayush262
915c6c1dfe feat: better format for change logs 2024-02-10 23:11:20 +05:30
aayush262
e319aeb342 feat: monet icon for alpha 2024-02-10 23:08:13 +05:30
Sadwhy
ac20426689 Added proper padding to offline text/buttons 2024-02-09 09:02:53 +06:00
rebelonion
83c07467a9 fix: disable some buttons on fdroid build 2024-02-08 08:54:10 -06:00
rebelonion
0225b28fea fix: hitting enter on password input continues 2024-02-08 08:38:41 -06:00
rebelonion
1e2a740dae Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-02-08 08:15:53 -06:00
rebelonion
22e687b9d8 feat: add more helpers/developers 2024-02-08 08:15:51 -06:00
rebelonion
f088b90964 fix: ignore fdroid builds in beta 2024-02-08 08:15:29 -06:00
513 changed files with 33448 additions and 11540 deletions

View File

@@ -15,7 +15,7 @@ jobs:
steps:
- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
@@ -39,7 +39,7 @@ jobs:
fi
echo "Commits since $LAST_SHA:"
# Accumulate commit logs in a shell variable
COMMIT_LOGS=$(git log $LAST_SHA..HEAD --pretty=format:"%h - %s")
COMMIT_LOGS=$(git log $LAST_SHA..HEAD --pretty=format:"● %s ~%an")
# URL-encode the newline characters for GitHub Actions
COMMIT_LOGS="${COMMIT_LOGS//'%'/'%25'}"
COMMIT_LOGS="${COMMIT_LOGS//$'\n'/'%0A'}"
@@ -48,9 +48,11 @@ jobs:
echo "COMMIT_LOG=${COMMIT_LOGS}" >> $GITHUB_ENV
# Debugging: Print the variable to check its content
echo "$COMMIT_LOGS"
echo "$COMMIT_LOGS" > commit_log.txt
shell: /usr/bin/bash -e {0}
env:
CI: true
continue-on-error: true
- name: Save Current SHA for Next Run
run: echo ${{ github.sha }} > last_sha.txt
@@ -64,7 +66,7 @@ jobs:
echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: Setup JDK 17
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 17
@@ -75,7 +77,7 @@ jobs:
- name: List files in the directory
run: ls -l
- name: Make gradlew executable
run: chmod +x ./gradlew
@@ -83,28 +85,31 @@ jobs:
run: ./gradlew assembleGoogleAlpha -Pandroid.injected.signing.store.file=$GITHUB_WORKSPACE/key.keystore -Pandroid.injected.signing.store.password=${{ secrets.KEYSTORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.KEY_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.KEY_PASSWORD }}
- name: Upload a Build Artifact
uses: actions/upload-artifact@v3.0.0
uses: actions/upload-artifact@v4
with:
name: Dantotsu
retention-days: 5
compression-level: 9
path: "app/build/outputs/apk/google/alpha/app-google-alpha.apk"
- name: Upload APK to Discord and Telegram
if: ${{ github.repository == 'rebelonion/Dantotsu' }}
shell: bash
run: |
#Discord
commit_messages=$(echo "$COMMIT_LOG" | sed 's/%0A/\n/g')
commit_messages=$(echo "$COMMIT_LOG" | sed 's/%0A/\n/g; s/^/\n/')
# Truncate commit messages if they are too long
max_length=1900 # Adjust this value as needed
if [ ${#commit_messages} -gt $max_length ]; then
commit_messages="${commit_messages:0:$max_length}... (truncated)"
fi
contentbody=$( jq -nc --arg msg "Alpha-Build: <@714249925248024617> **$VERSION**:" --arg commits "$commit_messages" '{"content": ($msg + "\n" + $commits)}' )
contentbody=$( jq -nc --arg msg "Alpha-Build: <@&1225347048321191996> **$VERSION**:" --arg commits "$commit_messages" '{"content": ($msg + "\n" + $commits)}' )
curl -F "payload_json=${contentbody}" -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }}
#Telegram
curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \
-F "document=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" \
-F "caption=[Alpha-Build: ${VERSION}] Change logs :${commit_messages}" \
-F "caption=Alpha-Build: ${VERSION}: ${commit_messages}" \
https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument
env:
@@ -112,18 +117,13 @@ jobs:
VERSION: ${{ env.VERSION }}
- name: Upload Current SHA as Artifact
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v4
with:
name: last-sha
path: last_sha.txt
- name: Delete Old Pre-Releases
id: delete-pre-releases
uses: sgpublic/delete-release-action@master
- name: Upload Commit log as Artifact
uses: actions/upload-artifact@v4
with:
pre-release-drop: true
pre-release-keep-count: 3
pre-release-drop-tag: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
name: commit-log
path: commit_log.txt

6
.gitignore vendored
View File

@@ -8,6 +8,9 @@ local.properties
# Log/OS Files
*.log
# Secrets
apikey.properties
# Android Studio generated files and folders
captures/
.externalNativeBuild/
@@ -28,3 +31,6 @@ output.json
#other
scripts/
#crowdin
crowdin.yml

View File

@@ -15,15 +15,16 @@ android {
defaultConfig {
applicationId "ani.dantotsu"
minSdk 23
minSdk 21
targetSdk 34
versionCode((System.currentTimeMillis() / 60000).toInteger())
versionName "2.2.0"
versionCode 220000000
versionName "3.0.0"
versionCode 300000000
signingConfig signingConfigs.debug
}
flavorDimensions "store"
flavorDimensions += "store"
productFlavors {
fdroid {
// F-Droid specific configuration
@@ -42,19 +43,22 @@ android {
buildTypes {
alpha {
applicationIdSuffix ".beta" // keep as beta by popular request
versionNameSuffix "-alpha01"
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_alpha", icon_placeholder_round: "@mipmap/ic_launcher_alpha_round"]
versionNameSuffix "-alpha01-" + gitCommitHash
manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher_alpha"
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_alpha_round"
debuggable System.getenv("CI") == null
isDefault true
}
debug {
applicationIdSuffix ".beta"
versionNameSuffix "-beta01"
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_beta", icon_placeholder_round: "@mipmap/ic_launcher_beta_round"]
debuggable System.getenv("CI") == null
versionNameSuffix "-beta04"
manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher_beta"
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_beta_round"
debuggable false
}
release {
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher", icon_placeholder_round: "@mipmap/ic_launcher_round"]
manifestPlaceholders.icon_placeholder = "@mipmap/ic_launcher"
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_round"
debuggable false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-gson.pro', 'proguard-rules.pro'
}
@@ -76,28 +80,29 @@ android {
dependencies {
// FireBase
googleImplementation platform('com.google.firebase:firebase-bom:32.2.3')
googleImplementation 'com.google.firebase:firebase-analytics-ktx:21.5.0'
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.1'
// Core
// FireBase
googleImplementation platform('com.google.firebase:firebase-bom:33.0.0')
googleImplementation 'com.google.firebase:firebase-analytics-ktx:22.0.0'
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:19.0.0'
// Core
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.browser:browser:1.7.0'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.fragment:fragment-ktx:1.6.2'
implementation 'androidx.browser:browser:1.8.0'
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.fragment:fragment-ktx:1.7.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.multidex:multidex:2.0.1'
implementation "androidx.work:work-runtime-ktx:2.9.0"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.google.code.gson:gson:2.10'
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.github.Blatzar:NiceHttp:0.4.4'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.webkit:webkit:1.10.0'
implementation 'androidx.webkit:webkit:1.11.0'
implementation "com.anggrayudi:storage:1.5.5"
// Glide
// Glide
ext.glide_version = '4.16.0'
api "com.github.bumptech.glide:glide:$glide_version"
implementation "com.github.bumptech.glide:glide:$glide_version"
@@ -105,33 +110,48 @@ dependencies {
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
implementation 'jp.wasabeef:glide-transformations:4.3.0'
// Exoplayer
ext.exo_version = '1.2.1'
// Exoplayer
ext.exo_version = '1.3.1'
implementation "androidx.media3:media3-exoplayer:$exo_version"
implementation "androidx.media3:media3-ui:$exo_version"
implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
implementation "androidx.media3:media3-exoplayer-dash:$exo_version"
implementation "androidx.media3:media3-datasource-okhttp:$exo_version"
implementation "androidx.media3:media3-session:$exo_version"
//media3 casting
// Media3 Casting
implementation "androidx.media3:media3-cast:$exo_version"
implementation "androidx.mediarouter:mediarouter:1.6.0"
implementation "androidx.mediarouter:mediarouter:1.7.0"
// UI
implementation 'com.google.android.material:material:1.11.0'
implementation 'nl.joery.animatedbottombar:library:1.1.0'
implementation 'io.noties.markwon:core:4.6.2'
// UI
implementation 'com.google.android.material:material:1.12.0'
implementation 'com.github.RepoDevil:AnimatedBottomBar:7fcb9af'
implementation 'com.flaviofaria:kenburnsview:1.0.7'
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
implementation 'com.alexvasilkov:gesture-views:2.8.3'
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'
implementation 'com.github.AAChartModel:AAChartCore-Kotlin:7.2.1'
// string matching
// Markwon
ext.markwon_version = '4.6.2'
implementation "io.noties.markwon:core:$markwon_version"
implementation "io.noties.markwon:editor:$markwon_version"
implementation "io.noties.markwon:ext-strikethrough:$markwon_version"
implementation "io.noties.markwon:ext-tables:$markwon_version"
implementation "io.noties.markwon:ext-tasklist:$markwon_version"
implementation "io.noties.markwon:html:$markwon_version"
implementation "io.noties.markwon:image-glide:$markwon_version"
// Groupie
ext.groupie_version = '2.10.1'
implementation "com.github.lisawray.groupie:groupie:$groupie_version"
implementation "com.github.lisawray.groupie:groupie-viewbinding:$groupie_version"
// String Matching
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
// Aniyomi
// Aniyomi
implementation 'io.reactivex:rxjava:1.3.8'
implementation 'io.reactivex:rxandroid:1.2.1'
implementation 'ru.beryukhov:flowreactivenetwork:1.0.4'
@@ -141,11 +161,10 @@ dependencies {
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.12'
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'
implementation 'com.squareup.okio:okio:3.7.0'
implementation 'com.squareup.okio:okio:3.8.0'
implementation 'com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.12'
implementation 'ch.acra:acra-http:5.11.3'
implementation 'org.jsoup:jsoup:1.15.4'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.2'
implementation 'org.jsoup:jsoup:1.16.1'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.3'
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
implementation 'com.github.tachiyomiorg:unifile:17bec43'
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'

View File

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

View File

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

View File

@@ -11,13 +11,21 @@ import android.net.Uri
import android.os.Environment
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.content.getSystemService
import androidx.fragment.app.FragmentActivity
import ani.dantotsu.*
import ani.dantotsu.BuildConfig
import ani.dantotsu.Mapper
import ani.dantotsu.R
import ani.dantotsu.buildMarkwon
import ani.dantotsu.client
import ani.dantotsu.currContext
import ani.dantotsu.logError
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.settings.saving.PrefManager
import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin
import ani.dantotsu.snackString
import ani.dantotsu.toast
import ani.dantotsu.tryWithSuspend
import ani.dantotsu.util.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
@@ -25,9 +33,8 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.decodeFromJsonElement
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import java.util.Locale
object AppUpdater {
suspend fun check(activity: FragmentActivity, post: Boolean = false) {
@@ -39,9 +46,10 @@ object AppUpdater {
.parsed<JsonArray>().map {
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
}
val r = res.filter { it.prerelease }.maxByOrNull {
it.timeStamp()
} ?: throw Exception("No Pre Release Found")
val r = res.filter { it.prerelease }.filter { !it.tagName.contains("fdroid") }
.maxByOrNull {
it.timeStamp()
} ?: throw Exception("No Pre Release Found")
val v = r.tagName.substringAfter("v", "")
(r.body ?: "") to v.ifEmpty { throw Exception("Weird Version : ${r.tagName}") }
} else {
@@ -50,7 +58,7 @@ object AppUpdater {
res to res.substringAfter("# ").substringBefore("\n")
}
logger("Git Version : $version")
Logger.log("Git Version : $version")
val dontShow = PrefManager.getCustomVal("dont_ask_for_update_$version", false)
if (compareVersion(version) && !dontShow && !activity.isDestroyed) activity.runOnUiThread {
CustomBottomDialog.newInstance().apply {
@@ -61,8 +69,7 @@ object AppUpdater {
)
addView(
TextView(activity).apply {
val markWon = Markwon.builder(activity)
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
val markWon = buildMarkwon(activity, false)
markWon.setMarkdown(this, md)
}
)
@@ -78,13 +85,18 @@ object AppUpdater {
setPositiveButton(currContext()!!.getString(R.string.lets_go)) {
MainScope().launch(Dispatchers.IO) {
try {
client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
.parsed<GithubResponse>().assets?.find {
it.browserDownloadURL.endsWith("apk")
}?.browserDownloadURL.apply {
if (this != null) activity.downloadUpdate(version, this)
else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
}
val apks =
client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
.parsed<GithubResponse>().assets?.filter {
it.browserDownloadURL.endsWith(
".apk"
)
}
val apkToDownload = apks?.first()
apkToDownload?.browserDownloadURL.apply {
if (this != null) activity.downloadUpdate(version, this)
else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
}
} catch (e: Exception) {
logError(e)
}
@@ -104,24 +116,25 @@ object AppUpdater {
}
private fun compareVersion(version: String): Boolean {
return when (BuildConfig.BUILD_TYPE) {
"debug" -> BuildConfig.VERSION_NAME != version
"alpha" -> false
else -> {
fun toDouble(list: List<String>): Double {
return list.mapIndexed { i: Int, s: String ->
when (i) {
0 -> s.toDouble() * 100
1 -> s.toDouble() * 10
2 -> s.toDouble()
else -> s.toDoubleOrNull() ?: 0.0
}
}.sum()
}
if (BuildConfig.DEBUG) {
return BuildConfig.VERSION_NAME != version
} else {
fun toDouble(list: List<String>): Double {
return list.mapIndexed { i: Int, s: String ->
when (i) {
0 -> s.toDouble() * 100
1 -> s.toDouble() * 10
2 -> s.toDouble()
else -> s.toDoubleOrNull() ?: 0.0
}
}.sum()
val new = toDouble(version.split("."))
val curr = toDouble(BuildConfig.VERSION_NAME.split("."))
new > curr
}
val new = toDouble(version.split("."))
val curr = toDouble(BuildConfig.VERSION_NAME.split("."))
return new > curr
}
}
@@ -161,21 +174,8 @@ object AppUpdater {
DownloadManager.EXTRA_DOWNLOAD_ID, id
) ?: id
val query = DownloadManager.Query()
query.setFilterById(downloadId)
val c = downloadManager.query(query)
if (c.moveToFirst()) {
val columnIndex = c.getColumnIndex(DownloadManager.COLUMN_STATUS)
if (DownloadManager.STATUS_SUCCESSFUL == c
.getInt(columnIndex)
) {
c.getColumnIndex(DownloadManager.COLUMN_MEDIAPROVIDER_URI)
val uri = Uri.parse(
c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
)
openApk(this@downloadUpdate, uri)
}
downloadManager.getUriForDownloadedFile(downloadId)?.let {
openApk(this@downloadUpdate, it)
}
} catch (e: Exception) {
logError(e)
@@ -190,16 +190,11 @@ object AppUpdater {
private fun openApk(context: Context, uri: Uri) {
try {
uri.path?.let {
val contentUri = FileProvider.getUriForFile(
context,
BuildConfig.APPLICATION_ID + ".provider",
File(it)
)
val installIntent = Intent(Intent.ACTION_VIEW).apply {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
data = contentUri
data = uri
}
context.startActivity(installIntent)
}

View File

@@ -2,6 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-sdk tools:overrideLibrary="go.server.gojni" />
<uses-feature
android:name="android.software.leanback"
android:required="false" />
@@ -16,10 +18,10 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
android:maxSdkVersion="29" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" /> <!-- For background jobs -->
@@ -38,6 +40,17 @@
android:name="android.permission.READ_APP_SPECIFIC_LOCALES"
tools:ignore="ProtectedPermissions" />
<!-- ExoPlayer: Bluetooth Headsets -->
<uses-feature
android:name="android.hardware.bluetooth"
android:required="false" />
<uses-permission
android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- ExoPlayer: Bluetooth Headsets -->
<queries>
<package android:name="idm.internet.download.manager.plus" />
<package android:name="idm.internet.download.manager" />
@@ -49,6 +62,7 @@
android:name=".App"
android:allowBackup="true"
android:banner="@mipmap/ic_banner_foreground"
android:enableOnBackInvokedCallback="true"
android:icon="${icon_placeholder}"
android:label="@string/app_name"
android:largeHeap="true"
@@ -57,9 +71,30 @@
android:supportsRtl="true"
android:theme="@style/Theme.Dantotsu"
android:usesCleartextTraffic="true"
tools:ignore="AllowBackup">
tools:ignore="AllowBackup"
tools:targetApi="tiramisu">
<receiver
android:name=".widgets.CurrentlyAiringWidget"
android:name=".widgets.upcoming.UpcomingWidget"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/upcoming_widget_info" />
</receiver>
<activity
android:name=".widgets.upcoming.UpcomingWidgetConfigure"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<receiver
android:name=".widgets.statistics.ProfileStatsWidget"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
@@ -67,10 +102,9 @@
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/currently_airing_widget_info" />
android:resource="@xml/statistics_widget_info" />
</receiver>
<receiver android:name=".subcriptions.NotificationClickReceiver" />
<receiver android:name=".notifications.IncognitoNotificationClickReceiver" />
<activity
android:name=".media.novel.novelreader.NovelReaderActivity"
@@ -102,8 +136,64 @@
<activity
android:name=".settings.SettingsActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsAboutActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".home.status.StatusActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsAccountActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsAnimeActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsCommonActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsExtensionsActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsAddonActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsMangaActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsNotificationActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsThemeActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.ExtensionsActivity"
android:parentActivityName=".MainActivity"
android:windowSoftInputMode="adjustResize|stateHidden" />
<activity
android:name=".widgets.statistics.ProfileStatsConfigure"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<activity
android:name=".profile.ProfileActivity"
android:parentActivityName=".MainActivity"
android:windowSoftInputMode="adjustResize|stateHidden" />
<activity
android:name=".profile.FollowActivity"
android:parentActivityName=".MainActivity"
android:windowSoftInputMode="adjustResize|stateHidden" />
<activity
android:name=".profile.activity.FeedActivity"
android:configChanges="orientation|screenSize|screenLayout"
android:label="Inbox Activity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".profile.activity.NotificationActivity"
android:label="Inbox Activity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".others.imagesearch.ImageSearchActivity"
@@ -117,6 +207,9 @@
android:name=".media.CalendarActivity"
android:parentActivityName=".MainActivity" />
<activity android:name=".media.user.ListActivity" />
<activity
android:name=".profile.SingleStatActivity"
android:parentActivityName=".profile.ProfileActivity" />
<activity
android:name=".media.manga.mangareader.MangaReaderActivity"
android:excludeFromRecents="true"
@@ -124,12 +217,21 @@
android:label="@string/manga"
android:launchMode="singleTask" />
<activity android:name=".media.GenreActivity" />
<activity
android:name=".media.MediaListViewActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".media.MediaDetailsActivity"
android:parentActivityName=".MainActivity"
android:theme="@style/Theme.Dantotsu.NeverCutout" />
android:theme="@style/Theme.Dantotsu.NeverCutout"
android:windowSoftInputMode="adjustResize|stateHidden" />
<activity android:name=".media.CharacterDetailsActivity" />
<activity android:name=".home.NoInternet" />
<activity android:name=".others.CrashActivity"
android:excludeFromRecents="true"
android:exported="true"
android:process=":error_process"
android:launchMode="singleTask" />
<activity
android:name=".media.anime.ExoplayerView"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden|navigation"
@@ -239,6 +341,17 @@
<data android:host="myanimelist.net" />
<data android:pathPrefix="/anime" />
</intent-filter>
<intent-filter android:label="@string/view_profile_in_dantotsu">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="anilist.co" />
<data android:pathPrefix="/user" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
@@ -254,24 +367,40 @@
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="content" />
<data android:mimeType="*/*" />
<data android:pathPattern=".*\\.ani" />
<data android:pathPattern=".*\\.sani" />
<data android:host="*" />
</intent-filter>
</activity>
<activity
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallActivity"
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallActivity"
android:exported="false"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallActivity"
android:exported="false"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
android:theme="@style/Theme.AppCompat" />
<receiver
android:name=".subcriptions.AlarmReceiver"
android:name=".notifications.AlarmPermissionStateReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.app.action.SCHEDULE_EXACT_ALARM_PERMISSION_STATE_CHANGED" />
</intent-filter>
</receiver>
<receiver
android:name=".notifications.BootCompletedReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="Aani.dantotsu.ACTION_ALARM" />
</intent-filter>
</receiver>
<receiver android:name=".notifications.anilist.AnilistNotificationReceiver" />
<receiver android:name=".notifications.comment.CommentNotificationReceiver" />
<receiver android:name=".notifications.subscription.SubscriptionNotificationReceiver" />
<meta-data
android:name="preloaded_fonts"
@@ -289,25 +418,11 @@
</provider>
<service
android:name=".widgets.CurrentlyAiringRemoteViewsService"
android:name=".widgets.upcoming.UpcomingRemoteViewsService"
android:exported="true"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<service
android:name=".download.video.ExoplayerDownloadService"
android:exported="false"
android:foregroundServiceType="dataSync">
<intent-filter>
<action android:name="androidx.media3.exoplayer.downloadService.action.RESTART" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
<service
android:name="eu.kanade.tachiyomi.extension.manga.util.MangaExtensionInstallService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service
android:name="eu.kanade.tachiyomi.extension.anime.util.AnimeExtensionInstallService"
android:name="eu.kanade.tachiyomi.extension.util.ExtensionInstallService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service
@@ -330,6 +445,11 @@
android:name="androidx.media3.exoplayer.scheduler.PlatformScheduler$PlatformSchedulerService"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" />
<service
android:name=".addons.torrent.TorrentServerService"
android:exported="false"
android:foregroundServiceType="dataSync"
android:stopWithTask="true" />
<meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"

View File

@@ -4,11 +4,16 @@ import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.os.Bundle
import android.util.Log
import androidx.multidex.MultiDex
import androidx.multidex.MultiDexApplication
import ani.dantotsu.addons.download.DownloadAddonManager
import ani.dantotsu.addons.torrent.TorrentAddonManager
import ani.dantotsu.aniyomi.anime.custom.AppModule
import ani.dantotsu.aniyomi.anime.custom.PreferenceModule
import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.notifications.TaskScheduler
import ani.dantotsu.others.DisabledReports
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.MangaSources
@@ -17,20 +22,24 @@ import ani.dantotsu.parsers.novel.NovelExtensionManager
import ani.dantotsu.settings.SettingsActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.util.FinalExceptionHandler
import ani.dantotsu.util.Logger
import com.google.android.material.color.DynamicColors
import 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.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import logcat.AndroidLogcatLogger
import logcat.LogPriority
import logcat.LogcatLogger
import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.lang.IllegalStateException
@SuppressLint("StaticFieldLeak")
@@ -38,6 +47,9 @@ class App : MultiDexApplication() {
private lateinit var animeExtensionManager: AnimeExtensionManager
private lateinit var mangaExtensionManager: MangaExtensionManager
private lateinit var novelExtensionManager: NovelExtensionManager
private lateinit var torrentAddonManager: TorrentAddonManager
private lateinit var downloadAddonManager: DownloadAddonManager
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
MultiDex.install(this)
@@ -49,6 +61,7 @@ class App : MultiDexApplication() {
val mFTActivityLifecycleCallbacks = FTActivityLifecycleCallbacks()
@OptIn(DelicateCoroutinesApi::class)
override fun onCreate() {
super.onCreate()
@@ -79,45 +92,59 @@ class App : MultiDexApplication() {
}
crashlytics.setCustomKey("device Info", SettingsActivity.getDeviceInfo())
Logger.init(this)
Thread.setDefaultUncaughtExceptionHandler(FinalExceptionHandler())
Logger.log(Log.WARN, "App: Logging started")
initializeNetwork(baseContext)
initializeNetwork()
setupNotificationChannels()
if (!LogcatLogger.isInstalled) {
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
}
animeExtensionManager = Injekt.get()
mangaExtensionManager = Injekt.get()
novelExtensionManager = Injekt.get()
val animeScope = CoroutineScope(Dispatchers.Default)
animeScope.launch {
CoroutineScope(Dispatchers.IO).launch {
animeExtensionManager = Injekt.get()
animeExtensionManager.findAvailableExtensions()
logger("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
Logger.log("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
AnimeSources.init(animeExtensionManager.installedExtensionsFlow)
}
val mangaScope = CoroutineScope(Dispatchers.Default)
mangaScope.launch {
CoroutineScope(Dispatchers.IO).launch {
mangaExtensionManager = Injekt.get()
mangaExtensionManager.findAvailableExtensions()
logger("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
Logger.log("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
MangaSources.init(mangaExtensionManager.installedExtensionsFlow)
}
val novelScope = CoroutineScope(Dispatchers.Default)
novelScope.launch {
CoroutineScope(Dispatchers.IO).launch {
novelExtensionManager = Injekt.get()
novelExtensionManager.findAvailableExtensions()
logger("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
Logger.log("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
NovelSources.init(novelExtensionManager.installedExtensionsFlow)
}
}
GlobalScope.launch {
torrentAddonManager = Injekt.get()
downloadAddonManager = Injekt.get()
torrentAddonManager.init()
downloadAddonManager.init()
CommentsAPI.fetchAuthToken(this@App)
val useAlarmManager = PrefManager.getVal<Boolean>(PrefName.UseAlarmManager)
val scheduler = TaskScheduler.create(this@App, useAlarmManager)
try {
scheduler.scheduleAllTasks(this@App)
} catch (e: IllegalStateException) {
Logger.log("Failed to schedule tasks")
Logger.log(e)
}
}
}
private fun setupNotificationChannels() {
try {
Notifications.createChannels(this)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Failed to modify notification channels" }
Logger.log("Failed to modify notification channels")
Logger.log(e)
}
}
@@ -139,7 +166,11 @@ class App : MultiDexApplication() {
}
companion object {
private var instance: App? = null
var instance: App? = null
/** Reference to the application context.
*
* USE WITH EXTREME CAUTION!**/
var context: Context? = null
fun currentContext(): Context? {
return instance?.mFTActivityLifecycleCallbacks?.currentActivity ?: context

View File

@@ -1,13 +1,16 @@
package ani.dantotsu
import android.Manifest
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.app.Activity
import android.app.DatePickerDialog
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.ActivityNotFoundException
import android.content.ClipData
import android.content.ClipboardManager
import android.content.ComponentName
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
@@ -16,32 +19,71 @@ import android.content.res.Configuration
import android.content.res.Resources.getSystem
import android.graphics.Bitmap
import android.graphics.Color
import android.Manifest
import android.graphics.drawable.Drawable
import android.media.MediaScannerConnection
import android.net.ConnectivityManager
import android.net.NetworkCapabilities.*
import android.net.NetworkCapabilities.TRANSPORT_BLUETOOTH
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
import android.net.NetworkCapabilities.TRANSPORT_LOWPAN
import android.net.NetworkCapabilities.TRANSPORT_USB
import android.net.NetworkCapabilities.TRANSPORT_VPN
import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE
import android.net.Uri
import android.os.*
import android.os.Build
import android.os.Bundle
import android.os.CountDownTimer
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.os.PowerManager
import android.os.SystemClock
import android.provider.Settings
import android.telephony.TelephonyManager
import android.text.InputFilter
import android.text.Spanned
import android.util.AttributeSet
import android.util.TypedValue
import android.view.*
import android.view.GestureDetector
import android.view.Gravity
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewAnimationUtils
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.view.animation.*
import android.widget.*
import androidx.appcompat.app.AppCompatActivity
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.animation.AnimationSet
import android.view.animation.OvershootInterpolator
import android.view.animation.ScaleAnimation
import android.view.animation.TranslateAnimation
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.DatePicker
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getSystemService
import androidx.core.content.FileProvider
import androidx.core.math.MathUtils.clamp
import androidx.core.view.*
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.RecyclerView
@@ -49,18 +91,31 @@ import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.BuildConfig.APPLICATION_ID
import ani.dantotsu.connections.anilist.Genre
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.bakaupdates.MangaUpdates
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.databinding.ItemCountDownBinding
import ani.dantotsu.media.Media
import ani.dantotsu.notifications.IncognitoNotificationClickReceiver
import ani.dantotsu.others.SpoilerPlugin
import ani.dantotsu.parsers.ShowResponse
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt
import ani.dantotsu.subcriptions.NotificationClickReceiver
import ani.dantotsu.util.CountUpTimer
import ani.dantotsu.util.Logger
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.RequestManager
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade
import com.bumptech.glide.load.resource.gif.GifDrawable
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.Target
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.bottomsheet.BottomSheetBehavior
@@ -68,15 +123,40 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.internal.ViewUtils
import com.google.android.material.snackbar.Snackbar
import eu.kanade.tachiyomi.data.notification.Notifications
import kotlinx.coroutines.*
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonConfiguration
import io.noties.markwon.SoftBreakAddsNewLinePlugin
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin
import io.noties.markwon.ext.tables.TablePlugin
import io.noties.markwon.ext.tasklist.TaskListPlugin
import io.noties.markwon.html.HtmlPlugin
import io.noties.markwon.html.TagHandlerNoOp
import io.noties.markwon.image.AsyncDrawable
import io.noties.markwon.image.glide.GlideImagesPlugin
import jp.wasabeef.glide.transformations.BlurTransformation
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import nl.joery.animatedbottombar.AnimatedBottomBar
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.*
import java.lang.Runnable
import java.io.File
import java.io.FileOutputStream
import java.io.OutputStream
import java.lang.reflect.Field
import java.util.*
import kotlin.math.*
import java.util.Calendar
import java.util.TimeZone
import java.util.Timer
import java.util.TimerTask
import kotlin.collections.set
import kotlin.math.log2
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
var statusBarHeight = 0
@@ -108,11 +188,10 @@ fun currActivity(): Activity? {
var loadMedia: Int? = null
var loadIsMAL = false
fun logger(e: Any?, print: Boolean = true) {
if (print)
println(e)
}
val Int.toPx
get() = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), getSystem().displayMetrics
).toInt()
fun initActivity(a: Activity) {
val window = a.window
@@ -132,11 +211,17 @@ fun initActivity(a: Activity) {
if (navBarHeight == 0) {
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
?.apply {
navBarHeight = this.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
navBarHeight = this.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) navBarHeight += 48.toPx
}
}
a.hideStatusBar()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && statusBarHeight == 0 && a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
WindowInsetsControllerCompat(
window,
window.decorView
).hide(WindowInsetsCompat.Type.statusBars())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && statusBarHeight == 0
&& a.resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
) {
window.decorView.rootWindowInsets?.displayCutout?.apply {
if (boundingRects.size > 0) {
statusBarHeight = min(boundingRects[0].width(), boundingRects[0].height())
@@ -148,47 +233,117 @@ fun initActivity(a: Activity) {
val windowInsets =
ViewCompat.getRootWindowInsets(window.decorView.findViewById(android.R.id.content))
if (windowInsets != null) {
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
statusBarHeight = insets.top
navBarHeight = insets.bottom
statusBarHeight = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()).top
navBarHeight =
windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) navBarHeight += 48.toPx
}
}
if (a !is MainActivity) a.setNavigationTheme()
}
@Suppress("DEPRECATION")
fun Activity.hideSystemBars() {
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
)
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
controller.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
controller.hide(WindowInsetsCompat.Type.systemBars())
}
}
@Suppress("DEPRECATION")
fun Activity.hideStatusBar() {
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
fun Activity.hideSystemBarsExtendView() {
WindowCompat.setDecorFitsSystemWindows(window, false)
hideSystemBars()
}
fun Activity.showSystemBars() {
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
controller.show(WindowInsetsCompat.Type.systemBars())
}
}
fun Activity.showSystemBarsRetractView() {
WindowCompat.setDecorFitsSystemWindows(window, true)
showSystemBars()
}
fun Activity.setNavigationTheme() {
val tv = TypedValue()
theme.resolveAttribute(android.R.attr.colorBackground, tv, true)
if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && tv.isColorType)
|| (tv.type >= TypedValue.TYPE_FIRST_COLOR_INT && tv.type <= TypedValue.TYPE_LAST_COLOR_INT)
) {
window.navigationBarColor = tv.data
}
}
/**
* Sets clipToPadding false and sets the combined height of navigation bars as bottom padding.
*
* When nesting multiple scrolling views, only call this method on the inner most scrolling view.
*/
fun ViewGroup.setBaseline(navBar: AnimatedBottomBar) {
navBar.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
clipToPadding = false
setPadding(paddingLeft, paddingTop, paddingRight, navBarHeight + navBar.measuredHeight)
}
/**
* Sets clipToPadding false and sets the combined height of navigation bars as bottom padding.
*
* When nesting multiple scrolling views, only call this method on the inner most scrolling view.
*/
fun ViewGroup.setBaseline(navBar: AnimatedBottomBar, overlayView: View) {
navBar.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
overlayView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
clipToPadding = false
setPadding(
paddingLeft,
paddingTop,
paddingRight,
navBarHeight + navBar.measuredHeight + overlayView.measuredHeight
)
}
fun Activity.reloadActivity() {
Refresh.all()
finish()
startActivity(Intent(this, this::class.java))
initActivity(this)
}
fun Activity.restartApp() {
val mainIntent = Intent.makeRestartActivityTask(
packageManager.getLaunchIntentForPackage(this.packageName)!!.component
)
val component =
ComponentName(this@restartApp.packageName, this@restartApp::class.qualifiedName!!)
try {
startActivity(Intent().setComponent(component))
} catch (e: Exception) {
startActivity(mainIntent)
}
finishAndRemoveTask()
PrefManager.setCustomVal("reload", true)
}
open class BottomSheetDialogFragment : BottomSheetDialogFragment() {
override fun 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) {
val behavior = BottomSheetBehavior.from(requireView().parent as View)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
dialog?.window?.let { window ->
WindowCompat.setDecorFitsSystemWindows(window, false)
val immersiveMode: Boolean = PrefManager.getVal(PrefName.ImmersiveMode)
if (immersiveMode) {
WindowInsetsControllerCompat(
window, window.decorView
).hide(WindowInsetsCompat.Type.statusBars())
}
if (this.resources.configuration.orientation != Configuration.ORIENTATION_PORTRAIT) {
val behavior = BottomSheetBehavior.from(requireView().parent as View)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
window.navigationBarColor = requireContext().getThemeColor(com.google.android.material.R.attr.colorSurface)
}
val typedValue = TypedValue()
val theme = requireContext().theme
theme.resolveAttribute(
com.google.android.material.R.attr.colorSurface,
typedValue,
true
)
window.navigationBarColor = typedValue.data
}
override fun show(manager: FragmentManager, tag: String?) {
@@ -202,21 +357,35 @@ fun isOnline(context: Context): Boolean {
val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
return tryWith {
val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
return@tryWith if (cap != null) {
when {
cap.hasTransport(TRANSPORT_BLUETOOTH) ||
cap.hasTransport(TRANSPORT_CELLULAR) ||
cap.hasTransport(TRANSPORT_ETHERNET) ||
cap.hasTransport(TRANSPORT_LOWPAN) ||
cap.hasTransport(TRANSPORT_USB) ||
cap.hasTransport(TRANSPORT_VPN) ||
cap.hasTransport(TRANSPORT_WIFI) ||
cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val cap = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
return@tryWith if (cap != null) {
when {
cap.hasTransport(TRANSPORT_BLUETOOTH) ||
cap.hasTransport(TRANSPORT_CELLULAR) ||
cap.hasTransport(TRANSPORT_ETHERNET) ||
cap.hasTransport(TRANSPORT_LOWPAN) ||
cap.hasTransport(TRANSPORT_USB) ||
cap.hasTransport(TRANSPORT_VPN) ||
cap.hasTransport(TRANSPORT_WIFI) ||
cap.hasTransport(TRANSPORT_WIFI_AWARE) -> true
else -> false
}
} else false
else -> false
}
} else false
} else {
@Suppress("DEPRECATION")
return@tryWith connectivityManager.activeNetworkInfo?.run {
type == ConnectivityManager.TYPE_BLUETOOTH ||
type == ConnectivityManager.TYPE_ETHERNET ||
type == ConnectivityManager.TYPE_MOBILE ||
type == ConnectivityManager.TYPE_MOBILE_DUN ||
type == ConnectivityManager.TYPE_MOBILE_HIPRI ||
type == ConnectivityManager.TYPE_WIFI ||
type == ConnectivityManager.TYPE_WIMAX ||
type == ConnectivityManager.TYPE_VPN
} ?: false
}
} ?: false
}
@@ -248,7 +417,7 @@ class DatePickerFragment(activity: Activity, var date: FuzzyDate = FuzzyDate().g
dialog.setButton(
DialogInterface.BUTTON_NEUTRAL,
activity.getString(R.string.remove)
) { dialog, which ->
) { _, which ->
if (which == DialogInterface.BUTTON_NEUTRAL) {
date = FuzzyDate()
}
@@ -278,12 +447,11 @@ class InputFilterMinMax(
val input = (dest.toString() + source.toString()).toDouble()
if (isInRange(min, max, input)) return null
} catch (nfe: NumberFormatException) {
logger(nfe.stackTraceToString())
Logger.log(nfe)
}
return ""
}
@SuppressLint("SetTextI18n")
private fun isInRange(a: Double, b: Double, c: Double): Boolean {
val statusStrings = currContext()!!.resources.getStringArray(R.array.status_manga)[2]
@@ -296,7 +464,7 @@ class InputFilterMinMax(
}
class ZoomOutPageTransformer() :
class ZoomOutPageTransformer :
ViewPager2.PageTransformer {
override fun transformPage(view: View, position: Float) {
if (position == 0.0f && PrefManager.getVal(PrefName.LayoutAnimations)) {
@@ -449,15 +617,38 @@ fun ImageView.loadImage(url: String?, size: Int = 0) {
}
fun ImageView.loadImage(file: FileUrl?, size: Int = 0) {
file?.url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { file?.url ?: "" }
if (file?.url?.isNotEmpty() == true) {
tryWith {
val glideUrl = GlideUrl(file.url) { file.headers }
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
.into(this)
if (file.url.startsWith("content://")) {
Glide.with(this.context).load(Uri.parse(file.url)).transition(withCrossFade())
.override(size).into(this)
} else {
val glideUrl = GlideUrl(file.url) { file.headers }
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(size)
.into(this)
}
}
}
}
fun ImageView.loadImage(file: FileUrl?, width: Int = 0, height: Int = 0) {
file?.url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { file?.url ?: "" }
if (file?.url?.isNotEmpty() == true) {
tryWith {
if (file.url.startsWith("content://")) {
Glide.with(this.context).load(Uri.parse(file.url)).transition(withCrossFade())
.override(width, height).into(this)
} else {
val glideUrl = GlideUrl(file.url) { file.headers }
Glide.with(this.context).load(glideUrl).transition(withCrossFade()).override(width, height)
.into(this)
}
}
}
}
fun ImageView.loadLocalImage(file: File?, size: Int = 0) {
if (file?.exists() == true) {
tryWith {
@@ -579,9 +770,41 @@ fun View.circularReveal(ex: Int, ey: Int, subX: Boolean, time: Long) {
}
fun openLinkInBrowser(link: String?) {
tryWith {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
currContext()?.startActivity(intent)
link?.let {
try {
val emptyBrowserIntent = Intent(Intent.ACTION_VIEW).apply {
addCategory(Intent.CATEGORY_BROWSABLE)
data = Uri.fromParts("http", "", null)
}
val sendIntent = Intent().apply {
action = Intent.ACTION_VIEW
addCategory(Intent.CATEGORY_BROWSABLE)
data = Uri.parse(link)
selector = emptyBrowserIntent
}
currContext()!!.startActivity(sendIntent)
} catch (e: ActivityNotFoundException) {
snackString("No browser found")
} catch (e: Exception) {
Logger.log(e)
}
}
}
fun openLinkInYouTube(link: String?) {
link?.let {
try {
val videoIntent = Intent(Intent.ACTION_VIEW).apply {
addCategory(Intent.CATEGORY_BROWSABLE)
data = Uri.parse(link)
setPackage("com.google.android.youtube")
}
currContext()!!.startActivity(videoIntent)
} catch (e: ActivityNotFoundException) {
openLinkInBrowser(link)
} catch (e: Exception) {
Logger.log(e)
}
}
}
@@ -676,26 +899,6 @@ fun savePrefs(
}
}
fun downloadsPermission(activity: AppCompatActivity): Boolean {
val permissions = arrayOf(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
val requiredPermissions = permissions.filter {
ContextCompat.checkSelfPermission(activity, it) != PackageManager.PERMISSION_GRANTED
}.toTypedArray()
return if (requiredPermissions.isNotEmpty()) {
ActivityCompat.requestPermissions(activity, requiredPermissions, DOWNLOADS_PERMISSION_REQUEST_CODE)
false
} else {
true
}
}
private const val DOWNLOADS_PERMISSION_REQUEST_CODE = 100
fun shareImage(title: String, bitmap: Bitmap, context: Context) {
val contentUri = FileProvider.getUriForFile(
@@ -728,7 +931,7 @@ fun saveImage(image: Bitmap, path: String, imageFileName: String): File? {
private fun scanFile(path: String, context: Context) {
MediaScannerConnection.scanFile(context, arrayOf(path), null) { p, _ ->
logger("Finished scanning $p")
Logger.log("Finished scanning $p")
}
}
@@ -760,12 +963,15 @@ fun copyToClipboard(string: String, toast: Boolean = true) {
val clipboard = getSystemService(activity, ClipboardManager::class.java)
val clip = ClipData.newPlainText("label", string)
clipboard?.setPrimaryClip(clip)
if (toast) snackString(activity.getString(R.string.copied_text, string))
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
if (toast) snackString(activity.getString(R.string.copied_text, string))
}
}
@SuppressLint("SetTextI18n")
fun countDown(media: Media, view: ViewGroup) {
if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null && (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 28.toLong()) {
if (media.anime?.nextAiringEpisode != null && media.anime.nextAiringEpisodeTime != null
&& (media.anime.nextAiringEpisodeTime!! - System.currentTimeMillis() / 1000) <= 86400 * 28.toLong()
) {
val v = ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
view.addView(v.root, 0)
v.mediaCountdownText.text =
@@ -797,6 +1003,50 @@ fun countDown(media: Media, view: ViewGroup) {
}
}
fun sinceWhen(media: Media, view: ViewGroup) {
if (media.status != "RELEASING" && media.status != "HIATUS") return
CoroutineScope(Dispatchers.IO).launch {
MangaUpdates().search(media.mangaName(), media.startDate)?.let {
val latestChapter = MangaUpdates.getLatestChapter(view.context, it)
val timeSince = (System.currentTimeMillis() -
(it.metadata.series.lastUpdated!!.timestamp * 1000)) / 1000
withContext(Dispatchers.Main) {
val v =
ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
view.addView(v.root, 0)
v.mediaCountdownText.text =
currActivity()?.getString(R.string.chapter_release_timeout, latestChapter)
object : CountUpTimer(86400000) {
override fun onTick(second: Int) {
val a = second + timeSince
v.mediaCountdown.text = currActivity()?.getString(
R.string.time_format,
a / 86400,
a % 86400 / 3600,
a % 86400 % 3600 / 60,
a % 86400 % 3600 % 60
)
}
override fun onFinish() {
// The legend will never die.
}
}.start()
}
}
}
}
fun displayTimer(media: Media, view: ViewGroup) {
when {
media.anime != null -> countDown(media, view)
media.format == "MANGA" || media.format == "ONE_SHOT" -> sinceWhen(media, view)
else -> {} // No timer yet
}
}
fun MutableMap<String, Genre>.checkId(id: Int): Boolean {
this.forEach {
if (it.value.id == id) {
@@ -866,9 +1116,13 @@ class EmptyAdapter(private val count: Int) : RecyclerView.Adapter<RecyclerView.V
inner class EmptyViewHolder(view: View) : RecyclerView.ViewHolder(view)
}
fun getAppString(res: Int): String {
return currContext()?.getString(res) ?: ""
}
fun toast(string: String?) {
if (string != null) {
logger(string)
Logger.log(string)
MainScope().launch {
Toast.makeText(currActivity()?.application ?: return@launch, string, Toast.LENGTH_SHORT)
.show()
@@ -876,16 +1130,20 @@ fun toast(string: String?) {
}
}
fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null) {
fun toast(res: Int) {
toast(getAppString(res))
}
fun snackString(s: String?, activity: Activity? = null, clipboard: String? = null): Snackbar? {
try { //I have no idea why this sometimes crashes for some people...
if (s != null) {
(activity ?: currActivity())?.apply {
val snackBar = Snackbar.make(
window.decorView.findViewById(android.R.id.content),
s,
Snackbar.LENGTH_SHORT
)
runOnUiThread {
val snackBar = Snackbar.make(
window.decorView.findViewById(android.R.id.content),
s,
Snackbar.LENGTH_SHORT
)
snackBar.view.apply {
updateLayoutParams<FrameLayout.LayoutParams> {
gravity = (Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM)
@@ -905,13 +1163,19 @@ fun snackString(s: String?, activity: Activity? = null, clipboard: String? = nul
}
snackBar.show()
}
return snackBar
}
logger(s)
Logger.log(s)
}
} catch (e: Exception) {
logger(e.stackTraceToString())
Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e)
}
return null
}
fun snackString(r: Int, activity: Activity? = null, clipboard: String? = null): Snackbar? {
return snackString(getAppString(r), activity, clipboard)
}
open class NoPaddingArrayAdapter<T>(context: Context, layoutId: Int, items: List<T>) :
@@ -1037,7 +1301,7 @@ fun incognitoNotification(context: Context) {
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
if (incognito) {
val intent = Intent(context, NotificationClickReceiver::class.java)
val intent = Intent(context, IncognitoNotificationClickReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
context, 0, intent,
PendingIntent.FLAG_IMMUTABLE
@@ -1055,6 +1319,28 @@ fun incognitoNotification(context: Context) {
}
}
fun hasNotificationPermission(context: Context): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
} else {
NotificationManagerCompat.from(context).areNotificationsEnabled()
}
}
fun openSettings(context: Context, channelId: String?): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val intent = Intent(
if (channelId != null) Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS
else Settings.ACTION_APP_NOTIFICATION_SETTINGS
).apply {
putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
putExtra(Settings.EXTRA_CHANNEL_ID, channelId)
}
context.startActivity(intent)
true
} else false
}
suspend fun View.pop() {
currActivity()?.runOnUiThread {
ObjectAnimator.ofFloat(this@pop, "scaleX", 1f, 1.25f).setDuration(120).start()
@@ -1067,3 +1353,116 @@ suspend fun View.pop() {
}
delay(100)
}
fun blurImage(imageView: ImageView, banner: String?) {
if (banner != null) {
val radius = PrefManager.getVal<Float>(PrefName.BlurRadius).toInt()
val sampling = PrefManager.getVal<Float>(PrefName.BlurSampling).toInt()
val context = imageView.context
if (!(context as Activity).isDestroyed) {
val url = PrefManager.getVal<String>(PrefName.ImageUrl).ifEmpty { banner }
if (PrefManager.getVal(PrefName.BlurBanners)) {
Glide.with(context as Context)
.load(
if (banner.startsWith("http")) GlideUrl(url) else if (banner.startsWith("content://")) Uri.parse(
url
) else File(url)
)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE).override(400)
.apply(RequestOptions.bitmapTransform(BlurTransformation(radius, sampling)))
.into(imageView)
} else {
Glide.with(context as Context)
.load(
if (banner.startsWith("http")) GlideUrl(url) else if (banner.startsWith("content://")) Uri.parse(
url
) else File(url)
)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE).override(400)
.into(imageView)
}
}
} else {
imageView.setImageResource(R.drawable.linear_gradient_bg)
}
}
fun Context.getThemeColor(@AttrRes attribute: Int): Int {
val typedValue = TypedValue()
theme.resolveAttribute(attribute, typedValue, true)
return typedValue.data
}
/**
* Builds the markwon instance with all the plugins
* @return the markwon instance
*/
fun buildMarkwon(
activity: Context,
userInputContent: Boolean = true,
fragment: Fragment? = null
): Markwon {
val glideContext = fragment?.let { Glide.with(it) } ?: Glide.with(activity)
val markwon = Markwon.builder(activity)
.usePlugin(object : AbstractMarkwonPlugin() {
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
builder.linkResolver { _, link ->
copyToClipboard(link, true)
}
}
})
.usePlugin(SoftBreakAddsNewLinePlugin.create())
.usePlugin(StrikethroughPlugin.create())
.usePlugin(TablePlugin.create(activity))
.usePlugin(TaskListPlugin.create(activity))
.usePlugin(SpoilerPlugin())
.usePlugin(HtmlPlugin.create { plugin ->
if (userInputContent) {
plugin.addHandler(
TagHandlerNoOp.create("h1", "h2", "h3", "h4", "h5", "h6", "hr", "pre", "a")
)
}
})
.usePlugin(GlideImagesPlugin.create(object : GlideImagesPlugin.GlideStore {
private val requestManager: RequestManager = glideContext.apply {
addDefaultRequestListener(object : RequestListener<Any> {
override fun onResourceReady(
resource: Any,
model: Any,
target: Target<Any>,
dataSource: DataSource,
isFirstResource: Boolean
): Boolean {
if (resource is GifDrawable) {
resource.start()
}
return false
}
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Any>,
isFirstResource: Boolean
): Boolean {
Logger.log("Image failed to load: $model")
Logger.log(e as Exception)
return false
}
})
}
override fun load(drawable: AsyncDrawable): RequestBuilder<Drawable> {
Logger.log("Loading image: ${drawable.destination}")
return requestManager.load(drawable.destination)
}
override fun cancel(target: Target<*>) {
Logger.log("Cancelling image load")
requestManager.clear(target)
}
}))
.build()
return markwon
}

View File

@@ -2,7 +2,9 @@ package ani.dantotsu
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Intent
import android.content.res.Configuration
import android.graphics.drawable.Animatable
import android.graphics.drawable.GradientDrawable
import android.net.Uri
@@ -11,12 +13,10 @@ import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.view.animation.AnticipateInterpolator
import android.widget.TextView
import android.widget.Toast
import androidx.activity.addCallback
import androidx.activity.viewModels
import androidx.annotation.OptIn
@@ -25,40 +25,54 @@ import androidx.core.animation.doOnEnd
import androidx.core.content.ContextCompat
import androidx.core.view.doOnAttach
import androidx.core.view.updateLayoutParams
import androidx.core.view.updateMargins
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.offline.Download
import androidx.viewpager2.adapter.FragmentStateAdapter
import ani.dantotsu.addons.torrent.TorrentAddonManager
import ani.dantotsu.addons.torrent.TorrentServerService
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
import ani.dantotsu.databinding.ActivityMainBinding
import ani.dantotsu.databinding.DialogUserAgentBinding
import ani.dantotsu.databinding.SplashScreenBinding
import ani.dantotsu.download.video.Helper
import ani.dantotsu.home.AnimeFragment
import ani.dantotsu.home.HomeFragment
import ani.dantotsu.home.LoginFragment
import ani.dantotsu.home.MangaFragment
import ani.dantotsu.home.NoInternet
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.notifications.TaskScheduler
import ani.dantotsu.others.CustomBottomDialog
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.activity.FeedActivity
import ani.dantotsu.profile.activity.NotificationActivity
import ani.dantotsu.settings.ExtensionsActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefManager.asLiveBool
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.settings.saving.SharedPreferenceBooleanLiveData
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferencePackager
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.Logger
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputEditText
import eu.kanade.domain.source.service.SourcePreferences
import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import nl.joery.animatedbottombar.AnimatedBottomBar
import tachiyomi.core.util.lang.launchIO
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.Serializable
@@ -71,6 +85,7 @@ class MainActivity : AppCompatActivity() {
private var load = false
@kotlin.OptIn(DelicateCoroutinesApi::class)
@SuppressLint("InternalInsetResource", "DiscouragedApi")
@OptIn(UnstableApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
@@ -84,17 +99,72 @@ class MainActivity : AppCompatActivity() {
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
TaskScheduler.scheduleSingleWork(this)
val action = intent.action
val type = intent.type
if (Intent.ACTION_VIEW == action && type != null) {
val uri: Uri? = intent.data
try {
if (uri == null) {
throw Exception("Uri is null")
}
val jsonString =
contentResolver.openInputStream(uri)?.readBytes()
?: throw Exception("Error reading file")
val name =
DocumentFile.fromSingleUri(this, uri)?.name ?: "settings"
//.sani is encrypted, .ani is not
if (name.endsWith(".sani")) {
passwordAlertDialog { password ->
if (password != null) {
val salt = jsonString.copyOfRange(0, 16)
val encrypted = jsonString.copyOfRange(16, jsonString.size)
val decryptedJson = try {
PreferenceKeystore.decryptWithPassword(
password,
encrypted,
salt
)
} catch (e: Exception) {
toast("Incorrect password")
return@passwordAlertDialog
}
if (PreferencePackager.unpack(decryptedJson)) {
val intent = Intent(this, this.javaClass)
this.finish()
startActivity(intent)
}
} else {
toast("Password cannot be empty")
}
}
} else if (name.endsWith(".ani")) {
val decryptedJson = jsonString.toString(Charsets.UTF_8)
if (PreferencePackager.unpack(decryptedJson)) {
val intent = Intent(this, this.javaClass)
this.finish()
startActivity(intent)
}
} else {
toast("Invalid file type")
}
} catch (e: Exception) {
e.printStackTrace()
toast("Error importing settings")
}
}
val bottomNavBar = findViewById<AnimatedBottomBar>(R.id.navbar)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val backgroundDrawable = _bottomBar.background as GradientDrawable
val backgroundDrawable = bottomNavBar.background as GradientDrawable
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xF9000000.toInt()
backgroundDrawable.setColor(semiTransparentColor)
_bottomBar.background = backgroundDrawable
bottomNavBar.background = backgroundDrawable
}
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
bottomNavBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
val offset = try {
val statusBarHeightId = resources.getIdentifier("status_bar_height", "dimen", "android")
@@ -144,22 +214,14 @@ class MainActivity : AppCompatActivity() {
finish()
}
doubleBackToExitPressedOnce = true
snackString(this@MainActivity.getString(R.string.back_to_exit))
Handler(Looper.getMainLooper()).postDelayed(
{ doubleBackToExitPressedOnce = false },
2000
)
}
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()
snackString(this@MainActivity.getString(R.string.back_to_exit)).apply {
this?.addCallback(object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
super.onDismissed(transientBottomBar, event)
doubleBackToExitPressedOnce = false
}
})
}
}
binding.root.isMotionEventSplittingEnabled = false
@@ -205,6 +267,17 @@ class MainActivity : AppCompatActivity() {
binding.root.doOnAttach {
initActivity(this)
val preferences: SourcePreferences = Injekt.get()
if (preferences.animeExtensionUpdatesCount()
.get() > 0 || preferences.mangaExtensionUpdatesCount().get() > 0
) {
snackString(R.string.extension_updates_available)
?.setDuration(Snackbar.LENGTH_LONG)
?.setAction(R.string.review) {
startActivity(Intent(this, ExtensionsActivity::class.java))
}
}
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent)
selectedOption = if (fragment != null) {
when (fragment) {
AnimeFragment::class.java.name -> 0
@@ -217,7 +290,40 @@ class MainActivity : AppCompatActivity() {
}
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
}
var launched = false
intent.extras?.let { extras ->
val fragmentToLoad = extras.getString("FRAGMENT_TO_LOAD")
val mediaId = extras.getInt("mediaId", -1)
val commentId = extras.getInt("commentId", -1)
val activityId = extras.getInt("activityId", -1)
if (fragmentToLoad != null && mediaId != -1 && commentId != -1) {
val detailIntent = Intent(this, MediaDetailsActivity::class.java).apply {
putExtra("FRAGMENT_TO_LOAD", fragmentToLoad)
putExtra("mediaId", mediaId)
putExtra("commentId", commentId)
}
launched = true
startActivity(detailIntent)
} else if (fragmentToLoad == "FEED" && activityId != -1) {
val feedIntent = Intent(this, FeedActivity::class.java).apply {
putExtra("FRAGMENT_TO_LOAD", "NOTIFICATIONS")
putExtra("activityId", activityId)
}
launched = true
startActivity(feedIntent)
} else if (fragmentToLoad == "NOTIFICATIONS" && activityId != -1) {
Logger.log("MainActivity, onCreate: $activityId")
val notificationIntent = Intent(this, NotificationActivity::class.java).apply {
putExtra("FRAGMENT_TO_LOAD", "NOTIFICATIONS")
putExtra("activityId", activityId)
}
launched = true
startActivity(notificationIntent)
}
}
val offlineMode: Boolean = PrefManager.getVal(PrefName.OfflineMode)
@@ -230,45 +336,39 @@ class MainActivity : AppCompatActivity() {
startActivity(Intent(this, NoInternet::class.java))
} else {
val model: AnilistHomeViewModel by viewModels()
model.genres.observe(this) { it ->
if (it != null) {
if (it) {
val navbar = binding.includedNavbar.navbar
bottomBar = navbar
navbar.visibility = View.VISIBLE
binding.mainProgressBar.visibility = View.GONE
val mainViewPager = binding.viewpager
mainViewPager.isUserInputEnabled = false
mainViewPager.adapter =
ViewPagerAdapter(supportFragmentManager, lifecycle)
mainViewPager.setPageTransformer(ZoomOutPageTransformer())
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)
mainViewPager.post {
mainViewPager.setCurrentItem(
selectedOption,
false
)
}
} else {
binding.mainProgressBar.visibility = View.GONE
}
val navbar = binding.includedNavbar.navbar
bottomBar = navbar
navbar.visibility = View.VISIBLE
binding.mainProgressBar.visibility = View.GONE
val mainViewPager = binding.viewpager
mainViewPager.isUserInputEnabled = false
mainViewPager.adapter =
ViewPagerAdapter(supportFragmentManager, lifecycle)
mainViewPager.setPageTransformer(ZoomOutPageTransformer())
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)
}
})
if (mainViewPager.currentItem != selectedOption) {
navbar.selectTabAt(selectedOption)
mainViewPager.post {
mainViewPager.setCurrentItem(
selectedOption,
false
)
}
}
//Load Data
if (!load) {
if (!load && !launched) {
scope.launch(Dispatchers.IO) {
model.loadMain(this@MainActivity)
val id = intent.extras?.getInt("mediaId", 0)
@@ -288,8 +388,21 @@ class MainActivity : AppCompatActivity() {
snackString(this@MainActivity.getString(R.string.anilist_not_found))
}
}
delay(500)
startSubscription()
val username = intent.extras?.getString("username")
if (username != null) {
val nameInt = username.toIntOrNull()
if (nameInt != null) {
startActivity(
Intent(this@MainActivity, ProfileActivity::class.java)
.putExtra("userId", nameInt)
)
} else {
startActivity(
Intent(this@MainActivity, ProfileActivity::class.java)
.putExtra("username", username)
)
}
}
}
load = true
}
@@ -326,27 +439,76 @@ 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)
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)
val torrentManager = Injekt.get<TorrentAddonManager>()
fun startTorrent() {
if (torrentManager.isAvailable() && PrefManager.getVal(PrefName.TorrentEnabled)) {
launchIO {
if (!TorrentServerService.isRunning()) {
TorrentServerService.start()
}
}
}
}
if (torrentManager.isInitialized.value == false) {
torrentManager.isInitialized.observe(this) {
if (it) {
startTorrent()
}
}
} else {
startTorrent()
}
}
override fun onRestart() {
super.onRestart()
window.navigationBarColor = ContextCompat.getColor(this, android.R.color.transparent)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
val margin = if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) 8 else 32
val params: ViewGroup.MarginLayoutParams =
binding.includedNavbar.navbar.layoutParams as ViewGroup.MarginLayoutParams
params.updateMargins(bottom = margin.toPx)
}
private fun passwordAlertDialog(callback: (CharArray?) -> Unit) {
val password = CharArray(16).apply { fill('0') }
// Inflate the dialog layout
val dialogView = DialogUserAgentBinding.inflate(layoutInflater)
dialogView.userAgentTextBox.hint = "Password"
dialogView.subtitle.visibility = View.VISIBLE
dialogView.subtitle.text = getString(R.string.enter_password_to_decrypt_file)
val dialog = AlertDialog.Builder(this, R.style.MyPopup)
.setTitle("Enter Password")
.setView(dialogView.root)
.setPositiveButton("OK", null)
.setNegativeButton("Cancel") { dialog, _ ->
password.fill('0')
dialog.dismiss()
callback(null)
}
.create()
dialog.window?.setDimAmount(0.8f)
dialog.show()
// Override the positive button here
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val editText = dialog.findViewById<TextInputEditText>(R.id.userAgentTextBox)
if (editText?.text?.isNotBlank() == true) {
editText.text?.toString()?.trim()?.toCharArray(password)
dialog.dismiss()
callback(password)
} else {
toast("Password cannot be empty")
}
}
}
//ViewPager
private class ViewPagerAdapter(fragmentManager: FragmentManager, lifecycle: Lifecycle) :

View File

@@ -1,14 +1,15 @@
package ani.dantotsu
import android.content.Context
import android.os.Build
import androidx.fragment.app.FragmentActivity
import ani.dantotsu.others.webview.CloudFlare
import ani.dantotsu.others.webview.WebViewBottomDialog
import ani.dantotsu.util.Logger
import com.lagradost.nicehttp.Requests
import com.lagradost.nicehttp.ResponseParser
import com.lagradost.nicehttp.addGenericDns
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkHelper.Companion.defaultUserAgentProvider
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
@@ -34,13 +35,13 @@ lateinit var defaultHeaders: Map<String, String>
lateinit var okHttpClient: OkHttpClient
lateinit var client: Requests
fun initializeNetwork(context: Context) {
fun initializeNetwork() {
val networkHelper = Injekt.get<NetworkHelper>()
defaultHeaders = mapOf(
"User-Agent" to
Injekt.get<NetworkHelper>().defaultUserAgentProvider()
defaultUserAgentProvider()
.format(Build.VERSION.RELEASE, Build.MODEL)
)
@@ -104,6 +105,7 @@ fun logError(e: Throwable, post: Boolean = true, snackbar: Boolean = true) {
toast(e.localizedMessage)
}
e.printStackTrace()
Logger.log(e)
}
fun <T> tryWith(post: Boolean = false, snackbar: Boolean = true, call: () -> T): T? {
@@ -134,7 +136,7 @@ suspend fun <T> tryWithSuspend(
* A url, which can also have headers
* **/
data class FileUrl(
val url: String,
var url: String,
val headers: Map<String, String> = mapOf()
) : Serializable {
companion object {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ fun updateProgress(media: Media, number: String) {
if (Anilist.userid != null) {
CoroutineScope(Dispatchers.IO).launch {
val a = number.toFloatOrNull()?.toInt()
if ((a ?: 0) > (media.userProgress ?: 0)) {
if ((a ?: 0) > (media.userProgress ?: -1)) {
Anilist.mutation.editList(
media.id,
a,

View File

@@ -3,16 +3,17 @@ package ani.dantotsu.connections.anilist
import android.content.ActivityNotFoundException
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.browser.customtabs.CustomTabsIntent
import ani.dantotsu.R
import ani.dantotsu.client
import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.currContext
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.toast
import ani.dantotsu.tryWithSuspend
import ani.dantotsu.util.Logger
import java.util.Calendar
object Anilist {
@@ -27,30 +28,67 @@ object Anilist {
var bg: String? = null
var episodesWatched: Int? = null
var chapterRead: Int? = null
var unreadNotificationCount: Int = 0
var genres: ArrayList<String>? = null
var tags: Map<Boolean, List<String>>? = null
var rateLimitReset: Long = 0
var initialized = false
val sortBy = listOf(
"SCORE_DESC",
"POPULARITY_DESC",
"TRENDING_DESC",
"START_DATE_DESC",
"TITLE_ENGLISH",
"TITLE_ENGLISH_DESC",
"SCORE"
)
val source = listOf(
"ORIGINAL",
"MANGA",
"LIGHT NOVEL",
"VISUAL NOVEL",
"VIDEO GAME",
"OTHER",
"NOVEL",
"DOUJINSHI",
"ANIME",
"WEB NOVEL",
"LIVE ACTION",
"GAME",
"COMIC",
"MULTIMEDIA PROJECT",
"PICTURE BOOK"
)
val animeStatus = listOf(
"FINISHED",
"RELEASING",
"NOT YET RELEASED",
"CANCELLED"
)
val mangaStatus = listOf(
"FINISHED",
"RELEASING",
"NOT YET RELEASED",
"HIATUS",
"CANCELLED"
)
val seasons = listOf(
"WINTER", "SPRING", "SUMMER", "FALL"
)
val anime_formats = listOf(
val animeFormats = listOf(
"TV", "TV SHORT", "MOVIE", "SPECIAL", "OVA", "ONA", "MUSIC"
)
val manga_formats = listOf(
val mangaFormats = listOf(
"MANGA", "NOVEL", "ONE SHOT"
)
@@ -114,6 +152,9 @@ object Anilist {
episodesWatched = null
chapterRead = null
PrefManager.removeVal(PrefName.AnilistToken)
//logout from comments api
CommentsAPI.logout()
}
suspend inline fun <reified T : Any> executeQuery(
@@ -124,7 +165,8 @@ object Anilist {
show: Boolean = false,
cache: Int? = null
): T? {
return tryWithSuspend {
return try {
if (show) Logger.log("Anilist Query: $query")
if (rateLimitReset > System.currentTimeMillis() / 1000) {
toast("Rate limited. Try after ${rateLimitReset - (System.currentTimeMillis() / 1000)} seconds")
throw Exception("Rate limited after ${rateLimitReset - (System.currentTimeMillis() / 1000)} seconds")
@@ -148,7 +190,7 @@ object Anilist {
cacheTime = cache ?: 10
)
val remaining = json.headers["X-RateLimit-Remaining"]?.toIntOrNull() ?: -1
Log.d("AnilistQuery", "Remaining requests: $remaining")
Logger.log("Remaining requests: $remaining")
if (json.code == 429) {
val retry = json.headers["Retry-After"]?.toIntOrNull() ?: -1
val passedLimitReset = json.headers["X-RateLimit-Reset"]?.toLongOrNull() ?: 0
@@ -159,10 +201,15 @@ object Anilist {
toast("Rate limited. Try after $retry seconds")
throw Exception("Rate limited after $retry seconds")
}
if (!json.text.startsWith("{")) throw Exception(currContext()?.getString(R.string.anilist_down))
if (show) println("Response : ${json.text}")
if (!json.text.startsWith("{")) {
throw Exception(currContext()?.getString(R.string.anilist_down))
}
json.parsed()
} else null
} catch (e: Exception) {
if (show) snackString("Error fetching Anilist data: ${e.message}")
Logger.log("Anilist Query Error: ${e.message}")
null
}
}
}

View File

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

View File

@@ -5,45 +5,34 @@ import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import ani.dantotsu.BuildConfig
import ani.dantotsu.R
import ani.dantotsu.connections.discord.Discord
import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.media.Media
import ani.dantotsu.others.AppUpdater
import ani.dantotsu.profile.User
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.tryWithSuspend
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
suspend fun getUserId(context: Context, block: () -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
val token = PrefManager.getVal(PrefName.DiscordToken, null as String?)
val userid = PrefManager.getVal(PrefName.DiscordId, null as String?)
if (userid == null && token != null) {
/*if (!Discord.getUserData())
snackString(context.getString(R.string.error_loading_discord_user_data))*/
//TODO: Discord.getUserData()
}
}
val anilist = if (Anilist.userid == null && Anilist.token != null) {
if (!Anilist.initialized) {
if (Anilist.query.getUserData()) {
tryWithSuspend {
if (MAL.token != null && !MAL.query.getUserData())
snackString(context.getString(R.string.error_loading_mal_user_data))
}
true
} else {
snackString(context.getString(R.string.error_loading_anilist_user_data))
false
}
} else true
if (anilist) block.invoke()
}
block.invoke()
}
class AnilistHomeViewModel : ViewModel() {
@@ -57,65 +46,75 @@ class AnilistHomeViewModel : ViewModel() {
MutableLiveData<ArrayList<Media>>(null)
fun getAnimeContinue(): LiveData<ArrayList<Media>> = animeContinue
suspend fun setAnimeContinue() = animeContinue.postValue(Anilist.query.continueMedia("ANIME"))
private val animeFav: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
suspend fun setAnimeFav() = animeFav.postValue(Anilist.query.favMedia(true))
private val animePlanned: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
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)
fun getMangaContinue(): LiveData<ArrayList<Media>> = mangaContinue
suspend fun setMangaContinue() = mangaContinue.postValue(Anilist.query.continueMedia("MANGA"))
private val mangaFav: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getMangaFav(): LiveData<ArrayList<Media>> = mangaFav
suspend fun setMangaFav() = mangaFav.postValue(Anilist.query.favMedia(false))
private val mangaPlanned: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
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)
fun getRecommendation(): LiveData<ArrayList<Media>> = recommendation
suspend fun setRecommendation() = recommendation.postValue(Anilist.query.recommendations())
private val userStatus: MutableLiveData<ArrayList<User>> =
MutableLiveData<ArrayList<User>>(null)
fun getUserStatus(): LiveData<ArrayList<User>> = userStatus
private val hidden: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getHidden(): LiveData<ArrayList<Media>> = hidden
@Suppress("UNCHECKED_CAST")
suspend fun initHomePage() {
val res = Anilist.query.initHomePage()
res["currentAnime"]?.let { animeContinue.postValue(it) }
res["favoriteAnime"]?.let { animeFav.postValue(it) }
res["plannedAnime"]?.let { animePlanned.postValue(it) }
res["currentManga"]?.let { mangaContinue.postValue(it) }
res["favoriteManga"]?.let { mangaFav.postValue(it) }
res["plannedManga"]?.let { mangaPlanned.postValue(it) }
res["recommendations"]?.let { recommendation.postValue(it) }
res["currentAnime"]?.let { animeContinue.postValue(it as ArrayList<Media>?) }
res["favoriteAnime"]?.let { animeFav.postValue(it as ArrayList<Media>?) }
res["plannedAnime"]?.let { animePlanned.postValue(it as ArrayList<Media>?) }
res["currentManga"]?.let { mangaContinue.postValue(it as ArrayList<Media>?) }
res["favoriteManga"]?.let { mangaFav.postValue(it as ArrayList<Media>?) }
res["plannedManga"]?.let { mangaPlanned.postValue(it as ArrayList<Media>?) }
res["recommendations"]?.let { recommendation.postValue(it as ArrayList<Media>?) }
res["hidden"]?.let { hidden.postValue(it as ArrayList<Media>?) }
res["status"]?.let { userStatus.postValue(it as ArrayList<User>?) }
}
suspend fun loadMain(context: FragmentActivity) {
Anilist.getSavedToken()
MAL.getSavedToken(context)
Discord.getSavedToken(context)
MAL.getSavedToken()
Discord.getSavedToken()
if (!BuildConfig.FLAVOR.contains("fdroid")) {
if (PrefManager.getVal(PrefName.CheckUpdate)) AppUpdater.check(context)
if (PrefManager.getVal(PrefName.CheckUpdate))
context.lifecycleScope.launch(Dispatchers.IO) {
AppUpdater.check(context, false)
}
}
val ret = Anilist.query.getGenresAndTags()
withContext(Dispatchers.Main) {
genres.value = ret
}
genres.postValue(Anilist.query.getGenresAndTags())
}
val empty = MutableLiveData<Boolean>(null)
@@ -142,22 +141,19 @@ class AnilistAnimeViewModel : ViewModel() {
sort = Anilist.sortBy[2],
season = season,
seasonYear = year,
hd = true
hd = true,
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
)?.results
)
}
private val updated: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getUpdated(): LiveData<MutableList<Media>> = updated
suspend fun loadUpdated() = updated.postValue(Anilist.query.recentlyUpdated())
private val animePopular = MutableLiveData<SearchResults?>(null)
fun getPopular(): LiveData<SearchResults?> = animePopular
suspend fun loadPopular(
type: String,
search_val: String? = null,
searchVal: String? = null,
genres: ArrayList<String>? = null,
sort: String = Anilist.sortBy[1],
onList: Boolean = true,
@@ -165,10 +161,11 @@ class AnilistAnimeViewModel : ViewModel() {
animePopular.postValue(
Anilist.query.search(
type,
search = search_val,
search = searchVal,
onList = if (onList) null else false,
sort = sort,
genres = genres
genres = genres,
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
)
)
}
@@ -183,13 +180,43 @@ class AnilistAnimeViewModel : ViewModel() {
r.sort,
r.genres,
r.tags,
r.status,
r.source,
r.format,
r.countryOfOrigin,
r.isAdult,
r.onList
r.onList,
adultOnly = PrefManager.getVal(PrefName.AdultOnly),
)
)
var loaded: Boolean = false
private val updated: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getUpdated(): LiveData<MutableList<Media>> = updated
private val popularMovies: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getMovies(): LiveData<MutableList<Media>> = popularMovies
private val topRatedAnime: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getTopRated(): LiveData<MutableList<Media>> = topRatedAnime
private val mostFavAnime: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getMostFav(): LiveData<MutableList<Media>> = mostFavAnime
suspend fun loadAll() {
val list = Anilist.query.loadAnimeList()
updated.postValue(list["recentUpdates"])
popularMovies.postValue(list["trendingMovies"])
topRatedAnime.postValue(list["topRated"])
mostFavAnime.postValue(list["mostFav"])
}
}
class AnilistMangaViewModel : ViewModel() {
@@ -207,29 +234,17 @@ class AnilistMangaViewModel : ViewModel() {
type,
perPage = 10,
sort = Anilist.sortBy[2],
hd = true
hd = true,
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
)?.results
)
private val updated: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getTrendingNovel(): LiveData<MutableList<Media>> = updated
suspend fun loadTrendingNovel() =
updated.postValue(
Anilist.query.search(
type,
perPage = 10,
sort = Anilist.sortBy[2],
format = "NOVEL"
)?.results
)
private val mangaPopular = MutableLiveData<SearchResults?>(null)
fun getPopular(): LiveData<SearchResults?> = mangaPopular
suspend fun loadPopular(
type: String,
search_val: String? = null,
searchVal: String? = null,
genres: ArrayList<String>? = null,
sort: String = Anilist.sortBy[1],
onList: Boolean = true,
@@ -237,10 +252,11 @@ class AnilistMangaViewModel : ViewModel() {
mangaPopular.postValue(
Anilist.query.search(
type,
search = search_val,
search = searchVal,
onList = if (onList) null else false,
sort = sort,
genres = genres
genres = genres,
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
)
)
}
@@ -255,17 +271,55 @@ class AnilistMangaViewModel : ViewModel() {
r.sort,
r.genres,
r.tags,
r.status,
r.source,
r.format,
r.countryOfOrigin,
r.isAdult,
r.onList,
r.excludedGenres,
r.excludedTags,
r.startYear,
r.seasonYear,
r.season
r.season,
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
)
)
var loaded: Boolean = false
private val popularManga: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getPopularManga(): LiveData<MutableList<Media>> = popularManga
private val popularManhwa: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getPopularManhwa(): LiveData<MutableList<Media>> = popularManhwa
private val popularNovel: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getPopularNovel(): LiveData<MutableList<Media>> = popularNovel
private val topRatedManga: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getTopRated(): LiveData<MutableList<Media>> = topRatedManga
private val mostFavManga: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
fun getMostFav(): LiveData<MutableList<Media>> = mostFavManga
suspend fun loadAll() {
val list = Anilist.query.loadMangaList()
popularManga.postValue(list["trendingManga"])
popularManhwa.postValue(list["trendingManhwa"])
popularNovel.postValue(list["trendingNovel"])
topRatedManga.postValue(list["topRated"])
mostFavManga.postValue(list["mostFav"])
}
}
class AnilistSearch : ViewModel() {
@@ -284,13 +338,17 @@ class AnilistSearch : ViewModel() {
r.sort,
r.genres,
r.tags,
r.status,
r.source,
r.format,
r.countryOfOrigin,
r.isAdult,
r.onList,
r.excludedGenres,
r.excludedTags,
r.startYear,
r.seasonYear,
r.season
r.season,
)
)
@@ -303,11 +361,15 @@ class AnilistSearch : ViewModel() {
r.sort,
r.genres,
r.tags,
r.status,
r.source,
r.format,
r.countryOfOrigin,
r.isAdult,
r.onList,
r.excludedGenres,
r.excludedTags,
r.startYear,
r.seasonYear,
r.season
)
@@ -331,4 +393,40 @@ class GenresViewModel : ViewModel() {
}
}
}
}
class ProfileViewModel : ViewModel() {
private val mangaFav: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getMangaFav(): LiveData<ArrayList<Media>> = mangaFav
private val animeFav: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getAnimeFav(): LiveData<ArrayList<Media>> = animeFav
suspend fun setData(id: Int) {
val res = Anilist.query.initProfilePage(id)
val mangaList = res?.data?.favoriteManga?.favourites?.manga?.edges?.mapNotNull {
it.node?.let { i ->
Media(i).apply { isFav = true }
}
}
mangaFav.postValue(ArrayList(mangaList ?: arrayListOf()))
val animeList = res?.data?.favoriteAnime?.favourites?.anime?.edges?.mapNotNull {
it.node?.let { i ->
Media(i).apply { isFav = true }
}
}
animeFav.postValue(ArrayList(animeList ?: arrayListOf()))
}
fun refresh() {
mangaFav.postValue(mangaFav.value)
animeFav.postValue(animeFav.value)
}
}

View File

@@ -4,7 +4,6 @@ import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import ani.dantotsu.logError
import ani.dantotsu.logger
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.startMainActivity
@@ -16,7 +15,6 @@ class Login : AppCompatActivity() {
ThemeManager(this).applyTheme()
val data: Uri? = intent?.data
logger(data.toString())
try {
Anilist.token =
Regex("""(?<=access_token=).+(?=&token_type)""").find(data.toString())!!.value

View File

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

View File

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

View File

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

View File

@@ -24,7 +24,9 @@ class Query {
@Serializable
data class Data(
@SerialName("Media")
val media: ani.dantotsu.connections.anilist.api.Media?
val media: ani.dantotsu.connections.anilist.api.Media?,
@SerialName("Page")
val page: ani.dantotsu.connections.anilist.api.Page?
)
}
@@ -136,44 +138,591 @@ class Query {
@SerialName("recommendationQuery") val recommendationQuery: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("recommendationPlannedQueryAnime") val recommendationPlannedQueryAnime: ani.dantotsu.connections.anilist.api.MediaListCollection?,
@SerialName("recommendationPlannedQueryManga") val recommendationPlannedQueryManga: ani.dantotsu.connections.anilist.api.MediaListCollection?,
@SerialName("Page1") val page1: ActivityPage?,
@SerialName("Page2") val page2: ActivityPage?
)
}
@Serializable
data class ProfilePageMedia(
@SerialName("data")
val data: Data?
) {
@Serializable
data class Data(
@SerialName("favoriteAnime") val favoriteAnime: ani.dantotsu.connections.anilist.api.User?,
@SerialName("favoriteManga") val favoriteManga: ani.dantotsu.connections.anilist.api.User?
)
}
@Serializable
data class AnimeList(
@SerialName("data")
val data: Data?
) {
@Serializable
data class Data(
@SerialName("recentUpdates") val recentUpdates: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("recentUpdates2") val recentUpdates2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingMovies") val trendingMovies: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingMovies2") val trendingMovies2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("topRated") val topRated: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("topRated2") val topRated2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("mostFav") val mostFav: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("mostFav2") val mostFav2: ani.dantotsu.connections.anilist.api.Page?,
)
}
@Serializable
data class MangaList(
@SerialName("data")
val data: Data?
) {
@Serializable
data class Data(
@SerialName("trendingManga") val trendingManga: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingManga2") val trendingManga2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingManhwa") val trendingManhwa: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingManhwa2") val trendingManhwa2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingNovel") val trendingNovel: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingNovel2") val trendingNovel2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("topRated") val topRated: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("topRated2") val topRated2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("mostFav") val mostFav: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("mostFav2") val mostFav2: ani.dantotsu.connections.anilist.api.Page?,
)
}
@Serializable
data class ToggleFollow(
@SerialName("data")
val data: Data?
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("ToggleFollow")
val toggleFollow: FollowData
) : java.io.Serializable
}
@Serializable
data class GenreCollection(
@SerialName("data")
val data: Data
) {
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("GenreCollection")
val genreCollection: List<String>?
)
) : java.io.Serializable
}
@Serializable
data class MediaTagCollection(
@SerialName("data")
val data: Data
) {
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("MediaTagCollection")
val mediaTagCollection: List<MediaTag>?
)
) : java.io.Serializable
}
@Serializable
data class User(
@SerialName("data")
val data: Data
) {
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("User")
val user: ani.dantotsu.connections.anilist.api.User?
)
) : java.io.Serializable
}
@Serializable
data class UserProfileResponse(
@SerialName("data")
val data: Data
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("followerPage")
val followerPage: UserProfilePage?,
@SerialName("followingPage")
val followingPage: UserProfilePage?,
@SerialName("user")
val user: UserProfile?
) : java.io.Serializable
}
@Serializable
data class UserProfilePage(
@SerialName("pageInfo")
val pageInfo: PageInfo,
) : java.io.Serializable
@Serializable
data class Following(
@SerialName("data")
val data: Data
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("Page")
val page: FollowingPage?
) : java.io.Serializable
}
@Serializable
data class Follower(
@SerialName("data")
val data: Data
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("Page")
val page: FollowerPage?
) : java.io.Serializable
}
@Serializable
data class FollowerPage(
@SerialName("followers")
val followers: List<ani.dantotsu.connections.anilist.api.User>?
) : java.io.Serializable
@Serializable
data class FollowingPage(
@SerialName("following")
val following: List<ani.dantotsu.connections.anilist.api.User>?
) : java.io.Serializable
@Serializable
data class UserProfile(
@SerialName("id")
val id: Int,
@SerialName("name")
val name: String,
@SerialName("about")
val about: String?,
@SerialName("avatar")
val avatar: UserAvatar?,
@SerialName("bannerImage")
val bannerImage: String?,
@SerialName("isFollowing")
var isFollowing: Boolean,
@SerialName("isFollower")
val isFollower: Boolean,
@SerialName("isBlocked")
val isBlocked: Boolean,
@SerialName("favourites")
val favourites: UserFavourites?,
@SerialName("statistics")
val statistics: NNUserStatisticTypes,
@SerialName("siteUrl")
val siteUrl: String,
) : java.io.Serializable
@Serializable
data class NNUserStatisticTypes(
@SerialName("anime") var anime: NNUserStatistics,
@SerialName("manga") var manga: NNUserStatistics
) : java.io.Serializable
@Serializable
data class NNUserStatistics(
@SerialName("count") var count: Int,
@SerialName("meanScore") var meanScore: Float,
@SerialName("standardDeviation") var standardDeviation: Float,
@SerialName("minutesWatched") var minutesWatched: Int,
@SerialName("episodesWatched") var episodesWatched: Int,
@SerialName("chaptersRead") var chaptersRead: Int,
@SerialName("volumesRead") var volumesRead: Int,
) : java.io.Serializable
@Serializable
data class UserFavourites(
@SerialName("anime")
val anime: UserMediaFavouritesCollection,
@SerialName("manga")
val manga: UserMediaFavouritesCollection,
@SerialName("characters")
val characters: UserCharacterFavouritesCollection,
@SerialName("staff")
val staff: UserStaffFavouritesCollection,
@SerialName("studios")
val studios: UserStudioFavouritesCollection,
) : java.io.Serializable
@Serializable
data class UserMediaFavouritesCollection(
@SerialName("nodes")
val nodes: List<UserMediaImageFavorite>,
) : java.io.Serializable
@Serializable
data class UserMediaImageFavorite(
@SerialName("id")
val id: Int,
@SerialName("coverImage")
val coverImage: MediaCoverImage
) : java.io.Serializable
@Serializable
data class UserCharacterFavouritesCollection(
@SerialName("nodes")
val nodes: List<UserCharacterImageFavorite>,
) : java.io.Serializable
@Serializable
data class UserCharacterImageFavorite(
@SerialName("id")
val id: Int,
@SerialName("name")
val name: CharacterName,
@SerialName("image")
val image: CharacterImage,
@SerialName("isFavourite")
val isFavourite: Boolean
) : java.io.Serializable
@Serializable
data class UserStaffFavouritesCollection(
@SerialName("nodes")
val nodes: List<UserCharacterImageFavorite>, //downstream it's the same as character
) : java.io.Serializable
@Serializable
data class UserStudioFavouritesCollection(
@SerialName("nodes")
val nodes: List<UserStudioFavorite>,
) : java.io.Serializable
@Serializable
data class UserStudioFavorite(
@SerialName("id")
val id: Int,
@SerialName("name")
val name: String,
) : java.io.Serializable
//----------------------------------------
// Statistics
@Serializable
data class StatisticsResponse(
@SerialName("data")
val data: Data
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("User")
val user: StatisticsUser?
) : java.io.Serializable
}
@Serializable
data class StatisticsUser(
@SerialName("id")
val id: Int,
@SerialName("name")
val name: String,
@SerialName("mediaListOptions")
val mediaListOptions: MediaListOptions,
@SerialName("statistics")
val statistics: StatisticsTypes
) : java.io.Serializable
@Serializable
data class StatisticsTypes(
@SerialName("anime")
val anime: Statistics,
@SerialName("manga")
val manga: Statistics
) : java.io.Serializable
@Serializable
data class Statistics(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("standardDeviation")
val standardDeviation: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("episodesWatched")
val episodesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("volumesRead")
val volumesRead: Int,
@SerialName("formats")
val formats: List<StatisticsFormat>,
@SerialName("statuses")
val statuses: List<StatisticsStatus>,
@SerialName("scores")
val scores: List<StatisticsScore>,
@SerialName("lengths")
val lengths: List<StatisticsLength>,
@SerialName("releaseYears")
val releaseYears: List<StatisticsReleaseYear>,
@SerialName("startYears")
val startYears: List<StatisticsStartYear>,
@SerialName("genres")
val genres: List<StatisticsGenre>,
@SerialName("tags")
val tags: List<StatisticsTag>,
@SerialName("countries")
val countries: List<StatisticsCountry>,
@SerialName("voiceActors")
val voiceActors: List<StatisticsVoiceActor>,
@SerialName("staff")
val staff: List<StatisticsStaff>,
@SerialName("studios")
val studios: List<StatisticsStudio>
) : java.io.Serializable
@Serializable
data class StatisticsFormat(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("format")
val format: String
) : java.io.Serializable
@Serializable
data class StatisticsStatus(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("status")
val status: String
) : java.io.Serializable
@Serializable
data class StatisticsScore(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("score")
val score: Int
) : java.io.Serializable
@Serializable
data class StatisticsLength(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("length")
val length: String? //can be null for manga
) : java.io.Serializable
@Serializable
data class StatisticsReleaseYear(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("releaseYear")
val releaseYear: Int
) : java.io.Serializable
@Serializable
data class StatisticsStartYear(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("startYear")
val startYear: Int
) : java.io.Serializable
@Serializable
data class StatisticsGenre(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("genre")
val genre: String
) : java.io.Serializable
@Serializable
data class StatisticsTag(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("tag")
val tag: Tag
) : java.io.Serializable
@Serializable
data class Tag(
@SerialName("id")
val id: Int,
@SerialName("name")
val name: String
) : java.io.Serializable
@Serializable
data class StatisticsCountry(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("country")
val country: String
) : java.io.Serializable
@Serializable
data class StatisticsVoiceActor(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("voiceActor")
val voiceActor: VoiceActor,
@SerialName("characterIds")
val characterIds: List<Int>
) : java.io.Serializable
@Serializable
data class VoiceActor(
@SerialName("id")
val id: Int,
@SerialName("name")
val name: StaffName
) : java.io.Serializable
@Serializable
data class StaffName(
@SerialName("first")
val first: String?,
@SerialName("middle")
val middle: String?,
@SerialName("last")
val last: String?,
@SerialName("full")
val full: String?,
@SerialName("native")
val native: String?,
@SerialName("alternative")
val alternative: List<String>?,
@SerialName("userPreferred")
val userPreferred: String?
) : java.io.Serializable
@Serializable
data class StatisticsStaff(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("staff")
val staff: VoiceActor
) : java.io.Serializable
@Serializable
data class StatisticsStudio(
@SerialName("count")
val count: Int,
@SerialName("meanScore")
val meanScore: Float,
@SerialName("minutesWatched")
val minutesWatched: Int,
@SerialName("chaptersRead")
val chaptersRead: Int,
@SerialName("mediaIds")
val mediaIds: List<Int>,
@SerialName("studio")
val studio: StatStudio
) : java.io.Serializable
@Serializable
data class StatStudio(
@SerialName("id")
val id: Int,
@SerialName("name")
val name: String,
@SerialName("isAnimationStudio")
val isAnimationStudio: Boolean
) : java.io.Serializable
}
//data class WhaData(
@@ -203,7 +752,7 @@ class Query {
// // Activity reply query
// val ActivityReply: ActivityReply?,
// // Comment query
// // CommentNotificationWorker query
// val ThreadComment: List<ThreadComment>?,
// // Notification query

View File

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

View File

@@ -174,7 +174,7 @@ data class Media(
// Notes for site moderators
@SerialName("modNotes") var modNotes: String?,
)
) : java.io.Serializable
@Serializable
data class MediaTitle(
@@ -189,7 +189,7 @@ data class MediaTitle(
// The currently authenticated users preferred title language. Default romaji for non-authenticated
@SerialName("userPreferred") var userPreferred: String,
)
): java.io.Serializable
@Serializable
enum class MediaType {
@@ -205,15 +205,17 @@ enum class MediaStatus {
FINISHED, RELEASING, NOT_YET_RELEASED, CANCELLED, HIATUS;
override fun toString(): String {
return when (super.toString()) {
"FINISHED" -> currContext()!!.getString(R.string.status_finished)
"RELEASING" -> currContext()!!.getString(R.string.status_releasing)
"NOT_YET_RELEASED" -> currContext()!!.getString(R.string.status_not_yet_released)
"CANCELLED" -> currContext()!!.getString(R.string.status_cancelled)
"HIATUS" -> currContext()!!.getString(R.string.status_hiatus)
else -> ""
currContext()?.let {
return when (super.toString()) {
"FINISHED" -> it.getString(R.string.status_finished)
"RELEASING" -> it.getString(R.string.status_releasing)
"NOT_YET_RELEASED" -> it.getString(R.string.status_not_yet_released)
"CANCELLED" -> it.getString(R.string.status_cancelled)
"HIATUS" -> it.getString(R.string.status_hiatus)
else -> ""
}
}
return super.toString().replace("_", " ")
}
}
@@ -251,7 +253,7 @@ data class MediaCoverImage(
// Average #hex color of cover image
@SerialName("color") var color: String?,
)
) : java.io.Serializable
@Serializable
data class MediaList(
@@ -445,17 +447,20 @@ enum class MediaRelation {
ADAPTATION, PREQUEL, SEQUEL, PARENT, SIDE_STORY, CHARACTER, SUMMARY, ALTERNATIVE, SPIN_OFF, OTHER, SOURCE, COMPILATION, CONTAINS;
override fun toString(): String {
return when (super.toString()) {
"ADAPTATION" -> currContext()!!.getString(R.string.type_adaptation)
"PARENT" -> currContext()!!.getString(R.string.type_parent)
"CHARACTER" -> currContext()!!.getString(R.string.type_character)
"SUMMARY" -> currContext()!!.getString(R.string.type_summary)
"ALTERNATIVE" -> currContext()!!.getString(R.string.type_alternative)
"OTHER" -> currContext()!!.getString(R.string.type_other)
"SOURCE" -> currContext()!!.getString(R.string.type_source)
"CONTAINS" -> currContext()!!.getString(R.string.type_contains)
else -> super.toString().replace("_", " ")
currContext()?.let {
return when (super.toString()) {
"ADAPTATION" -> it.getString(R.string.type_adaptation)
"PARENT" -> it.getString(R.string.type_parent)
"CHARACTER" -> it.getString(R.string.type_character)
"SUMMARY" -> it.getString(R.string.type_summary)
"ALTERNATIVE" -> it.getString(R.string.type_alternative)
"OTHER" -> it.getString(R.string.type_other)
"SOURCE" -> it.getString(R.string.type_source)
"CONTAINS" -> it.getString(R.string.type_contains)
else -> super.toString().replace("_", " ")
}
}
return super.toString().replace("_", " ")
}
}
@@ -490,7 +495,7 @@ data class MediaExternalLink(
// isDisabled: Boolean
@SerialName("notes") var notes: String?,
)
) : java.io.Serializable
@Serializable
enum class ExternalLinkType {
@@ -512,7 +517,13 @@ data class MediaListCollection(
// If there is another chunk
@SerialName("hasNextChunk") var hasNextChunk: Boolean?,
)
) : java.io.Serializable
@Serializable
data class FollowData(
@SerialName("id") var id: Int,
@SerialName("isFollowing") var isFollowing: Boolean,
) : java.io.Serializable
@Serializable
data class MediaListGroup(
@@ -526,4 +537,4 @@ data class MediaListGroup(
@SerialName("isSplitCompletedList") var isSplitCompletedList: Boolean?,
@SerialName("status") var status: MediaListStatus?,
)
) : java.io.Serializable

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,14 +20,14 @@ object Discord {
var avatar: String? = null
fun getSavedToken(context: Context): Boolean {
fun getSavedToken(): Boolean {
token = PrefManager.getVal(
PrefName.DiscordToken, null as String?
)
return token != null
}
fun saveToken(context: Context, token: String) {
fun saveToken(token: String) {
PrefManager.setVal(PrefName.DiscordToken, token)
}
@@ -70,19 +70,7 @@ object Discord {
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 {
RPC(it, Dispatchers.IO).apply {
applicationId = application_Id
smallImage = RPC.Link(
"Dantotsu",
small_Image
)
buttons.add(RPC.Link("Stream on Dantotsu", "https://github.com/rebelonion/Dantotsu/"))
}
}
}*/
"mp:external/GJEe4hKzr8w56IW6ZKQz43HFVEo8pOtA_C-dJiWwxKo/https/cdn.discordapp.com/app-icons/1163925779692912771/f6b42d41dfdf0b56fcc79d4a12d2ac66.png"
const val small_Image_AniList: String =
"mp:external/rHOIjjChluqQtGyL_UHk6Z4oAqiVYlo_B7HSGPLSoUg/%3Fsize%3D128/https/cdn.discordapp.com/icons/210521487378087947/a_f54f910e2add364a3da3bb2f2fce0c72.webp"
}

View File

@@ -5,17 +5,12 @@ 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
@@ -26,6 +21,7 @@ import ani.dantotsu.connections.discord.serializers.User
import ani.dantotsu.isOnline
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.util.Logger
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonParser
@@ -37,7 +33,6 @@ 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
@@ -49,6 +44,7 @@ class DiscordService : Service() {
private lateinit var heartbeatThread: Thread
private lateinit var client: OkHttpClient
private lateinit var wakeLock: PowerManager.WakeLock
private val shouldLog = false
var presenceStore = ""
val json = Json {
encodeDefaults = true
@@ -67,7 +63,7 @@ class DiscordService : Service() {
PowerManager.PARTIAL_WAKE_LOCK,
"discordRPC:backgroundPresence"
)
wakeLock.acquire()
wakeLock.acquire(30 * 60 * 1000L /*30 minutes*/)
log("WakeLock Acquired")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel(
@@ -162,8 +158,8 @@ class DiscordService : Service() {
inner class DiscordWebSocketListener : WebSocketListener() {
var retryAttempts = 0
val maxRetryAttempts = 10
private var retryAttempts = 0
private val maxRetryAttempts = 10
override fun onOpen(webSocket: WebSocket, response: Response) {
super.onOpen(webSocket, response)
this@DiscordService.webSocket = webSocket
@@ -232,7 +228,7 @@ class DiscordService : Service() {
resume()
resume = false
} else {
identify(webSocket, baseContext)
identify(webSocket)
log("WebSocket: Identified")
}
}
@@ -245,13 +241,13 @@ class DiscordService : Service() {
}
}
fun identify(webSocket: WebSocket, context: Context) {
private fun identify(webSocket: WebSocket) {
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("token", getToken())
d.addProperty("intents", 0)
d.add("properties", properties)
val payload = JsonObject()
@@ -270,11 +266,11 @@ class DiscordService : Service() {
retryAttempts++
if (retryAttempts >= maxRetryAttempts) {
log("WebSocket: Error, onFailure() reason: Max Retry Attempts")
errorNotification("Could not set the presence", "Max Retry Attempts")
errorNotification("Timeout setting presence", "Max Retry Attempts")
return
}
}
t.message?.let { Log.d("WebSocket", "onFailure() $it") }
t.message?.let { Logger.log("onFailure() $it") }
log("WebSocket: Error, onFailure() reason: ${t.message}")
client = OkHttpClient()
client.newWebSocket(
@@ -289,7 +285,7 @@ class DiscordService : Service() {
override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
super.onClosing(webSocket, code, reason)
Log.d("WebSocket", "onClosing() $code $reason")
Logger.log("onClosing() $code $reason")
if (::heartbeatThread.isInitialized && !heartbeatThread.isInterrupted) {
heartbeatThread.interrupt()
}
@@ -297,7 +293,7 @@ class DiscordService : Service() {
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
super.onClosed(webSocket, code, reason)
Log.d("WebSocket", "onClosed() $code $reason")
Logger.log("onClosed() $code $reason")
if (code >= 4000) {
log("WebSocket: Error, code: $code reason: $reason")
client = OkHttpClient()
@@ -311,7 +307,7 @@ class DiscordService : Service() {
}
}
fun getToken(context: Context): String {
fun getToken(): String {
val token = PrefManager.getVal(PrefName.DiscordToken, null as String?)
return if (token == null) {
log("WebSocket: Token not found")
@@ -349,13 +345,13 @@ class DiscordService : Service() {
Manifest.permission.POST_NOTIFICATIONS
) != PackageManager.PERMISSION_GRANTED
) {
//TODO: Request permission
return
}
notificationManager.notify(2, builder.build())
log("Error Notified")
}
@Suppress("unused")
fun saveSimpleTestPresence() {
val file = File(baseContext.cacheDir, "payload")
//fill with test payload
@@ -375,65 +371,22 @@ class DiscordService : Service() {
log("WebSocket: Simple Test Presence Saved")
}
fun setPresence(String: String) {
fun setPresence(string: String) {
log("WebSocket: Sending Presence payload")
log(String)
webSocket.send(String)
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")
if (shouldLog) {
Logger.log(string)
}
}
fun resume() {
log("Sending Resume payload")
val d = JsonObject()
d.addProperty("token", getToken(baseContext))
d.addProperty("token", getToken())
d.addProperty("session_id", sessionId)
d.addProperty("seq", sequence)
val json = JsonObject()
@@ -449,7 +402,7 @@ class DiscordService : Service() {
Thread.sleep(heartbeat.toLong())
heartbeatSend(webSocket, sequence)
log("WebSocket: Heartbeat Sent")
} catch (e: InterruptedException) {
} catch (ignored: InterruptedException) {
}
}
}

View File

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

View File

@@ -2,6 +2,8 @@ package ani.dantotsu.connections.discord
import ani.dantotsu.connections.discord.serializers.Activity
import ani.dantotsu.connections.discord.serializers.Presence
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@@ -69,8 +71,8 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
assets = Activity.Assets(
largeImage = data.largeImage?.url?.discordUrl(),
largeText = data.largeImage?.label,
smallImage = data.smallImage?.url?.discordUrl(),
smallText = data.smallImage?.label
smallImage = if (PrefManager.getVal(PrefName.ShowAniListIcon)) Discord.small_Image_AniList.discordUrl() else Discord.small_Image.discordUrl(),
smallText = if (PrefManager.getVal(PrefName.ShowAniListIcon)) "Anilist" else "Dantotsu",
),
buttons = data.buttons.map { it.label },
metadata = Activity.Metadata(
@@ -81,7 +83,7 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
),
afk = true,
since = data.startTimestamp,
status = data.status
status = PrefManager.getVal(PrefName.DiscordStatus)
)
))
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,32 +9,35 @@ import android.content.IntentFilter
import android.content.pm.PackageManager
import android.content.pm.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.documentfile.provider.DocumentFile
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.addons.download.DownloadAddonManager
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.currActivity
import ani.dantotsu.defaultHeaders
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.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.download.anime.AnimeDownloaderService.AnimeDownloadTask.Companion.getTaskName
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.SubtitleDownloader
import ani.dantotsu.media.anime.AnimeWatchFragment
import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.Video
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.snackString
import ani.dantotsu.toast
import ani.dantotsu.util.Logger
import com.anggrayudi.storage.file.forceDelete
import com.anggrayudi.storage.file.openOutputStream
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.animesource.model.SAnime
@@ -45,9 +48,7 @@ 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
@@ -56,13 +57,12 @@ 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
@@ -73,6 +73,7 @@ class AnimeDownloaderService : Service() {
private val mutex = Mutex()
private var isCurrentlyProcessing = false
private var currentTasks: MutableList<AnimeDownloadTask> = mutableListOf()
private val ffExtension = Injekt.get<DownloadAddonManager>().extension?.extension
override fun onBind(intent: Intent?): IBinder? {
// This is only required for bound services.
@@ -81,6 +82,11 @@ class AnimeDownloaderService : Service() {
override fun onCreate() {
super.onCreate()
if (ffExtension == null) {
toast(getString(R.string.download_addon_not_found))
stopSelf()
return
}
notificationManager = NotificationManagerCompat.from(this)
builder =
NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER_PROGRESS).apply {
@@ -88,6 +94,7 @@ class AnimeDownloaderService : Service() {
setSmallIcon(R.drawable.ic_download_24)
priority = NotificationCompat.PRIORITY_DEFAULT
setOnlyAlertOnce(true)
setProgress(100, 0, false)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(
@@ -156,27 +163,14 @@ class AnimeDownloaderService : Service() {
@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
val sessionIds =
AnimeServiceDataSingleton.downloadQueue.filter { it.getTaskName() == taskName }
.map { it.sessionId }.toMutableList()
sessionIds.addAll(currentTasks.filter { it.getTaskName() == taskName }.map { it.sessionId })
sessionIds.forEach {
ffExtension!!.cancelDownload(it)
}
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()
@@ -209,7 +203,6 @@ class AnimeDownloaderService : Service() {
@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(
@@ -220,18 +213,66 @@ class AnimeDownloaderService : Service() {
true
}
builder.setContentText("Downloading ${task.title} - ${task.episode}")
builder.setContentText("Downloading ${getTaskName(task.title, task.episode)}")
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
currActivity()?.let {
Helper.downloadVideo(
it,
task.video,
task.subtitle
)
val outputDir = getSubDirectory(
this@AnimeDownloaderService,
MediaType.ANIME,
false,
task.title,
task.episode
) ?: throw Exception("Failed to create output directory")
outputDir.findFile("${task.getTaskName()}.mp4")?.delete()
val outputFile = outputDir.createFile("video/x-matroska", "${task.getTaskName()}.mkv")
?: throw Exception("Failed to create output file")
var percent = 0
var totalLength = 0.0
val path = ffExtension!!.setDownloadPath(
this@AnimeDownloaderService,
outputFile.uri
)
val headersStringBuilder = StringBuilder()
task.video.file.headers.forEach {
headersStringBuilder.append("\"${it.key}: ${it.value}\"\'\r\n\'")
}
if (!task.video.file.headers.containsKey("User-Agent")) { //headers should never be empty now
headersStringBuilder.append("\"").append("User-Agent: ")
.append(defaultHeaders["User-Agent"]).append("\"\'\r\n\'")
}
val probeRequest =
"-headers $headersStringBuilder -i ${task.video.file.url} -show_entries format=duration -v quiet -of csv=\"p=0\""
ffExtension.executeFFProbe(
probeRequest
) {
if (it.toDoubleOrNull() != null) {
totalLength = it.toDouble()
}
}
val headers = headersStringBuilder.toString()
var request = "-headers $headers "
request += "-i ${task.video.file.url} -c copy -map 0:v -map 0:a -map 0:s?" +
" -f matroska -timeout 600 -reconnect 1" +
" -reconnect_streamed 1 -allowed_extensions ALL " +
"-tls_verify 0 $path -v trace"
Logger.log("Request: $request")
val ffTask =
ffExtension.executeFFMpeg(request) {
// CALLED WHEN SESSION GENERATES STATISTICS
val timeInMilliseconds = it
if (timeInMilliseconds > 0 && totalLength > 0) {
percent = ((it / 1000) / totalLength * 100).toInt()
}
Logger.log("Statistics: $it")
}
task.sessionId = ffTask
currentTasks.find { it.getTaskName() == task.getTaskName() }?.sessionId =
ffTask
saveMediaInfo(task)
task.subtitle?.let {
@@ -241,94 +282,124 @@ class AnimeDownloaderService : Service() {
DownloadedType(
task.title,
task.episode,
DownloadedType.Type.ANIME,
MediaType.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(
while (ffExtension.getState(ffTask) != "COMPLETED") {
if (ffExtension.getState(ffTask) == "FAILED") {
Logger.log("Download failed")
builder.setContentText(
"${
getTaskName(
task.title,
task.episode,
DownloadedType.Type.ANIME,
task.episode
)
)
Injekt.get<CrashlyticsInterface>().logException(
Exception(
"Anime Download failed:" +
" ${download.failureReason}" +
" url: ${task.video.file.url}" +
" title: ${task.title}" +
" episode: ${task.episode}"
)
)
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
broadcastDownloadFailed(task.episode)
break
}
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_COMPLETED) {
logger("Download completed")
builder.setContentText("${task.title} - ${task.episode} Download completed")
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${task.title} - ${task.episode} Download completed")
PrefManager.getAnimeDownloadPreferences().edit().putString(
task.getTaskName(),
task.video.file.url
).apply()
downloadsManager.addDownload(
DownloadedType(
task.title,
task.episode,
DownloadedType.Type.ANIME,
)
)
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
broadcastDownloadFinished(task.episode)
break
}
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_STOPPED) {
logger("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()
} Download failed"
)
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
notificationManager.notify(NOTIFICATION_ID, builder.build())
toast("${getTaskName(task.title, task.episode)} Download failed")
Logger.log("Download failed: ${ffExtension.getStackTrace(ffTask)}")
downloadsManager.removeDownload(
DownloadedType(
task.title,
task.episode,
MediaType.ANIME,
),
false
) {}
Injekt.get<CrashlyticsInterface>().logException(
Exception(
"Anime Download failed:" +
" ${getTaskName(task.title, task.episode)}" +
" url: ${task.video.file.url}" +
" title: ${task.title}" +
" episode: ${task.episode}"
)
)
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
broadcastDownloadFailed(task.episode)
break
}
builder.setProgress(
100, percent.coerceAtMost(99),
false
)
broadcastDownloadProgress(
task.episode,
percent.coerceAtMost(99)
)
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
kotlinx.coroutines.delay(2000)
}
if (ffExtension.getState(ffTask) == "COMPLETED") {
if (ffExtension.hadError(ffTask)) {
Logger.log("Download failed")
builder.setContentText(
"${
getTaskName(
task.title,
task.episode
)
} Download failed"
)
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${getTaskName(task.title, task.episode)} Download failed")
downloadsManager.removeDownload(
DownloadedType(
task.title,
task.episode,
MediaType.ANIME,
)
) {}
Injekt.get<CrashlyticsInterface>().logException(
Exception(
"Anime Download failed:" +
" ${getTaskName(task.title, task.episode)}" +
" url: ${task.video.file.url}" +
" title: ${task.title}" +
" episode: ${task.episode}"
)
)
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
broadcastDownloadFailed(task.episode)
return@withContext
}
Logger.log("Download completed")
builder.setContentText(
"${
getTaskName(
task.title,
task.episode
)
} Download completed"
)
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${getTaskName(task.title, task.episode)} Download completed")
PrefManager.getAnimeDownloadPreferences().edit().putString(
task.getTaskName(),
task.video.file.url
).apply()
downloadsManager.addDownload(
DownloadedType(
task.title,
task.episode,
MediaType.ANIME,
)
)
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
broadcastDownloadFinished(task.episode)
} else throw Exception("Download failed")
}
} catch (e: Exception) {
if (e.message?.contains("Coroutine was cancelled") == false) { //wut
logger("Exception while downloading file: ${e.message}")
Logger.log("Exception while downloading file: ${e.message}")
snackString("Exception while downloading file: ${e.message}")
e.printStackTrace()
Injekt.get<CrashlyticsInterface>().logException(e)
@@ -337,36 +408,24 @@ class AnimeDownloaderService : Service() {
}
}
@androidx.annotation.OptIn(UnstableApi::class)
suspend fun hasDownloadStarted(
downloadManager: DownloadManager,
task: AnimeDownloadTask,
timeout: Long
): Boolean {
val startTime = System.currentTimeMillis()
while (System.currentTimeMillis() - startTime < timeout) {
val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
if (download != null) {
return true
}
// Delay between each poll
kotlinx.coroutines.delay(500)
}
return false
}
@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()
CoroutineScope(Dispatchers.IO).launch {
val directory =
getSubDirectory(this@AnimeDownloaderService, MediaType.ANIME, false, task.title)
?: throw Exception("Directory not found")
directory.findFile("media.json")?.forceDelete(this@AnimeDownloaderService)
val file = directory.createFile("application/json", "media.json")
?: throw Exception("File not created")
val episodeDirectory =
getSubDirectory(
this@AnimeDownloaderService,
MediaType.ANIME,
false,
task.title,
task.episode
)
?: throw Exception("Directory not found")
val file = File(directory, "media.json")
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
@@ -400,14 +459,25 @@ class AnimeDownloaderService : Service() {
val jsonString = gson.toJson(media)
withContext(Dispatchers.Main) {
file.writeText(jsonString)
try {
file.openOutputStream(this@AnimeDownloaderService, false).use { output ->
if (output == null) throw Exception("Output stream is null")
output.write(jsonString.toByteArray())
}
} catch (e: android.system.ErrnoException) {
e.printStackTrace()
Toast.makeText(
this@AnimeDownloaderService,
"Error while saving: ${e.localizedMessage}",
Toast.LENGTH_LONG
).show()
}
}
}
}
}
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
withContext(Dispatchers.IO) {
var connection: HttpURLConnection? = null
println("Downloading url $url")
@@ -418,13 +488,16 @@ class AnimeDownloaderService : Service() {
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
}
val file = File(directory, name)
FileOutputStream(file).use { output ->
directory.findFile(name)?.forceDelete(this@AnimeDownloaderService)
val file =
directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
file.openOutputStream(this@AnimeDownloaderService, false).use { output ->
if (output == null) throw Exception("Output stream is null")
connection.inputStream.use { input ->
input.copyTo(output)
}
}
return@withContext file.absolutePath
return@withContext file.uri.toString()
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
@@ -491,14 +564,15 @@ class AnimeDownloaderService : Service() {
val episodeImage: String? = null,
val retries: Int = 2,
val simultaneousDownloads: Int = 2,
var sessionId: Long = -1
) {
fun getTaskName(): String {
return "$title - $episode"
return "${title.replace("/", "")}/${episode.replace("/", "")}"
}
companion object {
fun getTaskName(title: String, episode: String): String {
return "$title - $episode"
return "${title.replace("/", "")}/${episode.replace("/", "")}"
}
}
}
@@ -512,7 +586,6 @@ class AnimeDownloaderService : Service() {
object AnimeServiceDataSingleton {
var video: Video? = null
var sourceMedia: Media? = null
var downloadQueue: Queue<AnimeDownloaderService.AnimeDownloadTask> = ConcurrentLinkedQueue()
@Volatile

View File

@@ -1,7 +1,6 @@
package ani.dantotsu.download.anime
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
@@ -38,7 +37,6 @@ class OfflineAnimeAdapter(
return position.toLong()
}
@SuppressLint("SetTextI18n")
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view: View = convertView ?: when (style) {
@@ -51,28 +49,27 @@ class OfflineAnimeAdapter(
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 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
episodes.text = context.getString(R.string.episodes)
bannerView.setImageURI(item.banner ?: item.image)
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
totalEpisodes.text = context.getString(R.string.total_divider, item.totalEpisode)
}
// Bind item data to the views
typeimage.setImageResource(R.drawable.ic_round_movie_filter_24)
typeImage.setImageResource(R.drawable.ic_round_movie_filter_24)
type.text = item.type
typeView.visibility = View.VISIBLE
imageView.setImageURI(item.image)

View File

@@ -4,10 +4,8 @@ package ani.dantotsu.download.anime
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.text.Editable
import android.text.TextWatcher
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -22,26 +20,35 @@ import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.marginBottom
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.util.UnstableApi
import ani.dantotsu.R
import ani.dantotsu.bottomBar
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.currActivity
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadCompat.Companion.loadMediaCompat
import ani.dantotsu.download.DownloadCompat.Companion.loadOfflineAnimeModelCompat
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.compareName
import ani.dantotsu.download.findValidName
import ani.dantotsu.getThemeColor
import ani.dantotsu.initActivity
import ani.dantotsu.logger
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaType
import ani.dantotsu.navBarHeight
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.SettingsDialogFragment
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import com.anggrayudi.storage.file.openInputStream
import com.google.android.material.card.MaterialCardView
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.textfield.TextInputLayout
@@ -53,9 +60,13 @@ import eu.kanade.tachiyomi.animesource.model.SEpisode
import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SChapterImpl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
@@ -64,6 +75,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
private lateinit var gridView: GridView
private lateinit var adapter: OfflineAnimeAdapter
private lateinit var total: TextView
private var downloadsJob: Job = Job()
override fun onCreateView(
inflater: LayoutInflater,
@@ -79,9 +91,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
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 color = requireContext().getThemeColor(android.R.attr.windowBackground)
val animeUserAvatar = view.findViewById<ShapeableImageView>(R.id.offlineMangaUserAvatar)
animeUserAvatar.setSafeOnClickListener {
@@ -110,10 +120,10 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
})
var style: Int = PrefManager.getVal(PrefName.OfflineView)
val layoutList = view.findViewById<ImageView>(R.id.downloadedList)
val layoutcompact = view.findViewById<ImageView>(R.id.downloadedGrid)
val layoutCompact = view.findViewById<ImageView>(R.id.downloadedGrid)
var selected = when (style) {
0 -> layoutList
1 -> layoutcompact
1 -> layoutCompact
else -> layoutList
}
selected.alpha = 1f
@@ -134,7 +144,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
grid()
}
layoutcompact.setOnClickListener {
layoutCompact.setOnClickListener {
selected(it as ImageView)
style = 1
PrefManager.setVal(PrefName.OfflineView, style)
@@ -154,11 +164,11 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
@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)
getDownloads()
gridView.adapter = adapter
gridView.scheduleLayoutAnimation()
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
@@ -166,20 +176,22 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
// Get the OfflineAnimeModel that was clicked
val item = adapter.getItem(position) as OfflineAnimeModel
val media =
downloadManager.animeDownloadedTypes.firstOrNull { it.title == item.title }
downloadManager.animeDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) }
media?.let {
val mediaModel = getMedia(it)
if (mediaModel == null) {
snackString("Error loading media.json")
return@let
lifecycleScope.launch {
val mediaModel = getMedia(it)
if (mediaModel == null) {
snackString("Error loading media.json")
return@launch
}
MediaDetailsActivity.mediaSingleton = mediaModel
ContextCompat.startActivity(
requireActivity(),
Intent(requireContext(), MediaDetailsActivity::class.java)
.putExtra("download", true),
null
)
}
MediaDetailsActivity.mediaSingleton = mediaModel
ContextCompat.startActivity(
requireActivity(),
Intent(requireContext(), MediaDetailsActivity::class.java)
.putExtra("download", true),
null
)
} ?: run {
snackString("no media found")
}
@@ -187,8 +199,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
gridView.setOnItemLongClickListener { _, _, position, _ ->
// Get the OfflineAnimeModel that was clicked
val item = adapter.getItem(position) as OfflineAnimeModel
val type: DownloadedType.Type =
DownloadedType.Type.ANIME
val type: MediaType = MediaType.ANIME
// Alert dialog to confirm deletion
val builder =
@@ -203,13 +214,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
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
@@ -237,7 +242,6 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
// Implement behavior for different scroll states if needed
}
override fun onScroll(
@@ -250,7 +254,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
val visibility = first != null && first.top < 0
scrollTop.translationY =
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE
scrollTop.isVisible = visibility
}
})
initActivity(requireActivity())
@@ -260,7 +264,6 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
override fun onResume() {
super.onResume()
getDownloads()
adapter.notifyDataSetChanged()
}
override fun onPause() {
@@ -280,29 +283,39 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
private fun getDownloads() {
downloads = listOf()
val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct()
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
for (title in animeTitles) {
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
val download = tDownloads.first()
val offlineAnimeModel = loadOfflineAnimeModel(download)
newAnimeDownloads += offlineAnimeModel
if (downloadsJob.isActive) {
downloadsJob.cancel()
}
downloadsJob = Job()
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
val animeTitles = downloadManager.animeDownloadedTypes.map { it.titleName.findValidName() }.distinct()
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
for (title in animeTitles) {
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.titleName.findValidName() == title }
val download = tDownloads.firstOrNull() ?: continue
val offlineAnimeModel = loadOfflineAnimeModel(download)
newAnimeDownloads += offlineAnimeModel
}
downloads = newAnimeDownloads
withContext(Dispatchers.Main) {
adapter.setItems(downloads)
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
adapter.notifyDataSetChanged()
}
}
downloads = newAnimeDownloads
}
private fun getMedia(downloadedType: DownloadedType): Media? {
val type = when (downloadedType.type) {
DownloadedType.Type.MANGA -> "Manga"
DownloadedType.Type.ANIME -> "Anime"
else -> "Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
/**
* Load media.json file from the directory and convert it to Media class
* @param downloadedType DownloadedType object
* @return Media object
*/
private suspend fun getMedia(downloadedType: DownloadedType): Media? {
return try {
val directory = DownloadsManager.getSubDirectory(
context ?: currContext()!!, downloadedType.type,
false, downloadedType.titleName
)
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
@@ -314,38 +327,44 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
SEpisodeImpl() // Provide an instance of SEpisodeImpl
})
.create()
val media = File(directory, "media.json")
val mediaJson = media.readText()
val media = directory?.findFile("media.json")
?: return loadMediaCompat(downloadedType)
val mediaJson =
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
it?.readText()
}
?: return null
gson.fromJson(mediaJson, Media::class.java)
} catch (e: Exception) {
logger("Error loading media.json: ${e.message}")
logger(e.printStackTrace())
Logger.log("Error loading media.json: ${e.message}")
Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e)
null
}
}
private fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel {
val type = when (downloadedType.type) {
DownloadedType.Type.MANGA -> "Manga"
DownloadedType.Type.ANIME -> "Anime"
else -> "Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
/**
* Load OfflineAnimeModel from the directory
* @param downloadedType DownloadedType object
* @return OfflineAnimeModel object
*/
private suspend fun loadOfflineAnimeModel(downloadedType: DownloadedType): OfflineAnimeModel {
val type = downloadedType.type.asText()
try {
val directory = DownloadsManager.getSubDirectory(
context ?: currContext()!!, downloadedType.type,
false, downloadedType.titleName
)
val mediaModel = getMedia(downloadedType)!!
val cover = File(directory, "cover.jpg")
val coverUri: Uri? = if (cover.exists()) {
Uri.fromFile(cover)
val cover = directory?.findFile("cover.jpg")
val coverUri: Uri? = if (cover?.exists() == true) {
cover.uri
} else null
val banner = File(directory, "banner.jpg")
val bannerUri: Uri? = if (banner.exists()) {
Uri.fromFile(banner)
val banner = directory?.findFile("banner.jpg")
val bannerUri: Uri? = if (banner?.exists() == true) {
banner.uri
} else null
if (coverUri == null && bannerUri == null) throw Exception("No cover or banner found, probably compat")
val title = mediaModel.mainName()
val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore
?: 0) else mediaModel.userScore) / 10.0).toString()
@@ -374,22 +393,26 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
bannerUri
)
} catch (e: Exception) {
logger("Error loading media.json: ${e.message}")
logger(e.printStackTrace())
Injekt.get<CrashlyticsInterface>().logException(e)
return OfflineAnimeModel(
"unknown",
"0",
"??",
"??",
"??",
"movie",
"hmm",
isOngoing = false,
isUserScored = false,
null,
null
)
return try {
loadOfflineAnimeModelCompat(downloadedType)
} catch (e: Exception) {
Logger.log("Error loading media.json: ${e.message}")
Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e)
OfflineAnimeModel(
"unknown",
"0",
"??",
"??",
"??",
"movie",
"hmm",
isOngoing = false,
isUserScored = false,
null,
null
)
}
}
}
}

View File

@@ -10,19 +10,20 @@ 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 androidx.documentfile.provider.DocumentFile
import ani.dantotsu.R
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaType
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
@@ -30,6 +31,10 @@ import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_PROG
import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STARTED
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import com.anggrayudi.storage.file.deleteRecursively
import com.anggrayudi.storage.file.forceDelete
import com.anggrayudi.storage.file.openOutputStream
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_DOWNLOADER_PROGRESS
@@ -37,8 +42,8 @@ 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.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
@@ -47,10 +52,9 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import tachiyomi.core.util.lang.launchIO
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.io.FileOutputStream
import java.net.HttpURLConnection
import java.net.URL
import java.util.Queue
@@ -187,13 +191,20 @@ class MangaDownloaderService : Service() {
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())
}
getSubDirectory(
this@MangaDownloaderService,
MediaType.MANGA,
false,
task.title,
task.chapter
)?.deleteRecursively(this@MangaDownloaderService)
// Loop through each ImageData object from the task
var farthest = 0
for ((index, image) in task.imageData.withIndex()) {
@@ -209,8 +220,7 @@ class MangaDownloaderService : Service() {
while (bitmap == null && retryCount < task.retries) {
bitmap = image.fetchAndProcessImage(
image.page,
image.source,
this@MangaDownloaderService
image.source
)
retryCount++
}
@@ -244,14 +254,14 @@ class MangaDownloaderService : Service() {
DownloadedType(
task.title,
task.chapter,
DownloadedType.Type.MANGA
MediaType.MANGA
)
)
broadcastDownloadFinished(task.chapter)
snackString("${task.title} - ${task.chapter} Download finished")
}
} catch (e: Exception) {
logger("Exception while downloading file: ${e.message}")
Logger.log("Exception while downloading file: ${e.message}")
snackString("Exception while downloading file: ${e.message}")
Injekt.get<CrashlyticsInterface>().logException(e)
broadcastDownloadFailed(task.chapter)
@@ -262,24 +272,18 @@ class MangaDownloaderService : Service() {
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)
val directory = getSubDirectory(this, MediaType.MANGA, false, title, chapter)
?: throw Exception("Directory not found")
directory.findFile(fileName)?.forceDelete(this)
// Create a file reference within that directory for the image
val file =
directory.createFile("image/jpeg", fileName) ?: throw Exception("File not created")
// Use a FileOutputStream to write the bitmap to the file
FileOutputStream(file).use { outputStream ->
file.openOutputStream(this, false).use { outputStream ->
if (outputStream == null) throw Exception("Output stream is null")
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
}
} catch (e: Exception) {
println("Exception while saving image: ${e.message}")
snackString("Exception while saving image: ${e.message}")
@@ -287,15 +291,15 @@ class MangaDownloaderService : Service() {
}
}
@OptIn(DelicateCoroutinesApi::class)
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")
launchIO {
val directory =
getSubDirectory(this@MangaDownloaderService, MediaType.MANGA, false, task.title)
?: throw Exception("Directory not found")
directory.findFile("media.json")?.forceDelete(this@MangaDownloaderService)
val file = directory.createFile("application/json", "media.json")
?: throw Exception("File not created")
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
@@ -310,7 +314,10 @@ class MangaDownloaderService : Service() {
val jsonString = gson.toJson(media)
withContext(Dispatchers.Main) {
try {
file.writeText(jsonString)
file.openOutputStream(this@MangaDownloaderService, false).use { output ->
if (output == null) throw Exception("Output stream is null")
output.write(jsonString.toByteArray())
}
} catch (e: android.system.ErrnoException) {
e.printStackTrace()
Toast.makeText(
@@ -325,7 +332,7 @@ class MangaDownloaderService : Service() {
}
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
withContext(Dispatchers.IO) {
var connection: HttpURLConnection? = null
println("Downloading url $url")
@@ -335,14 +342,16 @@ class MangaDownloaderService : Service() {
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
throw Exception("Server returned HTTP ${connection.responseCode} ${connection.responseMessage}")
}
val file = File(directory, name)
FileOutputStream(file).use { output ->
directory.findFile(name)?.forceDelete(this@MangaDownloaderService)
val file =
directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
file.openOutputStream(this@MangaDownloaderService, false).use { output ->
if (output == null) throw Exception("Output stream is null")
connection.inputStream.use { input ->
input.copyTo(output)
}
}
return@withContext file.absolutePath
return@withContext file.uri.toString()
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {

View File

@@ -1,6 +1,5 @@
package ani.dantotsu.download.manga
import android.annotation.SuppressLint
import android.content.Context
import android.view.LayoutInflater
import android.view.View
@@ -37,7 +36,6 @@ class OfflineMangaAdapter(
return position.toLong()
}
@SuppressLint("SetTextI18n")
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view: View = convertView ?: when (style) {
@@ -50,7 +48,6 @@ class OfflineMangaAdapter(
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)
@@ -60,14 +57,14 @@ class OfflineMangaAdapter(
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)
chapters.text = context.getString(R.string.chapters)
bannerView.setImageURI(item.banner ?: item.image)
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
totalChapter.text = context.getString(R.string.total_divider, item.totalChapter)
}
// Bind item data to the views

View File

@@ -3,10 +3,8 @@ package ani.dantotsu.download.manga
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.text.Editable
import android.text.TextWatcher
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -20,25 +18,35 @@ import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.marginBottom
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import ani.dantotsu.R
import ani.dantotsu.bottomBar
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.currActivity
import ani.dantotsu.currContext
import ani.dantotsu.download.DownloadCompat
import ani.dantotsu.download.DownloadCompat.Companion.loadOfflineMangaModelCompat
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.compareName
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.download.findValidName
import ani.dantotsu.getThemeColor
import ani.dantotsu.initActivity
import ani.dantotsu.logger
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.media.MediaType
import ani.dantotsu.navBarHeight
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.SettingsDialogFragment
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import com.anggrayudi.storage.file.openInputStream
import com.google.android.material.card.MaterialCardView
import com.google.android.material.imageview.ShapeableImageView
import com.google.android.material.textfield.TextInputLayout
@@ -46,9 +54,13 @@ 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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
@@ -57,6 +69,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private lateinit var gridView: GridView
private lateinit var adapter: OfflineMangaAdapter
private lateinit var total: TextView
private var downloadsJob: Job = Job()
override fun onCreateView(
inflater: LayoutInflater,
@@ -72,9 +85,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
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 color = requireContext().getThemeColor(android.R.attr.windowBackground)
val animeUserAvatar = view.findViewById<ShapeableImageView>(R.id.offlineMangaUserAvatar)
animeUserAvatar.setSafeOnClickListener {
@@ -146,11 +157,11 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
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)
getDownloads()
gridView.adapter = adapter
gridView.scheduleLayoutAnimation()
total.text =
@@ -159,17 +170,18 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
// 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 }
downloadManager.mangaDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) }
?: downloadManager.novelDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) }
media?.let {
ContextCompat.startActivity(
requireActivity(),
Intent(requireContext(), MediaDetailsActivity::class.java)
.putExtra("media", getMedia(it))
.putExtra("download", true),
null
)
lifecycleScope.launch {
ContextCompat.startActivity(
requireActivity(),
Intent(requireContext(), MediaDetailsActivity::class.java)
.putExtra("media", getMedia(it))
.putExtra("download", true),
null
)
}
} ?: run {
snackString("no media found")
}
@@ -178,11 +190,11 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
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
val type: MediaType =
if (downloadManager.mangaDownloadedTypes.any { it.titleName == item.title }) {
MediaType.MANGA
} else {
DownloadedType.Type.NOVEL
MediaType.NOVEL
}
// Alert dialog to confirm deletion
val builder =
@@ -192,9 +204,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
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
@@ -223,7 +232,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
// Implement behavior for different scroll states if needed
}
override fun onScroll(
@@ -234,7 +242,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
) {
val first = view.getChildAt(0)
val visibility = first != null && first.top < 0
scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE
scrollTop.isVisible = visibility
scrollTop.translationY =
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
}
@@ -246,7 +254,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
override fun onResume() {
super.onResume()
getDownloads()
adapter.notifyDataSetChanged()
}
override fun onPause() {
@@ -266,90 +273,103 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private fun getDownloads() {
downloads = listOf()
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct()
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
for (title in mangaTitles) {
val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
val download = tDownloads.first()
val offlineMangaModel = loadOfflineMangaModel(download)
newMangaDownloads += offlineMangaModel
if (downloadsJob.isActive) {
downloadsJob.cancel()
}
downloads = newMangaDownloads
val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct()
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
for (title in novelTitles) {
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
val download = tDownloads.first()
val offlineMangaModel = loadOfflineMangaModel(download)
newNovelDownloads += offlineMangaModel
downloads = listOf()
downloadsJob = Job()
CoroutineScope(Dispatchers.IO + downloadsJob).launch {
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.titleName.findValidName() }.distinct()
val newMangaDownloads = mutableListOf<OfflineMangaModel>()
for (title in mangaTitles) {
val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.titleName.findValidName() == title }
val download = tDownloads.firstOrNull() ?: continue
val offlineMangaModel = loadOfflineMangaModel(download)
newMangaDownloads += offlineMangaModel
}
downloads = newMangaDownloads
val novelTitles = downloadManager.novelDownloadedTypes.map { it.titleName }.distinct()
val newNovelDownloads = mutableListOf<OfflineMangaModel>()
for (title in novelTitles) {
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.titleName.findValidName() == title }
val download = tDownloads.firstOrNull() ?: continue
val offlineMangaModel = loadOfflineMangaModel(download)
newNovelDownloads += offlineMangaModel
}
downloads += newNovelDownloads
withContext(Dispatchers.Main) {
adapter.setItems(downloads)
total.text =
if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
adapter.notifyDataSetChanged()
}
}
downloads += newNovelDownloads
}
private fun getMedia(downloadedType: DownloadedType): Media? {
val type = when (downloadedType.type) {
DownloadedType.Type.MANGA -> "Manga"
DownloadedType.Type.ANIME -> "Anime"
else -> "Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
/**
* Load media.json file from the directory and convert it to Media class
* @param downloadedType DownloadedType object
* @return Media object
*/
private suspend fun getMedia(downloadedType: DownloadedType): Media? {
return try {
val directory = getSubDirectory(
context ?: currContext()!!, downloadedType.type,
false, downloadedType.titleName
)
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()
val media = directory?.findFile("media.json")
?: return DownloadCompat.loadMediaCompat(downloadedType)
val mediaJson =
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
it?.readText()
}
gson.fromJson(mediaJson, Media::class.java)
} catch (e: Exception) {
logger("Error loading media.json: ${e.message}")
logger(e.printStackTrace())
Logger.log("Error loading media.json: ${e.message}")
Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e)
null
}
}
private fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
val type = when (downloadedType.type) {
DownloadedType.Type.MANGA -> "Manga"
DownloadedType.Type.ANIME -> "Anime"
else -> "Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.title}"
)
private suspend fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
val type = downloadedType.type.asText()
//load media.json and convert to media class with gson
try {
val directory = getSubDirectory(
context ?: currContext()!!, downloadedType.type,
false, downloadedType.titleName
)
val mediaModel = getMedia(downloadedType)!!
val cover = File(directory, "cover.jpg")
val coverUri: Uri? = if (cover.exists()) {
Uri.fromFile(cover)
val cover = directory?.findFile("cover.jpg")
val coverUri: Uri? = if (cover?.exists() == true) {
cover.uri
} else null
val banner = File(directory, "banner.jpg")
val bannerUri: Uri? = if (banner.exists()) {
Uri.fromFile(banner)
val banner = directory?.findFile("banner.jpg")
val bannerUri: Uri? = if (banner?.exists() == true) {
banner.uri
} else null
if (coverUri == null && bannerUri == null) throw Exception("No cover or banner found, probably compat")
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 readChapter = (mediaModel.userProgress ?: "~").toString()
val totalChapter = "${mediaModel.manga?.totalChapters ?: "??"}"
val chapters = " Chapters"
return OfflineMangaModel(
title,
score,
totalchapter,
readchapter,
totalChapter,
readChapter,
type,
chapters,
isOngoing,
@@ -358,21 +378,25 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
bannerUri
)
} catch (e: Exception) {
logger("Error loading media.json: ${e.message}")
logger(e.printStackTrace())
Injekt.get<CrashlyticsInterface>().logException(e)
return OfflineMangaModel(
"unknown",
"0",
"??",
"??",
"movie",
"hmm",
isOngoing = false,
isUserScored = false,
null,
null
)
return try {
loadOfflineMangaModelCompat(downloadedType)
} catch (e: Exception) {
Logger.log("Error loading media.json: ${e.message}")
Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e)
return OfflineMangaModel(
"unknown",
"0",
"??",
"??",
"movie",
"hmm",
isOngoing = false,
isUserScored = false,
null,
null
)
}
}
}
}

View File

@@ -9,21 +9,25 @@ 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.documentfile.provider.DocumentFile
import ani.dantotsu.R
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.logger
import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.novel.NovelReadFragment
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import com.anggrayudi.storage.file.forceDelete
import com.anggrayudi.storage.file.openOutputStream
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.data.notification.Notifications
@@ -31,8 +35,8 @@ 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.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
@@ -42,10 +46,9 @@ import kotlinx.coroutines.withContext
import okhttp3.Request
import okio.buffer
import okio.sink
import tachiyomi.core.util.lang.launchIO
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL
@@ -62,7 +65,7 @@ class NovelDownloaderService : Service() {
private val mutex = Mutex()
private var isCurrentlyProcessing = false
val networkHelper = Injekt.get<NetworkHelper>()
private val networkHelper = Injekt.get<NetworkHelper>()
override fun onBind(intent: Intent?): IBinder? {
// This is only required for bound services.
@@ -186,15 +189,15 @@ class NovelDownloaderService : Service() {
val contentType = response.header("Content-Type")
val contentDisposition = response.header("Content-Disposition")
logger("Content-Type: $contentType")
logger("Content-Disposition: $contentDisposition")
Logger.log("Content-Type: $contentType")
Logger.log("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}")
Logger.log("Error checking file type: ${e.message}")
false
}
}
@@ -225,12 +228,12 @@ class NovelDownloaderService : Service() {
if (!isEpubFile(task.downloadLink)) {
if (isAlreadyDownloaded(task.originalLink)) {
logger("Already downloaded")
Logger.log("Already downloaded")
broadcastDownloadFinished(task.originalLink)
snackString("Already downloaded")
return@withContext
}
logger("Download link is not an .epub file")
Logger.log("Download link is not an .epub file")
broadcastDownloadFailed(task.originalLink)
snackString("Download link is not an .epub file")
return@withContext
@@ -245,27 +248,30 @@ class NovelDownloaderService : Service() {
networkHelper.downloadClient.newCall(request).execute().use { response ->
// Ensure the response is successful and has a body
if (!response.isSuccessful || response.body == null) {
if (!response.isSuccessful) {
throw IOException("Failed to download file: ${response.message}")
}
val directory = getSubDirectory(
this@NovelDownloaderService,
MediaType.NOVEL,
false,
task.title,
task.chapter
) ?: throw Exception("Directory not found")
directory.findFile("0.epub")?.forceDelete(this@NovelDownloaderService)
val file = File(
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()
val file = directory.createFile("application/epub+zip", "0.epub")
?: throw Exception("File not created")
//download cover
task.coverUrl?.let {
file.parentFile?.let { it1 -> downloadImage(it, it1, "cover.jpg") }
}
val outputStream =
this@NovelDownloaderService.contentResolver.openOutputStream(file.uri)
?: throw Exception("Could not open OutputStream")
val sink = file.sink().buffer()
val sink = outputStream.sink().buffer()
val responseBody = response.body
val totalBytes = responseBody.contentLength()
var downloadedBytes = 0L
@@ -301,7 +307,7 @@ class NovelDownloaderService : Service() {
withContext(Dispatchers.Main) {
val progress =
(downloadedBytes * 100 / totalBytes).toInt()
logger("Download progress: $progress")
Logger.log("Download progress: $progress")
broadcastDownloadProgress(task.originalLink, progress)
}
lastBroadcastUpdate = downloadedBytes
@@ -316,7 +322,7 @@ class NovelDownloaderService : Service() {
}
}
} catch (e: Exception) {
logger("Exception while downloading .epub inside request: ${e.message}")
Logger.log("Exception while downloading .epub inside request: ${e.message}")
throw e
}
}
@@ -333,29 +339,33 @@ class NovelDownloaderService : Service() {
DownloadedType(
task.title,
task.chapter,
DownloadedType.Type.NOVEL
MediaType.NOVEL
)
)
broadcastDownloadFinished(task.originalLink)
snackString("${task.title} - ${task.chapter} Download finished")
}
} catch (e: Exception) {
logger("Exception while downloading .epub: ${e.message}")
Logger.log("Exception while downloading .epub: ${e.message}")
snackString("Exception while downloading .epub: ${e.message}")
Injekt.get<CrashlyticsInterface>().logException(e)
broadcastDownloadFailed(task.originalLink)
}
}
@OptIn(DelicateCoroutinesApi::class)
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")
launchIO {
val directory =
getSubDirectory(
this@NovelDownloaderService,
MediaType.NOVEL,
false,
task.title
) ?: throw Exception("Directory not found")
directory.findFile("media.json")?.forceDelete(this@NovelDownloaderService)
val file = directory.createFile("application/json", "media.json")
?: throw Exception("File not created")
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
@@ -369,33 +379,47 @@ class NovelDownloaderService : Service() {
val jsonString = gson.toJson(media)
withContext(Dispatchers.Main) {
file.writeText(jsonString)
try {
file.openOutputStream(this@NovelDownloaderService, false).use { output ->
if (output == null) throw Exception("Output stream is null")
output.write(jsonString.toByteArray())
}
} catch (e: android.system.ErrnoException) {
e.printStackTrace()
Toast.makeText(
this@NovelDownloaderService,
"Error while saving: ${e.localizedMessage}",
Toast.LENGTH_LONG
).show()
}
}
}
}
}
private suspend fun downloadImage(url: String, directory: File, name: String): String? =
private suspend fun downloadImage(url: String, directory: DocumentFile, name: String): String? =
withContext(
Dispatchers.IO
) {
var connection: HttpURLConnection? = null
println("Downloading url $url")
Logger.log("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 ->
directory.findFile(name)?.forceDelete(this@NovelDownloaderService)
val file =
directory.createFile("image/jpeg", name) ?: throw Exception("File not created")
file.openOutputStream(this@NovelDownloaderService, false).use { output ->
if (output == null) throw Exception("Output stream is null")
connection.inputStream.use { input ->
input.copyTo(output)
}
}
return@withContext file.absolutePath
return@withContext file.uri.toString()
} catch (e: Exception) {
e.printStackTrace()
withContext(Dispatchers.Main) {
@@ -470,7 +494,6 @@ class NovelDownloaderService : Service() {
}
object NovelServiceDataSingleton {
var sourceMedia: Media? = null
var downloadQueue: Queue<NovelDownloaderService.DownloadTask> = ConcurrentLinkedQueue()
@Volatile

View File

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

View File

@@ -7,15 +7,10 @@ import android.app.AlertDialog
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.util.Log
import androidx.annotation.OptIn
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.MimeTypes
import androidx.media3.common.util.UnstableApi
import androidx.media3.database.StandaloneDatabaseProvider
import androidx.media3.datasource.DataSource
@@ -23,11 +18,8 @@ import androidx.media3.datasource.HttpDataSource
import androidx.media3.datasource.cache.NoOpCacheEvictor
import androidx.media3.datasource.cache.SimpleCache
import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.offline.Download
import androidx.media3.exoplayer.offline.DownloadHelper
import androidx.media3.exoplayer.offline.DownloadManager
import androidx.media3.exoplayer.offline.DownloadService
import androidx.media3.exoplayer.scheduler.Requirements
import ani.dantotsu.R
import ani.dantotsu.defaultHeaders
@@ -35,92 +27,101 @@ 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.media.Media
import ani.dantotsu.okHttpClient
import ani.dantotsu.media.MediaType
import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.SubtitleType
import ani.dantotsu.parsers.Video
import ani.dantotsu.parsers.VideoType
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.util.Logger
import eu.kanade.tachiyomi.network.NetworkHelper
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.io.IOException
import java.util.concurrent.*
import java.util.concurrent.Executors
@SuppressLint("UnsafeOptInUsageError")
object Helper {
private var simpleCache: SimpleCache? = null
@SuppressLint("UnsafeOptInUsageError")
fun downloadVideo(context: Context, video: Video, subtitle: Subtitle?) {
val dataSourceFactory = DataSource.Factory {
val dataSource: HttpDataSource =
OkHttpDataSource.Factory(okHttpClient).createDataSource()
defaultHeaders.forEach {
dataSource.setRequestProperty(it.key, it.value)
}
video.file.headers.forEach {
dataSource.setRequestProperty(it.key, it.value)
}
dataSource
}
val mimeType = when (video.format) {
VideoType.M3U8 -> MimeTypes.APPLICATION_M3U8
VideoType.DASH -> MimeTypes.APPLICATION_MPD
else -> MimeTypes.APPLICATION_MP4
}
val builder = MediaItem.Builder().setUri(video.file.url).setMimeType(mimeType)
var sub: MediaItem.SubtitleConfiguration? = null
if (subtitle != null) {
sub = MediaItem.SubtitleConfiguration
.Builder(Uri.parse(subtitle.file.url))
.setSelectionFlags(C.SELECTION_FLAG_FORCED)
.setMimeType(
when (subtitle.type) {
SubtitleType.VTT -> MimeTypes.TEXT_VTT
SubtitleType.ASS -> MimeTypes.TEXT_SSA
SubtitleType.SRT -> MimeTypes.APPLICATION_SUBRIP
SubtitleType.UNKNOWN -> MimeTypes.TEXT_SSA
}
@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
)
.build()
}
}
if (sub != null) builder.setSubtitleConfigurations(mutableListOf(sub))
val mediaItem = builder.build()
val downloadHelper = DownloadHelper.forMediaItem(
context,
mediaItem,
DefaultRenderersFactory(context),
dataSourceFactory
)
downloadHelper.prepare(object : DownloadHelper.Callback {
override fun onPrepared(helper: DownloadHelper) {
helper.getDownloadRequest(null).let {
DownloadService.sendAddDownload(
context,
ExoplayerDownloadService::class.java,
it,
false
)
}
}
override fun onPrepareError(helper: DownloadHelper, e: IOException) {
logError(e)
val animeDownloadTask = AnimeDownloaderService.AnimeDownloadTask(
title,
episode,
video,
subtitle,
sourceMedia,
episodeImage
)
val downloadsManger = Injekt.get<DownloadsManager>()
val downloadCheck = downloadsManger
.queryDownload(title, episode, MediaType.ANIME)
if (downloadCheck) {
AlertDialog.Builder(context, R.style.MyPopup)
.setTitle("Download Exists")
.setMessage("A download for this episode already exists. Do you want to overwrite it?")
.setPositiveButton("Yes") { _, _ ->
PrefManager.getAnimeDownloadPreferences().edit()
.remove(animeDownloadTask.getTaskName())
.apply()
downloadsManger.removeDownload(
DownloadedType(
title,
episode,
MediaType.ANIME
)
) {
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
if (!AnimeServiceDataSingleton.isServiceRunning) {
val intent = Intent(context, AnimeDownloaderService::class.java)
ContextCompat.startForegroundService(context, intent)
AnimeServiceDataSingleton.isServiceRunning = true
}
}
}
.setNegativeButton("No") { _, _ -> }
.show()
} else {
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
if (!AnimeServiceDataSingleton.isServiceRunning) {
val intent = Intent(context, AnimeDownloaderService::class.java)
ContextCompat.startForegroundService(context, intent)
AnimeServiceDataSingleton.isServiceRunning = true
}
})
}
}
private var download: DownloadManager? = null
private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads"
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
}
@Synchronized
@UnstableApi
@Deprecated("exoplayer download manager is no longer used")
fun downloadManager(context: Context): DownloadManager {
return download ?: let {
val database = Injekt.get<StandaloneDatabaseProvider>()
@@ -157,15 +158,15 @@ object Helper {
finalException: Exception?
) {
if (download.state == Download.STATE_COMPLETED) {
Log.e("Downloader", "Download Completed")
Logger.log("Download Completed")
} else if (download.state == Download.STATE_FAILED) {
Log.e("Downloader", "Download Failed")
Logger.log("Download Failed")
} else if (download.state == Download.STATE_STOPPED) {
Log.e("Downloader", "Download Stopped")
Logger.log("Download Stopped")
} else if (download.state == Download.STATE_QUEUED) {
Log.e("Downloader", "Download Queued")
Logger.log("Download Queued")
} else if (download.state == Download.STATE_DOWNLOADING) {
Log.e("Downloader", "Download Downloading")
Logger.log("Download Downloading")
}
}
}
@@ -174,96 +175,7 @@ object Helper {
downloadManager
}
}
private var downloadDirectory: File? = null
@Synchronized
private fun getDownloadDirectory(context: Context): File {
if (downloadDirectory == null) {
downloadDirectory = context.getExternalFilesDir(null)
if (downloadDirectory == null) {
downloadDirectory = context.filesDir
}
}
return downloadDirectory!!
}
@OptIn(UnstableApi::class)
fun startAnimeDownloadService(
context: Context,
title: String,
episode: String,
video: Video,
subtitle: Subtitle? = null,
sourceMedia: Media? = null,
episodeImage: String? = null
) {
if (!isNotificationPermissionGranted(context)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ActivityCompat.requestPermissions(
context as Activity,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
1
)
}
}
val animeDownloadTask = AnimeDownloaderService.AnimeDownloadTask(
title,
episode,
video,
subtitle,
sourceMedia,
episodeImage
)
val downloadsManger = Injekt.get<DownloadsManager>()
val downloadCheck = downloadsManger
.queryDownload(title, episode, DownloadedType.Type.ANIME)
if (downloadCheck) {
AlertDialog.Builder(context, R.style.MyPopup)
.setTitle("Download Exists")
.setMessage("A download for this episode already exists. Do you want to overwrite it?")
.setPositiveButton("Yes") { _, _ ->
DownloadService.sendRemoveDownload(
context,
ExoplayerDownloadService::class.java,
PrefManager.getAnimeDownloadPreferences().getString(
animeDownloadTask.getTaskName(),
""
) ?: "",
false
)
PrefManager.getAnimeDownloadPreferences().edit()
.remove(animeDownloadTask.getTaskName())
.apply()
downloadsManger.removeDownload(
DownloadedType(
title,
episode,
DownloadedType.Type.ANIME
)
)
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
if (!AnimeServiceDataSingleton.isServiceRunning) {
val intent = Intent(context, AnimeDownloaderService::class.java)
ContextCompat.startForegroundService(context, intent)
AnimeServiceDataSingleton.isServiceRunning = true
}
}
.setNegativeButton("No") { _, _ -> }
.show()
} else {
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
if (!AnimeServiceDataSingleton.isServiceRunning) {
val intent = Intent(context, AnimeDownloaderService::class.java)
ContextCompat.startForegroundService(context, intent)
AnimeServiceDataSingleton.isServiceRunning = true
}
}
}
@Deprecated("exoplayer download manager is no longer used")
@OptIn(UnstableApi::class)
fun getSimpleCache(context: Context): SimpleCache {
return if (simpleCache == null) {
@@ -275,14 +187,23 @@ object Helper {
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
@Synchronized
@Deprecated("exoplayer download manager is no longer used")
private fun getDownloadDirectory(context: Context): File {
if (downloadDirectory == null) {
downloadDirectory = context.getExternalFilesDir(null)
if (downloadDirectory == null) {
downloadDirectory = context.filesDir
}
}
return true
return downloadDirectory!!
}
@Deprecated("exoplayer download manager is no longer used")
private var download: DownloadManager? = null
@Deprecated("exoplayer download manager is no longer used")
private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads"
@Deprecated("exoplayer download manager is no longer used")
private var simpleCache: SimpleCache? = null
@Deprecated("exoplayer download manager is no longer used")
private var downloadDirectory: File? = null
}

View File

@@ -36,6 +36,7 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -204,7 +205,22 @@ class AnimeFragment : Fragment() {
if (i) {
model.getUpdated().observe(viewLifecycleOwner) {
if (it != null) {
animePageAdapter.updateRecent(MediaAdaptor(0, it, requireActivity()))
animePageAdapter.updateRecent(MediaAdaptor(0, it, requireActivity()), it)
}
}
model.getMovies().observe(viewLifecycleOwner) {
if (it != null) {
animePageAdapter.updateMovies(MediaAdaptor(0, it, requireActivity()), it)
}
}
model.getTopRated().observe(viewLifecycleOwner) {
if (it != null) {
animePageAdapter.updateTopRated(MediaAdaptor(0, it, requireActivity()), it)
}
}
model.getMostFav().observe(viewLifecycleOwner) {
if (it != null) {
animePageAdapter.updateMostFav(MediaAdaptor(0, it, requireActivity()), it)
}
}
if (animePageAdapter.trendingViewPager != null) {
@@ -253,17 +269,29 @@ class AnimeFragment : Fragment() {
true
}
var running = false
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(false) }
live.observe(viewLifecycleOwner) {
if (it) {
if (it && !running) {
running = true
scope.launch {
withContext(Dispatchers.IO) {
getUserId(requireContext()) {
load()
Anilist.userid = PrefManager.getNullableVal<String>(PrefName.AnilistUserId, null)
?.toIntOrNull()
if (Anilist.userid == null) {
getUserId(requireContext()) {
load()
}
} else {
CoroutineScope(Dispatchers.IO).launch {
getUserId(requireContext()) {
load()
}
}
}
model.loaded = true
model.loadTrending(1)
model.loadUpdated()
model.loadAll()
model.loadPopular(
"ANIME", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
PrefName.PopularAnimeList
@@ -272,6 +300,7 @@ class AnimeFragment : Fragment() {
}
live.postValue(false)
_binding?.animeRefresh?.isRefreshing = false
running = false
}
}
}
@@ -283,7 +312,9 @@ class AnimeFragment : Fragment() {
binding.root.requestApplyInsets()
binding.root.requestLayout()
}
if (this::animePageAdapter.isInitialized && _binding != null) {
animePageAdapter.updateNotificationCount()
}
super.onResume()
}
}

View File

@@ -3,15 +3,17 @@ package ani.dantotsu.home
import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.util.TypedValue
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.LayoutAnimationController
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -21,11 +23,17 @@ import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemAnimePageBinding
import ani.dantotsu.databinding.LayoutTrendingBinding
import ani.dantotsu.getAppString
import ani.dantotsu.getThemeColor
import ani.dantotsu.loadImage
import ani.dantotsu.media.CalendarActivity
import ani.dantotsu.media.GenreActivity
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaAdaptor
import ani.dantotsu.media.MediaListViewActivity
import ani.dantotsu.media.SearchActivity
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.px
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.setSlideIn
@@ -40,6 +48,7 @@ import com.google.android.material.textfield.TextInputLayout
class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHolder>() {
val ready = MutableLiveData(false)
lateinit var binding: ItemAnimePageBinding
private lateinit var trendingBinding: LayoutTrendingBinding
private var trendHandler: Handler? = null
private lateinit var trendRun: Runnable
var trendingViewPager: ViewPager2? = null
@@ -52,32 +61,30 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
override fun onBindViewHolder(holder: AnimePageViewHolder, position: Int) {
binding = holder.binding
trendingViewPager = binding.animeTrendingViewPager
trendingBinding = LayoutTrendingBinding.bind(binding.root)
trendingViewPager = trendingBinding.trendingViewPager
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.animeSearchBar)
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.searchBar)
val currentColor = textInputLayout.boxBackgroundColor
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
textInputLayout.boxBackgroundColor = semiTransparentColor
val materialCardView =
holder.itemView.findViewById<MaterialCardView>(R.id.animeUserAvatarContainer)
holder.itemView.findViewById<MaterialCardView>(R.id.userAvatarContainer)
materialCardView.setCardBackgroundColor(semiTransparentColor)
val typedValue = TypedValue()
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
val color = typedValue.data
val color = binding.root.context.getThemeColor(android.R.attr.windowBackground)
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
binding.animeTitleContainer.updatePadding(top = statusBarHeight)
trendingBinding.titleContainer.updatePadding(top = statusBarHeight)
if (PrefManager.getVal(PrefName.SmallView)) binding.animeTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
if (PrefManager.getVal(PrefName.SmallView)) trendingBinding.trendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = (-108f).px
}
updateAvatar()
binding.animeSearchBar.hint = "ANIME"
binding.animeSearchBarText.setOnClickListener {
trendingBinding.searchBar.hint = "ANIME"
trendingBinding.searchBarText.setOnClickListener {
ContextCompat.startActivity(
it.context,
Intent(it.context, SearchActivity::class.java).putExtra("type", "ANIME"),
@@ -85,15 +92,28 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
)
}
binding.animeSearchBar.setEndIconOnClickListener {
binding.animeSearchBarText.performClick()
}
binding.animeUserAvatar.setSafeOnClickListener {
trendingBinding.userAvatar.setSafeOnClickListener {
val dialogFragment =
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.ANIME)
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
}
trendingBinding.userAvatar.setOnLongClickListener { view ->
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
ContextCompat.startActivity(
view.context,
Intent(view.context, ProfileActivity::class.java)
.putExtra("userId", Anilist.userid), null
)
false
}
trendingBinding.searchBar.setEndIconOnClickListener {
trendingBinding.searchBar.performClick()
}
trendingBinding.notificationCount.visibility =
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
listOf(
binding.animePreviousSeason,
@@ -122,8 +142,7 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
)
}
binding.animeIncludeList.visibility =
if (Anilist.userid != null) View.VISIBLE else View.GONE
binding.animeIncludeList.isVisible = Anilist.userid != null
binding.animeIncludeList.isChecked = PrefManager.getVal(PrefName.PopularAnimeList)
@@ -147,59 +166,137 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
}
fun updateTrending(adaptor: MediaAdaptor) {
binding.animeTrendingProgressBar.visibility = View.GONE
binding.animeTrendingViewPager.adapter = adaptor
binding.animeTrendingViewPager.offscreenPageLimit = 3
binding.animeTrendingViewPager.getChildAt(0).overScrollMode = RecyclerView.OVER_SCROLL_NEVER
binding.animeTrendingViewPager.setPageTransformer(MediaPageTransformer())
trendingBinding.trendingProgressBar.visibility = View.GONE
trendingBinding.trendingViewPager.adapter = adaptor
trendingBinding.trendingViewPager.offscreenPageLimit = 3
trendingBinding.trendingViewPager.getChildAt(0).overScrollMode =
RecyclerView.OVER_SCROLL_NEVER
trendingBinding.trendingViewPager.setPageTransformer(MediaPageTransformer())
trendHandler = Handler(Looper.getMainLooper())
trendRun = Runnable {
binding.animeTrendingViewPager.currentItem =
binding.animeTrendingViewPager.currentItem + 1
trendingBinding.trendingViewPager.currentItem += 1
}
binding.animeTrendingViewPager.registerOnPageChangeCallback(
trendingBinding.trendingViewPager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
trendHandler!!.removeCallbacks(trendRun)
trendHandler!!.postDelayed(trendRun, 4000)
trendHandler?.removeCallbacks(trendRun)
if (PrefManager.getVal(PrefName.TrendingScroller)) {
trendHandler!!.postDelayed(trendRun, 4000)
}
}
}
)
binding.animeTrendingViewPager.layoutAnimation =
trendingBinding.trendingViewPager.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
binding.animeTitleContainer.startAnimation(setSlideUp())
trendingBinding.titleContainer.startAnimation(setSlideUp())
binding.animeListContainer.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
binding.animeSeasonsCont.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
}
fun updateRecent(adaptor: MediaAdaptor) {
binding.animeUpdatedProgressBar.visibility = View.GONE
binding.animeUpdatedRecyclerView.adapter = adaptor
binding.animeUpdatedRecyclerView.layoutManager =
fun updateRecent(adaptor: MediaAdaptor, media: MutableList<Media>) {
binding.apply {
init(
adaptor,
animeUpdatedRecyclerView,
animeUpdatedProgressBar,
animeRecently,
animeRecentlyMore,
getAppString(R.string.updated),
media
)
animePopular.visibility = View.VISIBLE
animePopular.startAnimation(setSlideUp())
if (adaptor.itemCount == 0) {
animeRecentlyContainer.visibility = View.GONE
}
}
}
fun updateMovies(adaptor: MediaAdaptor, media: MutableList<Media>) {
binding.apply {
init(
adaptor,
animeMoviesRecyclerView,
animeMoviesProgressBar,
animeMovies,
animeMoviesMore,
getAppString(R.string.trending_movies),
media
)
}
}
fun updateTopRated(adaptor: MediaAdaptor, media: MutableList<Media>) {
binding.apply {
init(
adaptor,
animeTopRatedRecyclerView,
animeTopRatedProgressBar,
animeTopRated,
animeTopRatedMore,
getAppString(R.string.top_rated),
media
)
}
}
fun updateMostFav(adaptor: MediaAdaptor, media: MutableList<Media>) {
binding.apply {
init(
adaptor,
animeMostFavRecyclerView,
animeMostFavProgressBar,
animeMostFav,
animeMostFavMore,
getAppString(R.string.most_favourite),
media
)
}
}
fun init(adaptor: MediaAdaptor, recyclerView: RecyclerView, progress: View, title: View , more: View , string: String, media : MutableList<Media>) {
progress.visibility = View.GONE
recyclerView.adapter = adaptor
recyclerView.layoutManager =
LinearLayoutManager(
binding.animeUpdatedRecyclerView.context,
recyclerView.context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.animeUpdatedRecyclerView.visibility = View.VISIBLE
binding.animeRecently.visibility = View.VISIBLE
binding.animeRecently.startAnimation(setSlideUp())
binding.animeUpdatedRecyclerView.layoutAnimation =
more.setOnClickListener {
ContextCompat.startActivity(
it.context, Intent(it.context, MediaListViewActivity::class.java)
.putExtra("title", string)
.putExtra("media", media as ArrayList<Media>),
null
)
}
recyclerView.visibility = View.VISIBLE
title.visibility = View.VISIBLE
more.visibility = View.VISIBLE
title.startAnimation(setSlideUp())
more.startAnimation(setSlideUp())
recyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
binding.animePopular.visibility = View.VISIBLE
binding.animePopular.startAnimation(setSlideUp())
}
fun updateAvatar() {
if (Anilist.avatar != null && ready.value == true) {
binding.animeUserAvatar.loadImage(Anilist.avatar)
binding.animeUserAvatar.imageTintList = null
trendingBinding.userAvatar.loadImage(Anilist.avatar)
trendingBinding.userAvatar.imageTintList = null
}
}
fun updateNotificationCount() {
if (this::binding.isInitialized) {
trendingBinding.notificationCount.visibility =
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
}
}

View File

@@ -5,11 +5,13 @@ import android.content.Intent
import android.graphics.drawable.Animatable
import android.os.Build
import android.os.Bundle
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.LayoutAnimationController
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
@@ -21,28 +23,34 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.blurImage
import ani.dantotsu.bottomBar
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistHomeViewModel
import ani.dantotsu.connections.anilist.getUserId
import ani.dantotsu.currContext
import ani.dantotsu.databinding.FragmentHomeBinding
import ani.dantotsu.home.status.UserStatusAdapter
import ani.dantotsu.loadImage
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaAdaptor
import ani.dantotsu.media.MediaListViewActivity
import ani.dantotsu.media.user.ListActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.setSlideIn
import ani.dantotsu.setSlideUp
import ani.dantotsu.settings.SettingsDialogFragment
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefManager.asLiveBool
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import ani.dantotsu.util.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlin.math.max
import kotlin.math.min
@@ -51,7 +59,6 @@ import kotlin.math.min
class HomeFragment : Fragment() {
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -70,15 +77,22 @@ class HomeFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val scope = lifecycleScope
Logger.log("HomeFragment")
fun load() {
Logger.log("Loading HomeFragment")
if (activity != null && _binding != null) lifecycleScope.launch(Dispatchers.Main) {
binding.homeUserName.text = Anilist.username
binding.homeUserEpisodesWatched.text = Anilist.episodesWatched.toString()
binding.homeUserChaptersRead.text = Anilist.chapterRead.toString()
binding.homeUserAvatar.loadImage(Anilist.avatar)
if (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean)) binding.homeUserBg.pause()
binding.homeUserBg.loadImage(Anilist.bg)
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
blurImage(
if (bannerAnimations) binding.homeUserBg else binding.homeUserBgNoKen,
Anilist.bg
)
binding.homeUserDataProgressBar.visibility = View.GONE
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
binding.homeAnimeList.setOnClickListener {
ContextCompat.startActivity(
@@ -118,26 +132,38 @@ class HomeFragment : Fragment() {
"dialog"
)
}
binding.homeUserAvatarContainer.setOnLongClickListener {
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
ContextCompat.startActivity(
requireContext(), Intent(requireContext(), ProfileActivity::class.java)
.putExtra("userId", Anilist.userid), null
)
false
}
binding.homeContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
binding.homeUserBg.updateLayoutParams { height += statusBarHeight }
binding.homeUserBgNoKen.updateLayoutParams { height += statusBarHeight }
binding.homeTopContainer.updatePadding(top = statusBarHeight)
var reached = false
val duration = ((PrefManager.getVal(PrefName.AnimationSpeed) as Float) * 200).toLong()
binding.homeScroll.setOnScrollChangeListener { _, _, _, _, _ ->
if (!binding.homeScroll.canScrollVertically(1)) {
reached = true
bottomBar.animate().translationZ(0f).setDuration(duration).start()
ObjectAnimator.ofFloat(bottomBar, "elevation", 4f, 0f).setDuration(duration)
.start()
} else {
if (reached) {
bottomBar.animate().translationZ(12f).setDuration(duration).start()
ObjectAnimator.ofFloat(bottomBar, "elevation", 0f, 4f).setDuration(duration)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
binding.homeScroll.setOnScrollChangeListener { _, _, _, _, _ ->
if (!binding.homeScroll.canScrollVertically(1)) {
reached = true
bottomBar.animate().translationZ(0f).setDuration(duration).start()
ObjectAnimator.ofFloat(bottomBar, "elevation", 4f, 0f).setDuration(duration)
.start()
} else {
if (reached) {
bottomBar.animate().translationZ(12f).setDuration(duration).start()
ObjectAnimator.ofFloat(bottomBar, "elevation", 0f, 4f).setDuration(duration)
.start()
}
}
}
}
@@ -184,13 +210,16 @@ class HomeFragment : Fragment() {
recyclerView: RecyclerView,
progress: View,
empty: View,
title: View
title: View,
more: View,
string: String
) {
container.visibility = View.VISIBLE
progress.visibility = View.VISIBLE
recyclerView.visibility = View.GONE
empty.visibility = View.GONE
title.visibility = View.INVISIBLE
more.visibility = View.INVISIBLE
mode.observe(viewLifecycleOwner) {
recyclerView.visibility = View.GONE
@@ -203,6 +232,14 @@ class HomeFragment : Fragment() {
LinearLayoutManager.HORIZONTAL,
false
)
more.setOnClickListener { i ->
ContextCompat.startActivity(
i.context, Intent(i.context, MediaListViewActivity::class.java)
.putExtra("title", string)
.putExtra("media", it),
null
)
}
recyclerView.visibility = View.VISIBLE
recyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
@@ -210,7 +247,9 @@ class HomeFragment : Fragment() {
} else {
empty.visibility = View.VISIBLE
}
more.visibility = View.VISIBLE
title.visibility = View.VISIBLE
more.startAnimation(setSlideUp())
title.startAnimation(setSlideUp())
progress.visibility = View.GONE
}
@@ -225,7 +264,9 @@ class HomeFragment : Fragment() {
binding.homeWatchingRecyclerView,
binding.homeWatchingProgressBar,
binding.homeWatchingEmpty,
binding.homeContinueWatch
binding.homeContinueWatch,
binding.homeContinueWatchMore,
getString(R.string.continue_watching)
)
binding.homeWatchingBrowseButton.setOnClickListener {
bottomBar.selectTabAt(0)
@@ -237,7 +278,9 @@ class HomeFragment : Fragment() {
binding.homeFavAnimeRecyclerView,
binding.homeFavAnimeProgressBar,
binding.homeFavAnimeEmpty,
binding.homeFavAnime
binding.homeFavAnime,
binding.homeFavAnimeMore,
getString(R.string.fav_anime)
)
initRecyclerView(
@@ -246,7 +289,9 @@ class HomeFragment : Fragment() {
binding.homePlannedAnimeRecyclerView,
binding.homePlannedAnimeProgressBar,
binding.homePlannedAnimeEmpty,
binding.homePlannedAnime
binding.homePlannedAnime,
binding.homePlannedAnimeMore,
getString(R.string.planned_anime)
)
binding.homePlannedAnimeBrowseButton.setOnClickListener {
bottomBar.selectTabAt(0)
@@ -258,7 +303,9 @@ class HomeFragment : Fragment() {
binding.homeReadingRecyclerView,
binding.homeReadingProgressBar,
binding.homeReadingEmpty,
binding.homeContinueRead
binding.homeContinueRead,
binding.homeContinueReadMore,
getString(R.string.continue_reading)
)
binding.homeReadingBrowseButton.setOnClickListener {
bottomBar.selectTabAt(2)
@@ -270,7 +317,9 @@ class HomeFragment : Fragment() {
binding.homeFavMangaRecyclerView,
binding.homeFavMangaProgressBar,
binding.homeFavMangaEmpty,
binding.homeFavManga
binding.homeFavManga,
binding.homeFavMangaMore,
getString(R.string.fav_manga)
)
initRecyclerView(
@@ -279,7 +328,9 @@ class HomeFragment : Fragment() {
binding.homePlannedMangaRecyclerView,
binding.homePlannedMangaProgressBar,
binding.homePlannedMangaEmpty,
binding.homePlannedManga
binding.homePlannedManga,
binding.homePlannedMangaMore,
getString(R.string.planned_manga)
)
binding.homePlannedMangaBrowseButton.setOnClickListener {
bottomBar.selectTabAt(2)
@@ -291,12 +342,87 @@ class HomeFragment : Fragment() {
binding.homeRecommendedRecyclerView,
binding.homeRecommendedProgressBar,
binding.homeRecommendedEmpty,
binding.homeRecommended
binding.homeRecommended,
binding.homeRecommendedMore,
getString(R.string.recommended)
)
binding.homeUserStatusContainer.visibility = View.VISIBLE
binding.homeUserStatusProgressBar.visibility = View.VISIBLE
binding.homeUserStatusRecyclerView.visibility = View.GONE
model.getUserStatus().observe(viewLifecycleOwner) {
binding.homeUserStatusRecyclerView.visibility = View.GONE
if (it != null) {
if (it.isNotEmpty()) {
PrefManager.getLiveVal(PrefName.RefreshStatus, false).apply {
asLiveBool()
observe(viewLifecycleOwner) { _ ->
binding.homeUserStatusRecyclerView.adapter = UserStatusAdapter(it)
}
}
binding.homeUserStatusRecyclerView.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
binding.homeUserStatusRecyclerView.visibility = View.VISIBLE
binding.homeUserStatusRecyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
} else {
binding.homeUserStatusContainer.visibility = View.GONE
}
binding.homeUserStatusProgressBar.visibility = View.GONE
}
}
binding.homeHiddenItemsContainer.visibility = View.GONE
model.getHidden().observe(viewLifecycleOwner) {
if (it != null) {
if (it.isNotEmpty()) {
binding.homeHiddenItemsRecyclerView.adapter =
MediaAdaptor(0, it, requireActivity())
binding.homeHiddenItemsRecyclerView.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
binding.homeContinueWatch.setOnLongClickListener {
binding.homeHiddenItemsContainer.visibility = View.VISIBLE
binding.homeHiddenItemsRecyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
true
}
binding.homeHiddenItemsMore.setSafeOnClickListener { _ ->
ContextCompat.startActivity(
requireActivity(),
Intent(requireActivity(), MediaListViewActivity::class.java)
.putExtra("title", getString(R.string.hidden))
.putExtra("media", it),
null
)
}
binding.homeHiddenItemsTitle.setOnLongClickListener {
binding.homeHiddenItemsContainer.visibility = View.GONE
true
}
} else {
binding.homeContinueWatch.setOnLongClickListener {
snackString(getString(R.string.no_hidden_items))
true
}
}
} else {
binding.homeContinueWatch.setOnLongClickListener {
snackString(getString(R.string.no_hidden_items))
true
}
}
}
binding.homeUserAvatarContainer.startAnimation(setSlideUp())
model.empty.observe(viewLifecycleOwner) {
model.empty.observe(viewLifecycleOwner)
{
binding.homeDantotsuContainer.visibility = if (it == true) View.VISIBLE else View.GONE
(binding.homeDantotsuIcon.drawable as Animatable).start()
binding.homeDantotsuContainer.startAnimation(setSlideUp())
@@ -305,6 +431,7 @@ class HomeFragment : Fragment() {
}
}
val array = arrayOf(
"AnimeContinue",
"AnimeFav",
@@ -312,7 +439,8 @@ class HomeFragment : Fragment() {
"MangaContinue",
"MangaFav",
"MangaPlanned",
"Recommendation"
"Recommendation",
"UserStatus",
)
val containers = arrayOf(
@@ -322,26 +450,41 @@ class HomeFragment : Fragment() {
binding.homeContinueReadingContainer,
binding.homeFavMangaContainer,
binding.homePlannedMangaContainer,
binding.homeRecommendedContainer
binding.homeRecommendedContainer,
binding.homeUserStatusContainer,
)
val live = Refresh.activity.getOrPut(1) { MutableLiveData(false) }
live.observe(viewLifecycleOwner) {
if (it) {
var running = false
val live = Refresh.activity.getOrPut(1) { MutableLiveData(true) }
live.observe(viewLifecycleOwner)
{
if (!running && it) {
running = true
scope.launch {
withContext(Dispatchers.IO) {
//Get userData First
getUserId(requireContext()) {
load()
Anilist.userid =
PrefManager.getNullableVal<String>(PrefName.AnilistUserId, null)
?.toIntOrNull()
if (Anilist.userid == null) {
getUserId(requireContext()) {
load()
}
} else {
CoroutineScope(Dispatchers.IO).launch {
getUserId(requireContext()) {
load()
}
}
}
model.loaded = true
model.setListImages()
CoroutineScope(Dispatchers.IO).launch {
model.setListImages()
}
var empty = true
val homeLayoutShow: List<Boolean> =
PrefManager.getVal(PrefName.HomeLayoutShow)
runBlocking {
model.initHomePage()
}
PrefManager.getVal(PrefName.HomeLayout)
model.initHomePage()
(array.indices).forEach { i ->
if (homeLayoutShow.elementAt(i)) {
empty = false
@@ -353,13 +496,20 @@ class HomeFragment : Fragment() {
}
live.postValue(false)
_binding?.homeRefresh?.isRefreshing = false
running = false
}
binding.homeHiddenItemsContainer.visibility = View.GONE
}
}
}
override fun onResume() {
if (!model.loaded) Refresh.activity[1]!!.postValue(true)
if (_binding != null) {
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
}
super.onResume()
}
}

View File

@@ -17,6 +17,7 @@ import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.settings.saving.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferencePackager
import ani.dantotsu.toast
import ani.dantotsu.util.Logger
import com.google.android.material.textfield.TextInputEditText
class LoginFragment : Fragment() {
@@ -50,7 +51,7 @@ class LoginFragment : Fragment() {
DocumentFile.fromSingleUri(requireActivity(), uri)?.name ?: "settings"
//.sani is encrypted, .ani is not
if (name.endsWith(".sani")) {
passwordAlertDialog() { password ->
passwordAlertDialog { password ->
if (password != null) {
val salt = jsonString.copyOfRange(0, 16)
val encrypted = jsonString.copyOfRange(16, jsonString.size)
@@ -78,7 +79,7 @@ class LoginFragment : Fragment() {
toast("Invalid file type")
}
} catch (e: Exception) {
e.printStackTrace()
Logger.log(e)
toast("Error importing settings")
}
}

View File

@@ -33,6 +33,7 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -160,9 +161,35 @@ class MangaFragment : Fragment() {
})
mangaPageAdapter.ready.observe(viewLifecycleOwner) { i ->
if (i == true) {
model.getTrendingNovel().observe(viewLifecycleOwner) {
model.getPopularNovel().observe(viewLifecycleOwner) {
if (it != null) {
mangaPageAdapter.updateNovel(MediaAdaptor(0, it, requireActivity()))
mangaPageAdapter.updateNovel(MediaAdaptor(0, it, requireActivity()), it)
}
}
model.getPopularManga().observe(viewLifecycleOwner) {
if (it != null) {
mangaPageAdapter.updateTrendingManga(MediaAdaptor(0, it, requireActivity()), it)
}
}
model.getPopularManhwa().observe(viewLifecycleOwner) {
if (it != null) {
mangaPageAdapter.updateTrendingManhwa(
MediaAdaptor(
0,
it,
requireActivity()
), it
)
}
}
model.getTopRated().observe(viewLifecycleOwner) {
if (it != null) {
mangaPageAdapter.updateTopRated(MediaAdaptor(0, it, requireActivity()), it)
}
}
model.getMostFav().observe(viewLifecycleOwner) {
if (it != null) {
mangaPageAdapter.updateMostFav(MediaAdaptor(0, it, requireActivity()), it)
}
}
if (mangaPageAdapter.trendingViewPager != null) {
@@ -227,17 +254,29 @@ class MangaFragment : Fragment() {
mangaPageAdapter.updateAvatar()
}
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(false) }
var running = false
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) }
live.observe(viewLifecycleOwner) {
if (it) {
if (!running && it) {
running = true
scope.launch {
withContext(Dispatchers.IO) {
getUserId(requireContext()) {
load()
Anilist.userid = PrefManager.getNullableVal<String>(PrefName.AnilistUserId, null)
?.toIntOrNull()
if (Anilist.userid == null) {
getUserId(requireContext()) {
load()
}
} else {
CoroutineScope(Dispatchers.IO).launch {
getUserId(requireContext()) {
load()
}
}
}
model.loaded = true
model.loadTrending()
model.loadTrendingNovel()
model.loadAll()
model.loadPopular(
"MANGA", sort = Anilist.sortBy[1], onList = PrefManager.getVal(
PrefName.PopularMangaList
@@ -246,6 +285,7 @@ class MangaFragment : Fragment() {
}
live.postValue(false)
_binding?.mangaRefresh?.isRefreshing = false
running = false
}
}
}
@@ -258,6 +298,9 @@ class MangaFragment : Fragment() {
binding.root.requestApplyInsets()
binding.root.requestLayout()
}
if (this::mangaPageAdapter.isInitialized && _binding != null) {
mangaPageAdapter.updateNotificationCount()
}
super.onResume()
}

View File

@@ -3,13 +3,14 @@ package ani.dantotsu.home
import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.util.TypedValue
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.LayoutAnimationController
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.lifecycle.MutableLiveData
@@ -19,12 +20,17 @@ import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.MediaPageTransformer
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemMangaPageBinding
import ani.dantotsu.databinding.LayoutTrendingBinding
import ani.dantotsu.getAppString
import ani.dantotsu.getThemeColor
import ani.dantotsu.loadImage
import ani.dantotsu.media.GenreActivity
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaAdaptor
import ani.dantotsu.media.MediaListViewActivity
import ani.dantotsu.media.SearchActivity
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.px
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.setSlideIn
@@ -39,6 +45,7 @@ import com.google.android.material.textfield.TextInputLayout
class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHolder>() {
val ready = MutableLiveData(false)
lateinit var binding: ItemMangaPageBinding
private lateinit var trendingBinding: LayoutTrendingBinding
private var trendHandler: Handler? = null
private lateinit var trendRun: Runnable
var trendingViewPager: ViewPager2? = null
@@ -51,32 +58,31 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
override fun onBindViewHolder(holder: MangaPageViewHolder, position: Int) {
binding = holder.binding
trendingViewPager = binding.mangaTrendingViewPager
trendingBinding = LayoutTrendingBinding.bind(binding.root)
trendingViewPager = trendingBinding.trendingViewPager
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.mangaSearchBar)
val textInputLayout = holder.itemView.findViewById<TextInputLayout>(R.id.searchBar)
val currentColor = textInputLayout.boxBackgroundColor
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xA8000000.toInt()
textInputLayout.boxBackgroundColor = semiTransparentColor
val materialCardView =
holder.itemView.findViewById<MaterialCardView>(R.id.mangaUserAvatarContainer)
holder.itemView.findViewById<MaterialCardView>(R.id.userAvatarContainer)
materialCardView.setCardBackgroundColor(semiTransparentColor)
val typedValue = TypedValue()
currContext()?.theme?.resolveAttribute(android.R.attr.windowBackground, typedValue, true)
val color = typedValue.data
val color = binding.root.context.getThemeColor(android.R.attr.windowBackground)
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
trendingBinding.titleContainer.updatePadding(top = statusBarHeight)
binding.mangaTitleContainer.updatePadding(top = statusBarHeight)
if (PrefManager.getVal(PrefName.SmallView)) binding.mangaTrendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
if (PrefManager.getVal(PrefName.SmallView)) trendingBinding.trendingContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = (-108f).px
}
updateAvatar()
binding.mangaSearchBar.hint = "MANGA"
binding.mangaSearchBarText.setOnClickListener {
trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
trendingBinding.searchBar.hint = "MANGA"
trendingBinding.searchBarText.setOnClickListener {
ContextCompat.startActivity(
it.context,
Intent(it.context, SearchActivity::class.java).putExtra("type", "MANGA"),
@@ -84,14 +90,23 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
)
}
binding.mangaUserAvatar.setSafeOnClickListener {
trendingBinding.userAvatar.setSafeOnClickListener {
val dialogFragment =
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.MANGA)
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
}
trendingBinding.userAvatar.setOnLongClickListener { view ->
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
ContextCompat.startActivity(
view.context,
Intent(view.context, ProfileActivity::class.java)
.putExtra("userId", Anilist.userid), null
)
false
}
binding.mangaSearchBar.setEndIconOnClickListener {
binding.mangaSearchBarText.performClick()
trendingBinding.searchBar.setEndIconOnClickListener {
trendingBinding.searchBarText.performClick()
}
binding.mangaGenreImage.loadImage("https://s4.anilist.co/file/anilistcdn/media/manga/banner/105778-wk5qQ7zAaTGl.jpg")
@@ -115,8 +130,7 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
)
}
binding.mangaIncludeList.visibility =
if (Anilist.userid != null) View.VISIBLE else View.GONE
binding.mangaIncludeList.isVisible = Anilist.userid != null
binding.mangaIncludeList.isChecked = PrefManager.getVal(PrefName.PopularMangaList)
@@ -138,56 +152,153 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
}
fun updateTrending(adaptor: MediaAdaptor) {
binding.mangaTrendingProgressBar.visibility = View.GONE
binding.mangaTrendingViewPager.adapter = adaptor
binding.mangaTrendingViewPager.offscreenPageLimit = 3
binding.mangaTrendingViewPager.getChildAt(0).overScrollMode = RecyclerView.OVER_SCROLL_NEVER
binding.mangaTrendingViewPager.setPageTransformer(MediaPageTransformer())
trendingBinding.trendingProgressBar.visibility = View.GONE
trendingBinding.trendingViewPager.adapter = adaptor
trendingBinding.trendingViewPager.offscreenPageLimit = 3
trendingBinding.trendingViewPager.getChildAt(0).overScrollMode =
RecyclerView.OVER_SCROLL_NEVER
trendingBinding.trendingViewPager.setPageTransformer(MediaPageTransformer())
trendHandler = Handler(Looper.getMainLooper())
trendRun = Runnable {
binding.mangaTrendingViewPager.currentItem =
binding.mangaTrendingViewPager.currentItem + 1
trendingBinding.trendingViewPager.currentItem += 1
}
binding.mangaTrendingViewPager.registerOnPageChangeCallback(
trendingBinding.trendingViewPager.registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
trendHandler!!.removeCallbacks(trendRun)
trendHandler!!.postDelayed(trendRun, 4000)
trendHandler?.removeCallbacks(trendRun)
if (PrefManager.getVal(PrefName.TrendingScroller))
trendHandler!!.postDelayed(trendRun, 4000)
}
}
)
binding.mangaTrendingViewPager.layoutAnimation =
trendingBinding.trendingViewPager.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
binding.mangaTitleContainer.startAnimation(setSlideUp())
trendingBinding.titleContainer.startAnimation(setSlideUp())
binding.mangaListContainer.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
}
fun updateNovel(adaptor: MediaAdaptor) {
binding.mangaNovelProgressBar.visibility = View.GONE
binding.mangaNovelRecyclerView.adapter = adaptor
binding.mangaNovelRecyclerView.layoutManager =
fun updateTrendingManga(adaptor: MediaAdaptor, media: MutableList<Media>) {
binding.apply {
init(
adaptor,
mangaTrendingMangaRecyclerView,
mangaTrendingMangaProgressBar,
mangaTrendingManga,
mangaTrendingMangaMore,
getAppString(R.string.trending_manga),
media
)
}
}
fun updateTrendingManhwa(adaptor: MediaAdaptor, media: MutableList<Media>) {
binding.apply {
init(
adaptor,
mangaTrendingManhwaRecyclerView,
mangaTrendingManhwaProgressBar,
mangaTrendingManhwa,
mangaTrendingManhwaMore,
getAppString(R.string.trending_manhwa),
media
)
}
}
fun updateNovel(adaptor: MediaAdaptor, media: MutableList<Media>) {
binding.apply {
init(
adaptor,
mangaNovelRecyclerView,
mangaNovelProgressBar,
mangaNovel,
mangaNovelMore,
getAppString(R.string.trending_novel),
media
)
}
}
fun updateTopRated(adaptor: MediaAdaptor, media: MutableList<Media>) {
binding.apply {
init(
adaptor,
mangaTopRatedRecyclerView,
mangaTopRatedProgressBar,
mangaTopRated,
mangaTopRatedMore,
getAppString(R.string.top_rated),
media
)
}
}
fun updateMostFav(adaptor: MediaAdaptor, media: MutableList<Media>) {
binding.apply {
init(
adaptor,
mangaMostFavRecyclerView,
mangaMostFavProgressBar,
mangaMostFav,
mangaMostFavMore,
getAppString(R.string.most_favourite),
media
)
mangaPopular.visibility = View.VISIBLE
mangaPopular.startAnimation(setSlideUp())
}
}
fun init(
adaptor: MediaAdaptor,
recyclerView: RecyclerView,
progress: View,
title: View ,
more: View ,
string: String,
media : MutableList<Media>
) {
progress.visibility = View.GONE
recyclerView.adapter = adaptor
recyclerView.layoutManager =
LinearLayoutManager(
binding.mangaNovelRecyclerView.context,
recyclerView.context,
LinearLayoutManager.HORIZONTAL,
false
)
binding.mangaNovelRecyclerView.visibility = View.VISIBLE
binding.mangaNovel.visibility = View.VISIBLE
binding.mangaNovel.startAnimation(setSlideUp())
binding.mangaNovelRecyclerView.layoutAnimation =
more.setOnClickListener {
ContextCompat.startActivity(
it.context, Intent(it.context, MediaListViewActivity::class.java)
.putExtra("title", string)
.putExtra("media", media as ArrayList<Media>),
null
)
}
recyclerView.visibility = View.VISIBLE
title.visibility = View.VISIBLE
more.visibility = View.VISIBLE
title.startAnimation(setSlideUp())
more.startAnimation(setSlideUp())
recyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
binding.mangaPopular.visibility = View.VISIBLE
binding.mangaPopular.startAnimation(setSlideUp())
}
fun updateAvatar() {
if (Anilist.avatar != null && ready.value == true) {
binding.mangaUserAvatar.loadImage(Anilist.avatar)
binding.mangaUserAvatar.imageTintList = null
trendingBinding.userAvatar.loadImage(Anilist.avatar)
trendingBinding.userAvatar.imageTintList = null
}
}
fun updateNotificationCount() {
if (this::binding.isInitialized) {
trendingBinding.notificationCount.visibility =
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
}
}

View File

@@ -0,0 +1,82 @@
package ani.dantotsu.home.status
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.util.AttributeSet
import android.view.View
import ani.dantotsu.getThemeColor
class CircleView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
private var parts: Int = 3
private var gapAngle: Float = 9f
private val path = Path()
private var isUser = false
private var booleanList = listOf<Boolean>()
private val paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
strokeWidth = 6f
}
@SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val centerX = width / 2f
val centerY = height / 2f
val radius = centerX.coerceAtMost(centerY) - paint.strokeWidth / 2
val totalGapAngle = gapAngle * (parts)
val totalAngle = 360f - totalGapAngle
val primaryColor = context.getThemeColor(com.google.android.material.R.attr.colorPrimary)
val secondColor = context.getThemeColor(com.google.android.material.R.attr.colorOnPrimary)
fun setColor(int: Int) {
paint.color = if (int < booleanList.size && booleanList[int]) {
Color.GRAY
} else {
if (isUser) secondColor else primaryColor
}
canvas.drawPath(path, paint)
}
if (parts == 1) {
path.addArc(
centerX - radius,
centerY - radius,
centerX + radius,
centerY + radius,
0f,
360f
)
setColor(0)
} else {
val effectiveAngle = totalAngle / parts
for (i in 0 until parts) {
val startAngle = i * (effectiveAngle + gapAngle) -90f
path.reset()
path.addArc(
centerX - radius,
centerY - radius,
centerX + radius,
centerY + radius,
startAngle,
effectiveAngle
)
setColor(i)
}
}
}
fun setParts(parts: Int, list : List<Boolean> = mutableListOf(), isUser: Boolean) {
this.parts = parts
this.booleanList = list
this.isUser = isUser
invalidate()
}
}

View File

@@ -0,0 +1,110 @@
package ani.dantotsu.home.status
import android.os.Bundle
import android.view.ViewGroup
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.updateLayoutParams
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.api.Activity
import ani.dantotsu.databinding.ActivityStatusBinding
import ani.dantotsu.initActivity
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.home.status.listener.StoriesCallback
import ani.dantotsu.navBarHeight
import ani.dantotsu.profile.User
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.statusBarHeight
class StatusActivity : AppCompatActivity(), StoriesCallback {
private lateinit var activity: ArrayList<User>
private lateinit var binding: ActivityStatusBinding
private var position: Int = -1
private lateinit var slideInLeft: Animation
private lateinit var slideOutRight: Animation
private lateinit var slideOutLeft: Animation
private lateinit var slideInRight: Animation
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
initActivity(this)
binding = ActivityStatusBinding.inflate(layoutInflater)
setContentView(binding.root)
activity = user
position = intent.getIntExtra("position", -1)
binding.root.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight
bottomMargin = navBarHeight
}
slideInLeft = AnimationUtils.loadAnimation(this, R.anim.slide_in_left)
slideOutRight = AnimationUtils.loadAnimation(this, R.anim.slide_out_right)
slideOutLeft = AnimationUtils.loadAnimation(this, R.anim.slide_out_left)
slideInRight = AnimationUtils.loadAnimation(this, R.anim.slide_in_right)
val key = "activities"
val watchedActivity = PrefManager.getCustomVal<Set<Int>>(key, setOf())
val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity )
val startIndex = if ( startFrom > 0) startFrom else 0
binding.stories.setStoriesList(activity[position].activity, this, startIndex + 1)
}
private fun findFirstNonMatch(watchedActivity: Set<Int>, activity: List<Activity>): Int {
for (activityItem in activity) {
if (activityItem.id !in watchedActivity) {
return activity.indexOf(activityItem)
}
}
return -1
}
override fun onPause() {
super.onPause()
binding.stories.pause()
}
override fun onResume() {
super.onResume()
binding.stories.resume()
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
binding.stories.resume()
} else {
binding.stories.pause()
}
}
override fun onStoriesEnd() {
position += 1
if (position < activity.size) {
val key = "activities"
val watchedActivity = PrefManager.getCustomVal<Set<Int>>(key, setOf())
val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity )
val startIndex= if ( startFrom > 0) startFrom else 0
binding.stories.startAnimation(slideOutLeft)
binding.stories.setStoriesList(activity[position].activity, this, startIndex + 1)
binding.stories.startAnimation(slideInRight)
} else {
finish()
}
}
override fun onStoriesStart() {
position -= 1
if (position >= 0) {
val key = "activities"
val watchedActivity = PrefManager.getCustomVal<Set<Int>>(key, setOf())
val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity )
val startIndex = if ( startFrom > 0) startFrom else 0
binding.stories.startAnimation(slideOutRight)
binding.stories.setStoriesList(activity[position].activity, this, startIndex + 1)
binding.stories.startAnimation(slideInLeft)
} else {
finish()
}
}
companion object {
var user: ArrayList<User> = arrayListOf()
}
}

View File

@@ -0,0 +1,491 @@
package ani.dantotsu.home.status
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.widget.ProgressBar
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.fragment.app.FragmentActivity
import ani.dantotsu.R
import ani.dantotsu.blurImage
import ani.dantotsu.buildMarkwon
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.Activity
import ani.dantotsu.databinding.FragmentStatusBinding
import ani.dantotsu.getThemeColor
import ani.dantotsu.home.status.listener.StoriesCallback
import ani.dantotsu.loadImage
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.User
import ani.dantotsu.profile.UsersDialogFragment
import ani.dantotsu.profile.activity.ActivityItemBuilder
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.util.AniMarkdown
import ani.dantotsu.util.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.Calendar
import java.util.Locale
class Stories @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr), View.OnTouchListener {
private lateinit var activity: FragmentActivity
private lateinit var binding: FragmentStatusBinding
private lateinit var activityList: List<Activity>
private lateinit var storiesListener: StoriesCallback
private var userClicked: Boolean = false
private var storyIndex: Int = 1
private var primaryColor: Int = 0
private var onPrimaryColor: Int = 0
private var storyDuration: Int = 6
private val timer: StoryTimer = StoryTimer(secondsToMillis(storyDuration))
init {
initLayout()
}
@SuppressLint("ClickableViewAccessibility")
fun initLayout() {
val inflater: LayoutInflater = LayoutInflater.from(context)
binding = FragmentStatusBinding.inflate(inflater, this, false)
addView(binding.root)
primaryColor = context.getThemeColor(com.google.android.material.R.attr.colorPrimary)
onPrimaryColor = context.getThemeColor(com.google.android.material.R.attr.colorOnPrimary)
if (context is StoriesCallback) storiesListener = context as StoriesCallback
binding.leftTouchPanel.setOnTouchListener(this)
binding.rightTouchPanel.setOnTouchListener(this)
}
fun setStoriesList(
activityList: List<Activity>, activity: FragmentActivity, startIndex: Int = 1
) {
this.activityList = activityList
this.activity = activity
this.storyIndex = startIndex
addLoadingViews(activityList)
}
private fun addLoadingViews(storiesList: List<Activity>) {
var idCounter = 1
for (story in storiesList) {
binding.progressBarContainer.removeView(findViewWithTag<ProgressBar>("story${idCounter}"))
val progressBar = ProgressBar(context, null, android.R.attr.progressBarStyleHorizontal)
progressBar.visibility = View.VISIBLE
progressBar.id = idCounter
progressBar.tag = "story${idCounter++}"
progressBar.progressBackgroundTintList = ColorStateList.valueOf(primaryColor)
progressBar.progressTintList = ColorStateList.valueOf(onPrimaryColor)
val params = LayoutParams(0, LayoutParams.WRAP_CONTENT)
params.marginEnd = 5
params.marginStart = 5
binding.progressBarContainer.addView(progressBar, params)
}
val constraintSet = ConstraintSet()
constraintSet.clone(binding.progressBarContainer)
var counter = storiesList.size
for (story in storiesList) {
val progressBar = findViewWithTag<ProgressBar>("story${counter}")
if (progressBar != null) {
if (storiesList.size > 1) {
when (counter) {
storiesList.size -> {
constraintSet.connect(
getId("story${counter}"),
ConstraintSet.END,
LayoutParams.PARENT_ID,
ConstraintSet.END
)
constraintSet.connect(
getId("story${counter}"),
ConstraintSet.TOP,
LayoutParams.PARENT_ID,
ConstraintSet.TOP
)
constraintSet.connect(
getId("story${counter}"),
ConstraintSet.START,
getId("story${counter - 1}"),
ConstraintSet.END
)
}
1 -> {
constraintSet.connect(
getId("story${counter}"),
ConstraintSet.TOP,
LayoutParams.PARENT_ID,
ConstraintSet.TOP
)
constraintSet.connect(
getId("story${counter}"),
ConstraintSet.START,
LayoutParams.PARENT_ID,
ConstraintSet.START
)
constraintSet.connect(
getId("story${counter}"),
ConstraintSet.END,
getId("story${counter + 1}"),
ConstraintSet.START
)
}
else -> {
constraintSet.connect(
getId("story${counter}"),
ConstraintSet.TOP,
LayoutParams.PARENT_ID,
ConstraintSet.TOP
)
constraintSet.connect(
getId("story${counter}"),
ConstraintSet.START,
getId("story${counter - 1}"),
ConstraintSet.END
)
constraintSet.connect(
getId("story${counter}"),
ConstraintSet.END,
getId("story${counter + 1}"),
ConstraintSet.START
)
}
}
} else {
constraintSet.connect(
getId("story${counter}"),
ConstraintSet.END,
LayoutParams.PARENT_ID,
ConstraintSet.END
)
constraintSet.connect(
getId("story${counter}"),
ConstraintSet.TOP,
LayoutParams.PARENT_ID,
ConstraintSet.TOP
)
constraintSet.connect(
getId("story${counter}"),
ConstraintSet.START,
LayoutParams.PARENT_ID,
ConstraintSet.START
)
}
}
counter--
}
constraintSet.applyTo(binding.progressBarContainer)
startShowContent()
}
private fun startShowContent() {
showStory()
}
private fun showStory() {
if (storyIndex > 1) {
completeProgressBar(storyIndex - 1)
}
val progressBar = findViewWithTag<ProgressBar>("story${storyIndex}")
binding.androidStoriesLoadingView.visibility = View.VISIBLE
timer.setOnTimerCompletedListener {
Logger.log("onAnimationEnd: $storyIndex")
if (storyIndex - 1 <= activityList.size) {
if (false) {
Logger.log("userClicked: $storyIndex")
userClicked = false
} else {
Logger.log("userNotClicked: $storyIndex")
if (storyIndex < activityList.size) {
storyIndex += 1
showStory()
} else {
// on stories end
binding.androidStoriesLoadingView.visibility = View.GONE
onStoriesCompleted()
}
}
} else {
// on stories end
binding.androidStoriesLoadingView.visibility = View.GONE
onStoriesCompleted()
}
}
timer.setOnPercentTickListener {
progressBar.progress = it
}
loadStory(activityList[storyIndex - 1])
}
private fun getId(tag: String): Int {
return findViewWithTag<ProgressBar>(tag).id
}
private fun secondsToMillis(seconds: Int): Long {
return (seconds.toLong()).times(1000)
}
private fun resetProgressBar(storyIndex: Int) {
for (i in storyIndex until activityList.size + 1) {
val progressBar = findViewWithTag<ProgressBar>("story${i}")
progressBar?.let {
it.progress = 0
}
}
}
private fun completeProgressBar(storyIndex: Int) {
for (i in 1 until storyIndex + 1) {
val progressBar = findViewWithTag<ProgressBar>("story${i}")
progressBar?.let {
it.progress = 100
}
}
}
private var startClickTime = 0L
override fun onTouch(view: View?, event: MotionEvent?): Boolean {
val maxClickDuration = 200
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
startClickTime = Calendar.getInstance().timeInMillis
pause()
}
MotionEvent.ACTION_UP -> {
val clickDuration = Calendar.getInstance().timeInMillis - startClickTime
if (clickDuration < maxClickDuration) {
//click occurred
view?.let {
if (it.id == R.id.leftTouchPanel) {
leftPanelTouch()
} else if (it.id == R.id.rightTouchPanel) {
rightPanelTouch()
}
}
} else {
//hold click occurred
resume()
}
}
}
return true
}
private fun rightPanelTouch() {
Logger.log("rightPanelTouch: $storyIndex")
if (storyIndex == activityList.size) {
completeProgressBar(storyIndex)
onStoriesCompleted()
return
}
userClicked = true
timer.cancel()
if (storyIndex <= activityList.size) storyIndex += 1
showStory()
}
private fun leftPanelTouch() {
Logger.log("leftPanelTouch: $storyIndex")
if (storyIndex == 1) {
onStoriesPrevious()
return
}
userClicked = true
timer.cancel()
resetProgressBar(storyIndex)
if (storyIndex > 1) storyIndex -= 1
showStory()
}
private fun onStoriesCompleted() {
Logger.log("onStoriesCompleted")
if (::storiesListener.isInitialized) {
storyIndex = 1
storiesListener.onStoriesEnd()
resetProgressBar(storyIndex)
}
}
private fun onStoriesPrevious() {
if (::storiesListener.isInitialized) {
storyIndex = 1
storiesListener.onStoriesStart()
resetProgressBar(storyIndex)
}
}
fun pause() {
timer.pause()
}
fun resume() {
timer.resume()
}
private fun loadStory(story: Activity) {
val key = "activities"
val set = PrefManager.getCustomVal<Set<Int>>(key, setOf()).plus((story.id))
val newList = set.sorted().takeLast(200).toSet()
PrefManager.setCustomVal(key, newList)
binding.statusUserAvatar.loadImage(story.user?.avatar?.large)
binding.statusUserName.text = story.user?.name
binding.statusUserTime.text = ActivityItemBuilder.getDateTime(story.createdAt)
binding.statusUserContainer.setOnClickListener {
ContextCompat.startActivity(
context,
Intent(context, ProfileActivity::class.java).putExtra("userId", story.userId),
null
)
}
fun visible(isList: Boolean) {
val visible = if (isList) View.VISIBLE else View.GONE
val gone = if (isList) View.GONE else View.VISIBLE
binding.textActivity.visibility = gone
binding.textActivityContainer.visibility = gone
binding.infoText.visibility = visible
binding.coverImage.visibility = visible
binding.infoText.visibility = if (isList) View.VISIBLE else View.INVISIBLE
binding.infoText.text = ""
binding.contentImageViewKen.visibility = visible
binding.contentImageView.visibility = visible
}
when (story.typename) {
"ListActivity" -> {
visible(true)
val text = "${
story.status?.replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(Locale.ROOT)
} else {
it.toString()
}
}
} ${story.progress ?: story.media?.title?.userPreferred} " + if (story.status?.contains(
"completed"
) == false && !story.status.contains("plans") && !story.status.contains(
"repeating"
)
) {
"of ${story.media?.title?.userPreferred}"
} else {
""
}
binding.infoText.text = text
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
blurImage(
if (bannerAnimations) binding.contentImageViewKen else binding.contentImageView,
story.media?.bannerImage ?: story.media?.coverImage?.extraLarge
)
binding.coverImage.loadImage(story.media?.coverImage?.extraLarge)
binding.coverImage.setOnClickListener {
ContextCompat.startActivity(
context,
Intent(context, MediaDetailsActivity::class.java).putExtra(
"mediaId",
story.media?.id
),
ActivityOptionsCompat.makeSceneTransitionAnimation(
activity,
binding.coverImage,
ViewCompat.getTransitionName(binding.coverImage)!!
).toBundle()
)
}
}
"TextActivity" -> {
visible(false)
if (!(context as android.app.Activity).isDestroyed) {
val markwon = buildMarkwon(context, false)
markwon.setMarkdown(
binding.textActivity, AniMarkdown.getBasicAniHTML(story.text ?: "")
)
}
}
"MessageActivity" -> {
visible(false)
if (!(context as android.app.Activity).isDestroyed) {
val markwon = buildMarkwon(context, false)
markwon.setMarkdown(
binding.textActivity, AniMarkdown.getBasicAniHTML(story.message ?: "")
)
}
}
}
val userList = arrayListOf<User>()
story.likes?.forEach { i ->
userList.add(User(i.id, i.name.toString(), i.avatar?.medium, i.bannerImage))
}
val likeColor = ContextCompat.getColor(context, R.color.yt_red)
val notLikeColor = ContextCompat.getColor(context, R.color.bg_opp)
binding.activityLikeCount.text = story.likeCount.toString()
binding.activityLike.setColorFilter(if (story.isLiked == true) likeColor else notLikeColor)
binding.activityLikeContainer.setOnClickListener {
like()
}
binding.activityLikeContainer.setOnLongClickListener {
val context = activity
UsersDialogFragment().apply {
userList(userList)
show(context.supportFragmentManager, "dialog")
}
true
}
binding.androidStoriesLoadingView.visibility = View.GONE
timer.start()
}
fun like() {
val story = activityList[storyIndex - 1]
val likeColor = ContextCompat.getColor(context, R.color.yt_red)
val notLikeColor = ContextCompat.getColor(context, R.color.bg_opp)
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
scope.launch {
val res = Anilist.query.toggleLike(story.id, "ACTIVITY")
withContext(Dispatchers.Main) {
if (res != null) {
if (story.isLiked == true) {
story.likeCount = story.likeCount?.minus(1)
} else {
story.likeCount = story.likeCount?.plus(1)
}
binding.activityLikeCount.text = (story.likeCount ?: 0).toString()
story.isLiked = !story.isLiked!!
binding.activityLike.setColorFilter(if (story.isLiked == true) likeColor else notLikeColor)
} else {
snackString("Failed to like activity")
}
}
}
}
}

View File

@@ -0,0 +1,64 @@
package ani.dantotsu.home.status
import android.os.CountDownTimer
class StoryTimer(
private val updateInterval: Long
) {
private lateinit var timer: CountDownTimer
private var prevVal = 0
private var pauseLength = 0L
var onTimerCompleted: () -> Unit = {}
var percentTick: (Int) -> Unit = {}
var timeLeft: Long = 0
private set
fun start(durationInMillis: Long = updateInterval) {
cancel()
timer = object : CountDownTimer(durationInMillis, 1) {
override fun onTick(millisUntilFinished: Long) {
timeLeft = millisUntilFinished
val percent =
((pauseLength + durationInMillis - millisUntilFinished) * 100 / (pauseLength + durationInMillis)).toInt()
if (percent != prevVal) {
percentTick.invoke(percent)
prevVal = percent
}
}
override fun onFinish() {
onTimerCompleted.invoke()
pauseLength = 0
}
}
timer.start()
}
fun cancel() {
if (::timer.isInitialized) {
timer.cancel()
}
}
fun pause() {
if (::timer.isInitialized) {
timer.cancel()
pauseLength = updateInterval - timeLeft
}
}
fun resume() {
if (::timer.isInitialized && timeLeft > 0) {
start(timeLeft)
timer.start()
}
}
fun setOnTimerCompletedListener(onTimerCompleted: () -> Unit) {
this.onTimerCompleted = onTimerCompleted
}
fun setOnPercentTickListener(percentTick: (Int) -> Unit) {
this.percentTick = percentTick
}
}

View File

@@ -0,0 +1,73 @@
package ani.dantotsu.home.status
import android.content.Context
import android.content.Intent
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.ItemUserStatusBinding
import ani.dantotsu.getAppString
import ani.dantotsu.loadImage
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.User
import ani.dantotsu.setAnimation
import ani.dantotsu.settings.saving.PrefManager
class UserStatusAdapter(private val user: ArrayList<User>) :
RecyclerView.Adapter<UserStatusAdapter.UsersViewHolder>() {
inner class UsersViewHolder(val binding: ItemUserStatusBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
itemView.setOnClickListener {
StatusActivity.user = user
ContextCompat.startActivity(
itemView.context,
Intent(
itemView.context,
StatusActivity::class.java
).putExtra("position", bindingAdapterPosition),
null
)
}
itemView.setOnLongClickListener {
ContextCompat.startActivity(
itemView.context,
Intent(
itemView.context,
ProfileActivity::class.java
).putExtra("userId", user[bindingAdapterPosition].id),
null
)
true
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UsersViewHolder {
return UsersViewHolder(
ItemUserStatusBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: UsersViewHolder, position: Int) {
val b = holder.binding
setAnimation(b.root.context, b.root)
val user = user[position]
b.profileUserAvatar.loadImage(user.pfp)
b.profileUserName.text = if (Anilist.userid == user.id) getAppString(R.string.your_story) else user.name
val watchedActivity = PrefManager.getCustomVal<Set<Int>>("activities", setOf())
val booleanList = user.activity.map { watchedActivity.contains(it.id) }
b.profileUserStatusIndicator.setParts(user.activity.size, booleanList, user.id == Anilist.userid)
}
override fun getItemCount(): Int = user.size
}

View File

@@ -0,0 +1,6 @@
package ani.dantotsu.home.status.listener
interface StoriesCallback {
fun onStoriesEnd()
fun onStoriesStart()
}

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,9 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.os.Bundle
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.view.WindowManager
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
@@ -16,8 +13,9 @@ import androidx.lifecycle.lifecycleScope
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.databinding.ActivityListBinding
import ani.dantotsu.getThemeColor
import ani.dantotsu.hideSystemBarsExtendView
import ani.dantotsu.media.user.ListViewPagerAdapter
import ani.dantotsu.navBarHeight
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.statusBarHeight
@@ -34,7 +32,6 @@ class CalendarActivity : AppCompatActivity() {
private var selectedTabIdx = 1
private val model: OtherDetailsViewModel by viewModels()
@SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -42,22 +39,10 @@ class CalendarActivity : AppCompatActivity() {
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
val primaryColor = getThemeColor(com.google.android.material.R.attr.colorSurface)
val primaryTextColor = getThemeColor(com.google.android.material.R.attr.colorPrimary)
val secondaryTextColor = getThemeColor(com.google.android.material.R.attr.colorOutline)
window.statusBarColor = primaryColor
window.navigationBarColor = primaryColor
@@ -74,20 +59,16 @@ class CalendarActivity : AppCompatActivity() {
} else {
binding.root.fitsSystemWindows = false
requestWindowFeature(Window.FEATURE_NO_TITLE)
window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
hideSystemBarsExtendView()
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight
bottomMargin = navBarHeight
}
}
setContentView(binding.root)
binding.listTitle.setText(R.string.release_calendar)
binding.listSort.visibility = View.GONE
binding.random.visibility = View.GONE
binding.listTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
this@CalendarActivity.selectedTabIdx = tab?.position ?: 1

View File

@@ -9,10 +9,11 @@ data class Character(
val image: String?,
val banner: String?,
val role: String,
var isFav: Boolean,
var description: String? = null,
var age: String? = null,
var gender: String? = null,
var dateOfBirth: FuzzyDate? = null,
var roles: ArrayList<Media>? = null
var roles: ArrayList<Media>? = null,
val voiceActor: ArrayList<Author>? = null,
) : Serializable

View File

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

View File

@@ -1,5 +1,6 @@
package ani.dantotsu.media
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
@@ -7,6 +8,7 @@ import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils.clamp
import androidx.core.view.isGone
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.lifecycle.MutableLiveData
@@ -15,20 +17,25 @@ import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistMutations
import ani.dantotsu.databinding.ActivityCharacterBinding
import ani.dantotsu.initActivity
import ani.dantotsu.loadImage
import ani.dantotsu.navBarHeight
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.others.getSerialized
import ani.dantotsu.px
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import com.google.android.material.appbar.AppBarLayout
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.math.abs
class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
@@ -48,7 +55,7 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
initActivity(this)
screenWidth = resources.displayMetrics.run { widthPixels / density }
if (PrefManager.getVal(PrefName.ImmersiveMode)) this.window.statusBarColor =
ContextCompat.getColor(this, R.color.status)
ContextCompat.getColor(this, R.color.transparent)
val banner =
if (PrefManager.getVal(PrefName.BannerAnimations)) binding.characterBanner else binding.characterBannerNoKen
@@ -75,7 +82,40 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
character.image
)
}
val link = "https://anilist.co/character/${character.id}"
binding.characterShare.setOnClickListener {
val i = Intent(Intent.ACTION_SEND)
i.type = "text/plain"
i.putExtra(Intent.EXTRA_TEXT, link)
startActivity(Intent.createChooser(i, character.name))
}
binding.characterShare.setOnLongClickListener {
openLinkInBrowser(link)
true
}
lifecycleScope.launch {
withContext(Dispatchers.IO) {
character.isFav =
Anilist.query.isUserFav(AnilistMutations.FavType.CHARACTER, character.id)
}
withContext(Dispatchers.Main) {
binding.characterFav.setImageResource(
if (character.isFav) R.drawable.ic_round_favorite_24 else R.drawable.ic_round_favorite_border_24
)
}
}
binding.characterFav.setOnClickListener {
lifecycleScope.launch {
if (Anilist.mutation.toggleFav(AnilistMutations.FavType.CHARACTER, character.id)) {
character.isFav = !character.isFav
binding.characterFav.setImageResource(
if (character.isFav) R.drawable.ic_round_favorite_24 else R.drawable.ic_round_favorite_border_24
)
} else {
snackString("Failed to toggle favorite")
}
}
}
model.getCharacter().observe(this) {
if (it != null && !loaded) {
character = it
@@ -114,7 +154,7 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
}
override fun onResume() {
binding.characterProgress.visibility = if (!loaded) View.VISIBLE else View.GONE
binding.characterProgress.isGone = loaded
super.onResume()
}
@@ -139,13 +179,11 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
isCollapsed = true
if (immersiveMode) this.window.statusBarColor =
ContextCompat.getColor(this, R.color.nav_bg)
binding.characterAppBar.setBackgroundResource(R.color.nav_bg)
}
if (percentage <= percent && isCollapsed) {
isCollapsed = false
if (immersiveMode) this.window.statusBarColor =
ContextCompat.getColor(this, R.color.status)
binding.characterAppBar.setBackgroundResource(R.color.bg)
ContextCompat.getColor(this, R.color.transparent)
}
}
}

View File

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

View File

@@ -67,11 +67,12 @@ class GenreActivity : AppCompatActivity() {
private fun loadLocalGenres(): ArrayList<String>? {
val genres = PrefManager.getVal<Set<String>>(PrefName.GenresList)
.toMutableList() as ArrayList<String>?
return if (genres.isNullOrEmpty()) {
.toMutableList()
return if (genres.isEmpty()) {
null
} else {
genres
//sort alphabetically
genres.sort().let { genres as ArrayList<String> }
}
}
}

View File

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

View File

@@ -1,8 +1,6 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
@@ -15,25 +13,25 @@ import android.widget.ImageView
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
import androidx.core.util.Pair
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.*
import ani.dantotsu.R
import ani.dantotsu.blurImage
import ani.dantotsu.currActivity
import ani.dantotsu.databinding.ItemMediaCompactBinding
import ani.dantotsu.databinding.ItemMediaLargeBinding
import ani.dantotsu.databinding.ItemMediaPageBinding
import ani.dantotsu.databinding.ItemMediaPageSmallBinding
import ani.dantotsu.loadImage
import ani.dantotsu.setAnimation
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.request.RequestOptions
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
import jp.wasabeef.glide.transformations.BlurTransformation
import java.io.Serializable
@@ -43,6 +41,7 @@ class MediaAdaptor(
private val activity: FragmentActivity,
private val matchParent: Boolean = false,
private val viewPager: ViewPager2? = null,
private val fav: Boolean = false,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
@@ -84,7 +83,7 @@ class MediaAdaptor(
}
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
@SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (type) {
0 -> {
@@ -93,8 +92,8 @@ class MediaAdaptor(
val media = mediaList?.getOrNull(position)
if (media != null) {
b.itemCompactImage.loadImage(media.cover)
b.itemCompactOngoing.visibility =
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
b.itemCompactOngoing.isVisible =
media.status == currActivity()!!.getString(R.string.status_releasing)
b.itemCompactTitle.text = media.userPreferredName
b.itemCompactScore.text =
((if (media.userScore == 0) (media.meanScore
@@ -128,6 +127,7 @@ class MediaAdaptor(
)
b.itemCompactTotal.text = " | ${media.manga.totalChapters ?: "~"}"
}
b.itemCompactProgressContainer.visibility = if (fav) View.GONE else View.VISIBLE
}
}
@@ -137,9 +137,9 @@ class MediaAdaptor(
val media = mediaList?.get(position)
if (media != null) {
b.itemCompactImage.loadImage(media.cover)
b.itemCompactBanner.loadImage(media.banner ?: media.cover)
b.itemCompactOngoing.visibility =
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
blurImage(b.itemCompactBanner, media.banner ?: media.cover)
b.itemCompactOngoing.isVisible =
media.status == currActivity()!!.getString(R.string.status_releasing)
b.itemCompactTitle.text = media.userPreferredName
b.itemCompactScore.text =
((if (media.userScore == 0) (media.meanScore
@@ -149,25 +149,30 @@ class MediaAdaptor(
(if (media.userScore != 0) R.drawable.item_user_score else R.drawable.item_score)
)
if (media.anime != null) {
b.itemTotal.text = " " + if ((media.anime.totalEpisodes
val itemTotal = " " + if ((media.anime.totalEpisodes
?: 0) != 1
) currActivity()!!.getString(R.string.episode_plural)
else currActivity()!!.getString(R.string.episode_singular)
) currActivity()!!.getString(R.string.episode_plural) else currActivity()!!.getString(
R.string.episode_singular
)
b.itemTotal.text = itemTotal
b.itemCompactTotal.text =
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " / " + (media.anime.totalEpisodes
?: "??").toString()) else (media.anime.totalEpisodes
?: "??").toString()
} else if (media.manga != null) {
b.itemTotal.text = " " + if ((media.manga.totalChapters
val itemTotal = " " + if ((media.manga.totalChapters
?: 0) != 1
) currActivity()!!.getString(R.string.chapter_plural)
else currActivity()!!.getString(R.string.chapter_singular)
) currActivity()!!.getString(R.string.chapter_plural) else currActivity()!!.getString(
R.string.chapter_singular
)
b.itemTotal.text = itemTotal
b.itemCompactTotal.text = "${media.manga.totalChapters ?: "??"}"
}
@SuppressLint("NotifyDataSetChanged")
if (position == mediaList!!.size - 2 && viewPager != null) viewPager.post {
val start = mediaList.size
mediaList.addAll(mediaList)
notifyDataSetChanged()
val end = mediaList.size - start
notifyItemRangeInserted(start, end)
}
}
}
@@ -176,6 +181,7 @@ class MediaAdaptor(
val b = (holder as MediaPageViewHolder).binding
val media = mediaList?.get(position)
if (media != null) {
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
b.itemCompactImage.loadImage(media.cover)
if (bannerAnimations)
@@ -185,17 +191,12 @@ class MediaAdaptor(
AccelerateDecelerateInterpolator()
)
)
val banner =
if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
val context = b.itemCompactBanner.context
if (!(context as Activity).isDestroyed)
Glide.with(context as Context)
.load(GlideUrl(media.banner ?: media.cover))
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
.into(banner)
b.itemCompactOngoing.visibility =
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
blurImage(
if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen,
media.banner ?: media.cover
)
b.itemCompactOngoing.isVisible =
media.status == currActivity()!!.getString(R.string.status_releasing)
b.itemCompactTitle.text = media.userPreferredName
b.itemCompactScore.text =
((if (media.userScore == 0) (media.meanScore
@@ -242,17 +243,12 @@ class MediaAdaptor(
AccelerateDecelerateInterpolator()
)
)
val banner =
if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen
val context = b.itemCompactBanner.context
if (!(context as Activity).isDestroyed)
Glide.with(context as Context)
.load(GlideUrl(media.banner ?: media.cover))
.diskCacheStrategy(DiskCacheStrategy.ALL).override(400)
.apply(RequestOptions.bitmapTransform(BlurTransformation(2, 3)))
.into(banner)
b.itemCompactOngoing.visibility =
if (media.status == currActivity()!!.getString(R.string.status_releasing)) View.VISIBLE else View.GONE
blurImage(
if (bannerAnimations) b.itemCompactBanner else b.itemCompactBannerNoKen,
media.banner ?: media.cover
)
b.itemCompactOngoing.isVisible =
media.status == currActivity()!!.getString(R.string.status_releasing)
b.itemCompactTitle.text = media.userPreferredName
b.itemCompactScore.text =
((if (media.userScore == 0) (media.meanScore

View File

@@ -3,43 +3,50 @@ package ani.dantotsu.media
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.text.SpannableStringBuilder
import android.util.TypedValue
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.ImageView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.core.text.bold
import androidx.core.text.color
import androidx.core.view.isVisible
import androidx.core.view.marginBottom
import androidx.core.view.updateLayoutParams
import androidx.core.view.updateMargins
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope
import androidx.viewpager2.adapter.FragmentStateAdapter
import ani.dantotsu.CustomBottomNavBar
import ani.dantotsu.GesturesListener
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.ZoomOutPageTransformer
import ani.dantotsu.blurImage
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.copyToClipboard
import ani.dantotsu.databinding.ActivityMediaBinding
import ani.dantotsu.getThemeColor
import ani.dantotsu.initActivity
import ani.dantotsu.loadImage
import ani.dantotsu.media.anime.AnimeWatchFragment
import ani.dantotsu.media.comments.CommentsFragment
import ani.dantotsu.media.manga.MangaReadFragment
import ani.dantotsu.media.novel.NovelReadFragment
import ani.dantotsu.navBarHeight
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.AndroidBug5497Workaround
import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.others.getSerialized
import ani.dantotsu.settings.saving.PrefManager
@@ -47,35 +54,49 @@ import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.LauncherWrapper
import com.flaviofaria.kenburnsview.RandomTransitionGenerator
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.navigation.NavigationBarView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import nl.joery.animatedbottombar.AnimatedBottomBar
import kotlin.math.abs
class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
private lateinit var binding: ActivityMediaBinding
lateinit var launcher: LauncherWrapper
lateinit var binding: ActivityMediaBinding
private val scope = lifecycleScope
private val model: MediaDetailsViewModel by viewModels()
private lateinit var tabLayout: NavigationBarView
var selected = 0
lateinit var navBar: AnimatedBottomBar
var anime = true
private var adult = false
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
@SuppressLint("ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
var media: Media = intent.getSerialized("media") ?: mediaSingleton ?: emptyMedia()
val id = intent.getIntExtra("mediaId", -1)
if (id != -1) {
runBlocking {
withContext(Dispatchers.IO) {
media = Anilist.query.getMedia(id, false) ?: emptyMedia()
}
}
}
if (media.name == "No media found") {
snackString(media.name)
onBackPressedDispatcher.onBackPressed()
return
}
val contract = ActivityResultContracts.OpenDocumentTree()
launcher = LauncherWrapper(this, contract)
mediaSingleton = null
ThemeManager(this).applyTheme(MediaSingleton.bitmap)
MediaSingleton.bitmap = null
@@ -83,21 +104,44 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
binding = ActivityMediaBinding.inflate(layoutInflater)
setContentView(binding.root)
screenWidth = resources.displayMetrics.widthPixels.toFloat()
navBar = binding.mediaBottomBar
//Ui init
// Ui init
initActivity(this)
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
val oldMargin = binding.mediaViewPager.marginBottom
AndroidBug5497Workaround.assistActivity(this) {
if (it) {
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = 0
}
navBar.visibility = View.GONE
} else {
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = oldMargin
}
navBar.visibility = View.VISIBLE
}
}
val navBarRightMargin = if (resources.configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE
) navBarHeight else 0
val navBarBottomMargin = if (resources.configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE
) 0 else navBarHeight
navBar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
rightMargin = navBarRightMargin
bottomMargin = navBarBottomMargin
}
binding.mediaBanner.updateLayoutParams { height += statusBarHeight }
binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight }
binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.incognito.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.mediaCollapsing.minimumHeight = statusBarHeight
if (binding.mediaTab is CustomBottomNavBar) binding.mediaTab.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
binding.mediaTitle.isSelected = true
mMaxScrollSize = binding.mediaAppBar.totalScrollRange
@@ -119,7 +163,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
val banner =
if (bannerAnimations) binding.mediaBanner else binding.mediaBannerNoKen
val viewPager = binding.mediaViewPager
tabLayout = binding.mediaTab as NavigationBarView
viewPager.isUserInputEnabled = false
viewPager.setPageTransformer(ZoomOutPageTransformer())
@@ -129,13 +172,15 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
binding.mediaCoverImage.loadImage(media.cover)
binding.mediaCoverImage.setOnLongClickListener {
val coverTitle = getString(R.string.cover, media.userPreferredName)
ImageViewDialog.newInstance(
this,
media.userPreferredName + "[Cover]",
coverTitle,
media.cover
)
}
banner.loadImage(media.banner ?: media.cover, 400)
blurImage(banner, media.banner ?: media.cover)
val gestureDetector = GestureDetector(this, object : GesturesListener() {
override fun onDoubleClick(event: MotionEvent) {
if (!(PrefManager.getVal(PrefName.BannerAnimations) as Boolean))
@@ -147,9 +192,10 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
}
override fun onLongClick(event: MotionEvent) {
val bannerTitle = getString(R.string.banner, media.userPreferredName)
ImageViewDialog.newInstance(
this@MediaDetailsActivity,
media.userPreferredName + "[Banner]",
bannerTitle,
media.banner ?: media.cover
)
banner.performClick()
@@ -157,7 +203,8 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
})
banner.setOnTouchListener { _, motionEvent -> gestureDetector.onTouchEvent(motionEvent);true }
if (PrefManager.getVal(PrefName.Incognito)) {
binding.mediaTitle.text = " ${media.userPreferredName}"
val mediaTitle = " ${media.userPreferredName}"
binding.mediaTitle.text = mediaTitle
binding.incognito.visibility = View.VISIBLE
} else {
binding.mediaTitle.text = media.userPreferredName
@@ -181,20 +228,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
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(
scope,
@@ -202,7 +235,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
R.drawable.ic_round_favorite_24,
R.drawable.ic_round_favorite_border_24,
R.color.bg_opp,
R.color.violet_400,//TODO: Change to colorSecondary
R.color.violet_400,
media.isFav
) {
media.isFav = it
@@ -217,22 +250,12 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
@SuppressLint("ResourceType")
fun total() {
val text = SpannableStringBuilder().apply {
val typedValue = TypedValue()
this@MediaDetailsActivity.theme.resolveAttribute(
com.google.android.material.R.attr.colorOnBackground,
typedValue,
true
)
val white = typedValue.data
val white = this@MediaDetailsActivity.getThemeColor(com.google.android.material.R.attr.colorOnBackground)
if (media.userStatus != null) {
append(if (media.anime != null) getString(R.string.watched_num) else getString(R.string.read_num))
val typedValue = TypedValue()
theme.resolveAttribute(
com.google.android.material.R.attr.colorSecondary,
typedValue,
true
)
bold { color(typedValue.data) { append("${media.userProgress}") } }
val colorSecondary = getThemeColor(com.google.android.material.R.attr.colorSecondary)
bold { color(colorSecondary) { append("${media.userProgress}") } }
append(
if (media.anime != null) getString(R.string.episodes_out_of) else getString(
R.string.chapters_out_of
@@ -313,49 +336,68 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
progress()
}
}
adult = media.isAdult
tabLayout.menu.clear()
if (media.anime != null) {
viewPager.adapter =
ViewPagerAdapter(supportFragmentManager, lifecycle, SupportedMedia.ANIME)
tabLayout.inflateMenu(R.menu.anime_menu_detail)
ViewPagerAdapter(
supportFragmentManager,
lifecycle,
SupportedMedia.ANIME,
media,
intent.getIntExtra("commentId", -1)
)
} else if (media.manga != null) {
viewPager.adapter = ViewPagerAdapter(
supportFragmentManager,
lifecycle,
if (media.format == "NOVEL") SupportedMedia.NOVEL else SupportedMedia.MANGA
if (media.format == "NOVEL") SupportedMedia.NOVEL else SupportedMedia.MANGA,
media,
intent.getIntExtra("commentId", -1)
)
if (media.format == "NOVEL") {
tabLayout.inflateMenu(R.menu.novel_menu_detail)
} else {
tabLayout.inflateMenu(R.menu.manga_menu_detail)
}
anime = false
}
selected = media.selected!!.window
binding.mediaTitle.translationX = -screenWidth
tabLayout.visibility = View.VISIBLE
tabLayout.setOnItemSelectedListener { item ->
selectFromID(item.itemId)
viewPager.setCurrentItem(selected, false)
val sel = model.loadSelected(media, isDownload)
sel.window = selected
model.saveSelected(media.id, sel)
true
val infoTab = navBar.createTab(R.drawable.ic_round_info_24, R.string.info, R.id.info)
val watchTab = if (anime) {
navBar.createTab(R.drawable.ic_round_movie_filter_24, R.string.watch, R.id.watch)
} else if (media.format == "NOVEL") {
navBar.createTab(R.drawable.ic_round_book_24, R.string.read, R.id.read)
} else {
navBar.createTab(R.drawable.ic_round_import_contacts_24, R.string.read, R.id.read)
}
tabLayout.selectedItemId = idFromSelect()
viewPager.setCurrentItem(selected, false)
val commentTab =
navBar.createTab(R.drawable.ic_round_comment_24, R.string.comments, R.id.comment)
navBar.addTab(infoTab)
navBar.addTab(watchTab)
navBar.addTab(commentTab)
if (model.continueMedia == null && media.cameFromContinue) {
model.continueMedia = PrefManager.getVal(PrefName.ContinueMedia)
selected = 1
}
if (intent.getStringExtra("FRAGMENT_TO_LOAD") != null) selected = 2
if (viewPager.currentItem != selected) viewPager.post {
viewPager.setCurrentItem(selected, false)
}
binding.commentInputLayout.isVisible = selected == 2
navBar.selectTabAt(selected)
navBar.setOnTabSelectListener(object : AnimatedBottomBar.OnTabSelectListener {
override fun onTabSelected(
lastIndex: Int,
lastTab: AnimatedBottomBar.Tab?,
newIndex: Int,
newTab: AnimatedBottomBar.Tab
) {
selected = newIndex
binding.commentInputLayout.isVisible = selected == 2
viewPager.setCurrentItem(selected, true)
val sel = model.loadSelected(media, isDownload)
sel.window = selected
model.saveSelected(media.id, sel)
}
})
val live = Refresh.activity.getOrPut(this.hashCode()) { MutableLiveData(true) }
live.observe(this) {
@@ -368,35 +410,21 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
}
}
private fun selectFromID(id: Int) {
when (id) {
R.id.info -> {
selected = 0
}
R.id.watch, R.id.read -> {
selected = 1
}
}
}
private fun idFromSelect(): Int {
if (anime) when (selected) {
0 -> return R.id.info
1 -> return R.id.watch
}
else when (selected) {
0 -> return R.id.info
1 -> return R.id.read
}
return R.id.info
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
val rightMargin = if (resources.configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE
) navBarHeight else 0
val bottomMargin = if (resources.configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE
) 0 else navBarHeight
val params: ViewGroup.MarginLayoutParams =
navBar.layoutParams as ViewGroup.MarginLayoutParams
params.updateMargins(right = rightMargin, bottom = bottomMargin)
}
override fun onResume() {
if (this::tabLayout.isInitialized) {
tabLayout.selectedItemId = idFromSelect()
}
navBar.selectTabAt(selected)
super.onResume()
}
@@ -404,24 +432,36 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
ANIME, MANGA, NOVEL
}
//ViewPager
// ViewPager
private class ViewPagerAdapter(
fragmentManager: FragmentManager,
lifecycle: Lifecycle,
private val media: SupportedMedia
private val mediaType: SupportedMedia,
private val media: Media,
private val commentId: Int
) :
FragmentStateAdapter(fragmentManager, lifecycle) {
override fun getItemCount(): Int = 2
override fun getItemCount(): Int = 3
override fun createFragment(position: Int): Fragment = when (position) {
0 -> MediaInfoFragment()
1 -> when (media) {
1 -> when (mediaType) {
SupportedMedia.ANIME -> AnimeWatchFragment()
SupportedMedia.MANGA -> MangaReadFragment()
SupportedMedia.NOVEL -> NovelReadFragment()
}
2 -> {
val fragment = CommentsFragment()
val bundle = Bundle()
bundle.putInt("mediaId", media.id)
bundle.putString("mediaName", media.mainName())
if (commentId != -1) bundle.putInt("commentId", commentId)
fragment.arguments = bundle
fragment
}
else -> MediaInfoFragment()
}
}
@@ -439,13 +479,6 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
binding.mediaCover.visibility =
if (binding.mediaCover.scaleX == 0f) View.GONE else View.VISIBLE
val duration = (200 * (PrefManager.getVal(PrefName.AnimationSpeed) as Float)).toLong()
val typedValue = TypedValue()
this@MediaDetailsActivity.theme.resolveAttribute(
com.google.android.material.R.attr.colorSecondary,
typedValue,
true
)
val color = typedValue.data
if (percentage >= percent && !isCollapsed) {
isCollapsed = true
ObjectAnimator.ofFloat(binding.mediaTitle, "translationX", 0f).setDuration(duration)
@@ -484,6 +517,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
private val c1: Int,
private val c2: Int,
var clicked: Boolean,
needsInitialClick: Boolean = false,
callback: suspend (Boolean) -> (Unit)
) {
private var disabled = false
@@ -492,6 +526,11 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
init {
enabled(true)
if (needsInitialClick) {
scope.launch {
clicked()
}
}
image.setOnClickListener {
if (pressable && !disabled) {
pressable = false
@@ -546,5 +585,4 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
companion object {
var mediaSingleton: Media? = null
}
}
}

View File

@@ -9,7 +9,6 @@ import androidx.lifecycle.ViewModel
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.currContext
import ani.dantotsu.logger
import ani.dantotsu.media.anime.Episode
import ani.dantotsu.media.anime.SelectorDialogFragment
import ani.dantotsu.media.manga.MangaChapter
@@ -29,6 +28,7 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.tryWithSuspend
import ani.dantotsu.util.Logger
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
@@ -52,26 +52,23 @@ class MediaDetailsViewModel : ViewModel() {
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
data.sourceIndex = when {
media.anime != null -> {
AnimeSources.list.size - 1
}
media.format == "MANGA" || media.format == "ONE_SHOT" -> {
MangaSources.list.size - 1
}
else -> {
NovelSources.list.size - 1
}
}
}
return data
}
fun loadSelectedStringLocation(sourceName: String): Int {
//find the location of the source in the list
var location = watchSources?.list?.indexOfFirst { it.name == sourceName } ?: 0
if (location == -1) {
location = 0
}
return location
}
var continueMedia: Boolean? = null
private var loading = false
@@ -152,10 +149,10 @@ class MediaDetailsViewModel : ViewModel() {
watchSources?.get(i)?.apply {
if (!post && !allowsPreloading) return@apply
ep.sEpisode?.let {
loadByVideoServers(link, ep.extra, it) {
if (it.videos.isNotEmpty()) {
list.add(it)
ep.extractorCallback?.invoke(it)
loadByVideoServers(link, ep.extra, it) { extractor ->
if (extractor.videos.isNotEmpty()) {
list.add(extractor)
ep.extractorCallback?.invoke(extractor)
}
}
}
@@ -223,7 +220,7 @@ class MediaDetailsViewModel : ViewModel() {
}
fun setEpisode(ep: Episode?, who: String) {
logger("set episode ${ep?.number} - $who", false)
Logger.log("set episode ${ep?.number} - $who")
episode.postValue(ep)
MainScope().launch(Dispatchers.Main) {
episode.value = null
@@ -270,7 +267,7 @@ class MediaDetailsViewModel : ViewModel() {
mangaChapters
suspend fun loadMangaChapters(media: Media, i: Int, invalidate: Boolean = false) {
logger("Loading Manga Chapters : $mangaLoaded")
Logger.log("Loading Manga Chapters : $mangaLoaded")
if (!mangaLoaded.containsKey(i) || invalidate) tryWithSuspend {
mangaLoaded[i] =
mangaReadSources?.loadChaptersFromMedia(i, media) ?: return@tryWithSuspend
@@ -291,7 +288,6 @@ class MediaDetailsViewModel : ViewModel() {
suspend fun loadMangaChapterImages(
chapter: MangaChapter,
selected: Selected,
series: String,
post: Boolean = true
): Boolean {

View File

@@ -15,16 +15,35 @@ import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.*
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.GenresViewModel
import ani.dantotsu.databinding.*
import ani.dantotsu.copyToClipboard
import ani.dantotsu.currActivity
import ani.dantotsu.databinding.ActivityGenreBinding
import ani.dantotsu.databinding.FragmentMediaInfoBinding
import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.databinding.ItemQuelsBinding
import ani.dantotsu.databinding.ItemTitleChipgroupBinding
import ani.dantotsu.databinding.ItemTitleRecyclerBinding
import ani.dantotsu.databinding.ItemTitleSearchBinding
import ani.dantotsu.databinding.ItemTitleTextBinding
import ani.dantotsu.databinding.ItemTitleTrailerBinding
import ani.dantotsu.displayTimer
import ani.dantotsu.isOnline
import ani.dantotsu.loadImage
import ani.dantotsu.navBarHeight
import ani.dantotsu.profile.User
import ani.dantotsu.px
import ani.dantotsu.setSafeOnClickListener
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import io.noties.markwon.Markwon
@@ -36,7 +55,6 @@ import java.io.Serializable
import java.net.URLEncoder
@SuppressLint("SetTextI18n")
class MediaInfoFragment : Fragment() {
private var _binding: FragmentMediaInfoBinding? = null
private val binding get() = _binding!!
@@ -45,6 +63,8 @@ class MediaInfoFragment : Fragment() {
private var type = "ANIME"
private val genreModel: GenresViewModel by activityViewModels()
private val tripleTab = "\t\t\t"
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -61,9 +81,9 @@ class MediaInfoFragment : Fragment() {
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val model: MediaDetailsViewModel by activityViewModels()
val offline: Boolean = PrefManager.getVal(PrefName.OfflineMode)
binding.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
binding.mediaInfoContainer.visibility = if (loaded) View.VISIBLE else View.GONE
val offline: Boolean = PrefManager.getVal(PrefName.OfflineMode) || !isOnline(requireContext())
binding.mediaInfoProgressBar.isGone = loaded
binding.mediaInfoContainer.isVisible = loaded
binding.mediaInfoContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += 128f.px + navBarHeight }
model.scrolledToTop.observe(viewLifecycleOwner) {
@@ -73,16 +93,19 @@ class MediaInfoFragment : Fragment() {
model.getMedia().observe(viewLifecycleOwner) { media ->
if (media != null && !loaded) {
loaded = true
binding.mediaInfoProgressBar.visibility = View.GONE
binding.mediaInfoContainer.visibility = View.VISIBLE
binding.mediaInfoName.text = "\t\t\t" + (media.name ?: media.nameRomaji)
val infoName = tripleTab + (media.name ?: media.nameRomaji)
binding.mediaInfoName.text = infoName
binding.mediaInfoName.setOnLongClickListener {
copyToClipboard(media.name ?: media.nameRomaji)
true
}
if (media.name != null) binding.mediaInfoNameRomajiContainer.visibility =
View.VISIBLE
binding.mediaInfoNameRomaji.text = "\t\t\t" + media.nameRomaji
val infoNameRomanji = tripleTab + media.nameRomaji
binding.mediaInfoNameRomaji.text = infoNameRomanji
binding.mediaInfoNameRomaji.setOnLongClickListener {
copyToClipboard(media.nameRomaji)
true
@@ -94,6 +117,8 @@ class MediaInfoFragment : Fragment() {
binding.mediaInfoSource.text = media.source
binding.mediaInfoStart.text = media.startDate?.toString() ?: "??"
binding.mediaInfoEnd.text = media.endDate?.toString() ?: "??"
binding.mediaInfoPopularity.text = media.popularity.toString()
binding.mediaInfoFavorites.text = media.favourites.toString()
if (media.anime != null) {
val episodeDuration = media.anime.episodeDuration
@@ -122,8 +147,10 @@ class MediaInfoFragment : Fragment() {
}
binding.mediaInfoDurationContainer.visibility = View.VISIBLE
binding.mediaInfoSeasonContainer.visibility = View.VISIBLE
binding.mediaInfoSeason.text =
(media.anime.season ?: "??") + " " + (media.anime.seasonYear ?: "??")
val seasonInfo =
"${(media.anime.season ?: "??")} ${(media.anime.seasonYear ?: "??")}"
binding.mediaInfoSeason.text = seasonInfo
if (media.anime.mainStudio != null) {
binding.mediaInfoStudioContainer.visibility = View.VISIBLE
binding.mediaInfoStudio.text = media.anime.mainStudio!!.name
@@ -157,9 +184,12 @@ class MediaInfoFragment : Fragment() {
}
}
binding.mediaInfoTotalTitle.setText(R.string.total_eps)
binding.mediaInfoTotal.text =
if (media.anime.nextAiringEpisode != null) (media.anime.nextAiringEpisode.toString() + " | " + (media.anime.totalEpisodes
?: "~").toString()) else (media.anime.totalEpisodes ?: "~").toString()
val infoTotal = if (media.anime.nextAiringEpisode != null)
"${media.anime.nextAiringEpisode} | ${media.anime.totalEpisodes ?: "~"}"
else
(media.anime.totalEpisodes ?: "~").toString()
binding.mediaInfoTotal.text = infoTotal
} else if (media.manga != null) {
type = "MANGA"
binding.mediaInfoTotalTitle.setText(R.string.total_chaps)
@@ -186,8 +216,10 @@ class MediaInfoFragment : Fragment() {
(media.description ?: "null").replace("\\n", "<br>").replace("\\\"", "\""),
HtmlCompat.FROM_HTML_MODE_LEGACY
)
binding.mediaInfoDescription.text =
"\t\t\t" + if (desc.toString() != "null") desc else getString(R.string.no_description_available)
val infoDesc =
tripleTab + if (desc.toString() != "null") desc else getString(R.string.no_description_available)
binding.mediaInfoDescription.text = infoDesc
binding.mediaInfoDescription.setOnClickListener {
if (binding.mediaInfoDescription.maxLines == 5) {
ObjectAnimator.ofInt(binding.mediaInfoDescription, "maxLines", 100)
@@ -197,8 +229,7 @@ class MediaInfoFragment : Fragment() {
.setDuration(400).start()
}
}
countDown(media, binding.mediaInfoContainer)
displayTimer(media, binding.mediaInfoContainer)
val parent = _binding?.mediaInfoContainer!!
val screenWidth = resources.displayMetrics.run { widthPixels / density }
@@ -220,7 +251,39 @@ class MediaInfoFragment : Fragment() {
}
parent.addView(bind.root)
}
if (!media.users.isNullOrEmpty() && !offline) {
val users: ArrayList<User> = media.users ?: arrayListOf()
if (Anilist.token != null && media.userStatus != null) {
users.add(0,
User(
id = Anilist.userid!!,
name = getString(R.string.you),
pfp = Anilist.avatar,
banner = "",
status = media.userStatus,
score = media.userScore.toFloat(),
progress = media.userProgress,
totalEpisodes = media.anime?.totalEpisodes ?: media.manga?.totalChapters,
nextAiringEpisode = media.anime?.nextAiringEpisode
)
)
}
ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context),
parent,
false
).apply {
itemTitle.visibility = View.GONE
itemRecycler.adapter =
MediaSocialAdapter(users, type, requireActivity())
itemRecycler.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
parent.addView(root)
}
}
if (media.trailer != null && !offline) {
@Suppress("DEPRECATION")
class MyChrome : WebChromeClient() {
@@ -408,101 +471,140 @@ class MediaInfoFragment : Fragment() {
parent.addView(bind.root)
}
if (!media.characters.isNullOrEmpty() && !offline) {
val bind = ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context),
parent,
false
)
bind.itemTitle.setText(R.string.characters)
bind.itemRecycler.adapter =
CharacterAdapter(media.characters!!)
bind.itemRecycler.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
parent.addView(bind.root)
}
if (!media.relations.isNullOrEmpty() && !offline) {
if (media.sequel != null || media.prequel != null) {
val bind = ItemQuelsBinding.inflate(
ItemQuelsBinding.inflate(
LayoutInflater.from(context),
parent,
false
)
).apply {
if (media.sequel != null) {
bind.mediaInfoSequel.visibility = View.VISIBLE
bind.mediaInfoSequelImage.loadImage(
media.sequel!!.banner ?: media.sequel!!.cover
)
bind.mediaInfoSequel.setSafeOnClickListener {
ContextCompat.startActivity(
requireContext(),
Intent(
requireContext(),
MediaDetailsActivity::class.java
).putExtra(
"media",
media.sequel as Serializable
), null
if (media.sequel != null) {
mediaInfoSequel.visibility = View.VISIBLE
mediaInfoSequelImage.loadImage(
media.sequel!!.banner ?: media.sequel!!.cover
)
}
}
if (media.prequel != null) {
bind.mediaInfoPrequel.visibility = View.VISIBLE
bind.mediaInfoPrequelImage.loadImage(
media.prequel!!.banner ?: media.prequel!!.cover
)
bind.mediaInfoPrequel.setSafeOnClickListener {
ContextCompat.startActivity(
requireContext(),
Intent(
mediaInfoSequel.setSafeOnClickListener {
ContextCompat.startActivity(
requireContext(),
MediaDetailsActivity::class.java
).putExtra(
"media",
media.prequel as Serializable
), null
)
Intent(
requireContext(),
MediaDetailsActivity::class.java
).putExtra(
"media",
media.sequel as Serializable
), null
)
}
}
if (media.prequel != null) {
mediaInfoPrequel.visibility = View.VISIBLE
mediaInfoPrequelImage.loadImage(
media.prequel!!.banner ?: media.prequel!!.cover
)
mediaInfoPrequel.setSafeOnClickListener {
ContextCompat.startActivity(
requireContext(),
Intent(
requireContext(),
MediaDetailsActivity::class.java
).putExtra(
"media",
media.prequel as Serializable
), null
)
}
}
parent.addView(root)
}
ItemTitleSearchBinding.inflate(
LayoutInflater.from(context),
parent,
false
).apply {
titleSearchImage.loadImage(media.banner ?: media.cover)
titleSearchText.text =
getString(R.string.search_title, media.mainName())
titleSearchCard.setSafeOnClickListener {
val query = Intent(requireContext(), SearchActivity::class.java)
.putExtra("type", "ANIME")
.putExtra("query", media.mainName())
.putExtra("search", true)
ContextCompat.startActivity(requireContext(), query, null)
}
parent.addView(root)
}
parent.addView(bind.root)
}
val bindi = ItemTitleRecyclerBinding.inflate(
ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context),
parent,
false
)
).apply {
bindi.itemRecycler.adapter =
MediaAdaptor(0, media.relations!!, requireActivity())
bindi.itemRecycler.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
parent.addView(bindi.root)
itemRecycler.adapter =
MediaAdaptor(0, media.relations!!, requireActivity())
itemRecycler.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
parent.addView(root)
}
}
if (!media.recommendations.isNullOrEmpty() && !offline) {
val bind = ItemTitleRecyclerBinding.inflate(
if (!media.characters.isNullOrEmpty() && !offline) {
ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context),
parent,
false
)
bind.itemTitle.setText(R.string.recommended)
bind.itemRecycler.adapter =
MediaAdaptor(0, media.recommendations!!, requireActivity())
bind.itemRecycler.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
).apply {
itemTitle.setText(R.string.characters)
itemRecycler.adapter =
CharacterAdapter(media.characters!!)
itemRecycler.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
parent.addView(root)
}
}
if (!media.staff.isNullOrEmpty() && !offline) {
ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context),
parent,
false
)
parent.addView(bind.root)
).apply {
itemTitle.setText(R.string.staff)
itemRecycler.adapter =
AuthorAdapter(media.staff!!)
itemRecycler.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
parent.addView(root)
}
}
if (!media.recommendations.isNullOrEmpty() && !offline) {
ItemTitleRecyclerBinding.inflate(
LayoutInflater.from(context),
parent,
false
).apply {
itemTitle.setText(R.string.recommended)
itemRecycler.adapter =
MediaAdaptor(0, media.recommendations!!, requireActivity())
itemRecycler.layoutManager = LinearLayoutManager(
requireContext(),
LinearLayoutManager.HORIZONTAL,
false
)
parent.addView(root)
}
}
}
}
@@ -527,11 +629,12 @@ class MediaInfoFragment : Fragment() {
}
}
}
super.onViewCreated(view, null)
}
override fun onResume() {
binding.mediaInfoProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE
binding.mediaInfoProgressBar.isGone = loaded
super.onResume()
}

View File

@@ -1,6 +1,5 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.os.Bundle
import android.text.InputFilter.LengthFilter
import android.view.Gravity
@@ -11,11 +10,19 @@ import android.widget.ArrayAdapter
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import ani.dantotsu.*
import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.DatePickerFragment
import ani.dantotsu.InputFilterMinMax
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.databinding.BottomSheetMediaListBinding
import ani.dantotsu.navBarHeight
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.snackString
import ani.dantotsu.tryWith
import com.google.android.material.materialswitch.MaterialSwitch
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -36,7 +43,6 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
return binding.root
}
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
var media: Media?
@@ -168,9 +174,10 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
val init =
if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString()
.toInt() else 0
if (init < (total
?: 5000)
) binding.mediaListProgress.setText((init + 1).toString())
if (init < (total ?: 5000)) {
val progressText = "${init + 1}"
binding.mediaListProgress.setText(progressText)
}
if (init + 1 == (total ?: 5000)) {
binding.mediaListStatus.setText(statusStrings[2], false)
onComplete()
@@ -181,7 +188,12 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
binding.mediaListPrivate.setOnCheckedChangeListener { _, checked ->
media?.isListPrivate = checked
}
val removeList = PrefManager.getCustomVal("removeList", setOf<Int>())
var remove: Boolean? = null
binding.mediaListShow.isChecked = media?.id in removeList
binding.mediaListShow.setOnCheckedChangeListener { _, checked ->
remove = checked
}
media?.userRepeat?.apply {
binding.mediaListRewatch.setText(this.toString())
}
@@ -247,6 +259,11 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
)
}
}
if (remove == true) {
PrefManager.setCustomVal("removeList", removeList.plus(media!!.id))
} else if (remove == false) {
PrefManager.setCustomVal("removeList", removeList.minus(media!!.id))
}
Refresh.all()
snackString(getString(R.string.list_updated))
dismissAllowingStateLoss()
@@ -254,20 +271,29 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
}
binding.mediaListDelete.setOnClickListener {
val id = media!!.userListId
if (id != null) {
scope.launch {
withContext(Dispatchers.IO) {
Anilist.mutation.deleteList(id)
var id = media!!.userListId
scope.launch {
withContext(Dispatchers.IO) {
if (id != null) {
Anilist.mutation.deleteList(id!!)
MAL.query.deleteList(media?.anime != null, media?.idMAL)
} else {
val profile = Anilist.query.userMediaDetails(media!!)
profile.userListId?.let { listId ->
id = listId
Anilist.mutation.deleteList(listId)
MAL.query.deleteList(media?.anime != null, media?.idMAL)
}
}
Refresh.all()
snackString(getString(R.string.deleted_from_list))
dismissAllowingStateLoss()
}
PrefManager.setCustomVal("removeList", removeList.minus(media?.id))
}
if (id != null) {
Refresh.all()
snackString(getString(R.string.deleted_from_list))
dismissAllowingStateLoss()
} else {
snackString(getString(R.string.no_list_id))
Refresh.all()
}
}
}

View File

@@ -1,6 +1,5 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.os.Bundle
import android.text.InputFilter.LengthFilter
import android.view.Gravity
@@ -10,11 +9,17 @@ import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
import ani.dantotsu.*
import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.InputFilterMinMax
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.databinding.BottomSheetMediaListSmallBinding
import ani.dantotsu.navBarHeight
import ani.dantotsu.others.getSerialized
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.snackString
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -54,10 +59,43 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
}
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
val scope = viewLifecycleOwner.lifecycleScope
binding.mediaListDelete.setOnClickListener {
var id = media.userListId
viewLifecycleOwner.lifecycleScope.launch {
withContext(Dispatchers.IO) {
if (id != null) {
try {
Anilist.mutation.deleteList(id!!)
MAL.query.deleteList(media.anime != null, media.idMAL)
} catch (e: Exception) {
withContext(Dispatchers.Main) {
snackString(getString(R.string.delete_fail_reason, e.message))
}
return@withContext
}
} else {
val profile = Anilist.query.userMediaDetails(media)
profile.userListId?.let { listId ->
id = listId
Anilist.mutation.deleteList(listId)
MAL.query.deleteList(media.anime != null, media.idMAL)
}
}
}
withContext(Dispatchers.Main) {
if (id != null) {
Refresh.all()
snackString(getString(R.string.deleted_from_list))
dismissAllowingStateLoss()
} else {
snackString(getString(R.string.no_list_id))
}
}
}
}
binding.mediaListProgressBar.visibility = View.GONE
binding.mediaListLayout.visibility = View.VISIBLE
@@ -120,7 +158,10 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
val init =
if (binding.mediaListProgress.text.toString() != "") binding.mediaListProgress.text.toString()
.toInt() else 0
if (init < (total ?: 5000)) binding.mediaListProgress.setText((init + 1).toString())
if (init < (total ?: 5000)) {
val progressText = "${init + 1}"
binding.mediaListProgress.setText(progressText)
}
if (init + 1 == (total ?: 5000)) {
binding.mediaListStatus.setText(statusStrings[2], false)
}
@@ -130,7 +171,12 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
binding.mediaListPrivate.setOnCheckedChangeListener { _, checked ->
media.isListPrivate = checked
}
val removeList = PrefManager.getCustomVal("removeList", setOf<Int>())
var remove: Boolean? = null
binding.mediaListShow.isChecked = media.id in removeList
binding.mediaListShow.setOnCheckedChangeListener { _, checked ->
remove = checked
}
binding.mediaListSave.setOnClickListener {
scope.launch {
withContext(Dispatchers.IO) {
@@ -158,6 +204,11 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
)
}
}
if (remove == true) {
PrefManager.setCustomVal("removeList", removeList.plus(media.id))
} else if (remove == false) {
PrefManager.setCustomVal("removeList", removeList.minus(media.id))
}
Refresh.all()
snackString(getString(R.string.list_updated))
dismissAllowingStateLoss()

View File

@@ -0,0 +1,88 @@
package ani.dantotsu.media
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.view.Window
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams
import androidx.recyclerview.widget.GridLayoutManager
import ani.dantotsu.R
import ani.dantotsu.databinding.ActivityMediaListViewBinding
import ani.dantotsu.getThemeColor
import ani.dantotsu.hideSystemBarsExtendView
import ani.dantotsu.initActivity
import ani.dantotsu.others.getSerialized
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import java.util.ArrayList
class MediaListViewActivity: AppCompatActivity() {
private lateinit var binding: ActivityMediaListViewBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMediaListViewBinding.inflate(layoutInflater)
ThemeManager(this).applyTheme()
initActivity(this)
if (!PrefManager.getVal<Boolean>(PrefName.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)
hideSystemBarsExtendView()
binding.settingsContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = statusBarHeight
}
}
setContentView(binding.root)
val primaryColor = getThemeColor(com.google.android.material.R.attr.colorSurface)
val primaryTextColor = getThemeColor(com.google.android.material.R.attr.colorPrimary)
val secondaryTextColor = getThemeColor(com.google.android.material.R.attr.colorOutline)
window.statusBarColor = primaryColor
window.navigationBarColor = primaryColor
binding.listAppBar.setBackgroundColor(primaryColor)
binding.listTitle.setTextColor(primaryTextColor)
val screenWidth = resources.displayMetrics.run { widthPixels / density }
val mediaList = intent.getSerialized("media") as? ArrayList<Media> ?: ArrayList()
val view = PrefManager.getCustomVal("mediaView", 0)
var mediaView: View = when (view) {
1 -> binding.mediaList
0 -> binding.mediaGrid
else -> binding.mediaGrid
}
mediaView.alpha = 1f
fun changeView(mode: Int, current: View) {
mediaView.alpha = 0.33f
mediaView = current
current.alpha = 1f
PrefManager.setCustomVal("mediaView", mode)
binding.mediaRecyclerView.adapter = MediaAdaptor(mode, mediaList, this)
binding.mediaRecyclerView.layoutManager = GridLayoutManager(
this,
if (mode == 1) 1 else (screenWidth / 120f).toInt()
)
}
binding.mediaList.setOnClickListener {
changeView(1, binding.mediaList)
}
binding.mediaGrid.setOnClickListener {
changeView(0, binding.mediaGrid)
}
binding.listTitle.text = intent.getStringExtra("title")
binding.mediaRecyclerView.adapter = MediaAdaptor(view, mediaList, this)
binding.mediaRecyclerView.layoutManager = GridLayoutManager(
this,
if (view == 1) 1 else (screenWidth / 120f).toInt()
)
}
}

View File

@@ -0,0 +1,146 @@
package ani.dantotsu.media
import java.util.Locale
import java.util.regex.Matcher
import java.util.regex.Pattern
object MediaNameAdapter {
private const val REGEX_ITEM = "[\\s:.\\-]*(\\d+\\.?\\d*)[\\s:.\\-]*"
private const val REGEX_PART_NUMBER = "(?<!part\\s)\\b(\\d+)\\b"
private const val REGEX_EPISODE =
"(episode|episodio|ep|e)${REGEX_ITEM}\\(?\\s*(sub|subbed|dub|dubbed)*\\s*\\)?\\s*"
private const val REGEX_SEASON = "(season|s)[\\s:.\\-]*(\\d+)[\\s:.\\-]*"
private const val REGEX_SUBDUB = "^(soft)?[\\s-]*(sub|dub|mixed)(bed|s)?\\s*$"
private const val REGEX_CHAPTER = "(chapter|chap|ch|c)${REGEX_ITEM}"
fun setSubDub(text: String, typeToSetTo: SubDubType): String? {
val subdubPattern: Pattern = Pattern.compile(REGEX_SUBDUB, Pattern.CASE_INSENSITIVE)
val subdubMatcher: Matcher = subdubPattern.matcher(text)
return if (subdubMatcher.find()) {
val soft = subdubMatcher.group(1)
val subdub = subdubMatcher.group(2)
val bed = subdubMatcher.group(3) ?: ""
val toggled = when (typeToSetTo) {
SubDubType.SUB -> "sub"
SubDubType.DUB -> "dub"
SubDubType.NULL -> ""
}
val toggledCasePreserved =
if (subdub?.get(0)?.isUpperCase() == true || soft?.get(0)
?.isUpperCase() == true
) toggled.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(
Locale.ROOT
) else it.toString()
} else toggled
subdubMatcher.replaceFirst(toggledCasePreserved + bed)
} else {
null
}
}
fun getSubDub(text: String): SubDubType {
val subdubPattern: Pattern = Pattern.compile(REGEX_SUBDUB, Pattern.CASE_INSENSITIVE)
val subdubMatcher: Matcher = subdubPattern.matcher(text)
return if (subdubMatcher.find()) {
val subdub = subdubMatcher.group(2)?.lowercase(Locale.ROOT)
when (subdub) {
"sub" -> SubDubType.SUB
"dub" -> SubDubType.DUB
else -> SubDubType.NULL
}
} else {
SubDubType.NULL
}
}
enum class SubDubType {
SUB, DUB, NULL
}
fun findSeasonNumber(text: String): Int? {
val seasonPattern: Pattern = Pattern.compile(REGEX_SEASON, Pattern.CASE_INSENSITIVE)
val seasonMatcher: Matcher = seasonPattern.matcher(text)
return if (seasonMatcher.find()) {
seasonMatcher.group(2)?.toInt()
} else {
text.toIntOrNull()
}
}
fun findEpisodeNumber(text: String): Float? {
val episodePattern: Pattern = Pattern.compile(REGEX_EPISODE, Pattern.CASE_INSENSITIVE)
val episodeMatcher: Matcher = episodePattern.matcher(text)
return if (episodeMatcher.find()) {
if (episodeMatcher.group(2) != null) {
episodeMatcher.group(2)?.toFloat()
} else {
val failedEpisodeNumberPattern: Pattern =
Pattern.compile(REGEX_PART_NUMBER, Pattern.CASE_INSENSITIVE)
val failedEpisodeNumberMatcher: Matcher =
failedEpisodeNumberPattern.matcher(text)
if (failedEpisodeNumberMatcher.find()) {
failedEpisodeNumberMatcher.group(1)?.toFloat()
} else {
null
}
}
} else {
text.toFloatOrNull()
}
}
fun removeEpisodeNumber(text: String): String {
val regexPattern = Regex(REGEX_EPISODE, RegexOption.IGNORE_CASE)
val removedNumber = text.replace(regexPattern, "").ifEmpty {
text
}
val letterPattern = Regex("[a-zA-Z]")
return if (letterPattern.containsMatchIn(removedNumber)) {
removedNumber
} else {
text
}
}
fun removeEpisodeNumberCompletely(text: String): String {
val regexPattern = Regex(REGEX_EPISODE, RegexOption.IGNORE_CASE)
val removedNumber = text.replace(regexPattern, "")
return if (removedNumber.equals(text, true)) { // if nothing was removed
val failedEpisodeNumberPattern =
Regex(REGEX_PART_NUMBER, RegexOption.IGNORE_CASE)
failedEpisodeNumberPattern.replace(removedNumber) { mr ->
mr.value.replaceFirst(mr.groupValues[1], "")
}
} else {
removedNumber
}
}
fun findChapterNumber(text: String): Float? {
val pattern: Pattern = Pattern.compile(REGEX_CHAPTER, Pattern.CASE_INSENSITIVE)
val matcher: Matcher = pattern.matcher(text)
return if (matcher.find()) {
matcher.group(2)?.toFloat()
} else {
val failedChapterNumberPattern: Pattern =
Pattern.compile(REGEX_PART_NUMBER, Pattern.CASE_INSENSITIVE)
val failedChapterNumberMatcher: Matcher =
failedChapterNumberPattern.matcher(text)
if (failedChapterNumberMatcher.find()) {
failedChapterNumberMatcher.group(1)?.toFloat()
} else {
text.toFloatOrNull()
}
}
}
}

View File

@@ -0,0 +1,82 @@
package ani.dantotsu.media
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.databinding.ItemFollowerGridBinding
import ani.dantotsu.getAppString
import ani.dantotsu.loadImage
import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.User
import ani.dantotsu.setAnimation
class MediaSocialAdapter(
val user: ArrayList<User>,
val type: String,
val activity: FragmentActivity
) : RecyclerView.Adapter<MediaSocialAdapter.FollowerGridViewHolder>() {
inner class FollowerGridViewHolder(val binding: ItemFollowerGridBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FollowerGridViewHolder {
return FollowerGridViewHolder(
ItemFollowerGridBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: FollowerGridViewHolder, position: Int) {
holder.binding.apply {
val user = user[position]
val score = user.score?.div(10.0) ?: 0.0
setAnimation(root.context, root)
profileUserName.text = user.name
profileInfo.apply {
text = when (user.status) {
"CURRENT" -> if (type == "ANIME") getAppString(R.string.watching) else getAppString(R.string.reading)
else -> user.status ?: ""
}
visibility = View.VISIBLE
}
profileCompactUserProgress.text = user.progress.toString()
profileCompactScore.text = score.toString()
" | ${user.totalEpisodes ?: "~"}".also { profileCompactTotal.text = it }
profileUserAvatar.loadImage(user.pfp)
val scoreDrawable = if (score == 0.0) R.drawable.score else R.drawable.user_score
profileCompactScoreBG.apply {
visibility = View.VISIBLE
background = ContextCompat.getDrawable(root.context, scoreDrawable)
}
profileCompactProgressContainer.visibility = View.VISIBLE
profileUserAvatar.setOnClickListener {
ContextCompat.startActivity(root.context,
Intent(root.context, ProfileActivity::class.java)
.putExtra("userId", user.id),
null)
}
profileUserAvatarContainer.setOnLongClickListener {
ImageViewDialog.newInstance(
activity,
activity.getString(R.string.avatar, user.name),
user.pfp
)
}
}
}
override fun getItemCount(): Int = user.size
}

View File

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

View File

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

View File

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

View File

@@ -4,24 +4,30 @@ import android.annotation.SuppressLint
import android.os.Bundle
import android.os.Parcelable
import android.view.View
import android.view.WindowManager
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.core.view.updatePaddingRelative
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.*
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistSearch
import ani.dantotsu.connections.anilist.SearchResults
import ani.dantotsu.databinding.ActivitySearchBinding
import ani.dantotsu.initActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.px
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.*
import java.util.Timer
import java.util.TimerTask
class SearchActivity : AppCompatActivity() {
private lateinit var binding: ActivitySearchBinding
@@ -64,11 +70,18 @@ class SearchActivity : AppCompatActivity() {
intent.getStringExtra("type") ?: "ANIME",
isAdult = if (Anilist.adult) intent.getBooleanExtra("hentai", false) else false,
onList = listOnly,
search = intent.getStringExtra("query"),
genres = intent.getStringExtra("genre")?.let { mutableListOf(it) },
tags = intent.getStringExtra("tag")?.let { mutableListOf(it) },
sort = intent.getStringExtra("sortBy"),
status = intent.getStringExtra("status"),
source = intent.getStringExtra("source"),
countryOfOrigin = intent.getStringExtra("country"),
season = intent.getStringExtra("season"),
seasonYear = intent.getStringExtra("seasonYear")?.toIntOrNull(),
seasonYear = if (intent.getStringExtra("type") == "ANIME") intent.getStringExtra("seasonYear")
?.toIntOrNull() else null,
startYear = if (intent.getStringExtra("type") == "MANGA") intent.getStringExtra("seasonYear")
?.toIntOrNull() else null,
results = mutableListOf(),
hasNextPage = false
)
@@ -127,8 +140,12 @@ class SearchActivity : AppCompatActivity() {
excludedTags = it.excludedTags
tags = it.tags
season = it.season
startYear = it.startYear
seasonYear = it.seasonYear
status = it.status
source = it.source
format = it.format
countryOfOrigin = it.countryOfOrigin
page = it.page
hasNextPage = it.hasNextPage
}
@@ -137,7 +154,7 @@ class SearchActivity : AppCompatActivity() {
model.searchResults.results.addAll(it.results)
mediaAdaptor.notifyItemRangeInserted(prev, it.results.size)
progressAdapter.bar?.visibility = if (it.hasNextPage) View.VISIBLE else View.GONE
progressAdapter.bar?.isVisible = it.hasNextPage
}
}
@@ -151,7 +168,10 @@ class SearchActivity : AppCompatActivity() {
} else
headerAdaptor.requestFocus?.run()
if (intent.getBooleanExtra("search", false)) search()
if (intent.getBooleanExtra("search", false)) {
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED)
search()
}
}
}
}
@@ -199,7 +219,9 @@ class SearchActivity : AppCompatActivity() {
var state: Parcelable? = null
override fun onPause() {
headerAdaptor.addHistory()
if (this::headerAdaptor.isInitialized) {
headerAdaptor.addHistory()
}
super.onPause()
state = binding.searchRecyclerView.layoutManager?.onSaveInstanceState()
}

View File

@@ -1,6 +1,7 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.drawable.Drawable
import android.text.Editable
import android.text.TextWatcher
@@ -12,6 +13,7 @@ import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.PopupMenu
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.recyclerview.widget.LinearLayoutManager
@@ -23,9 +25,12 @@ import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.databinding.ItemSearchHeaderBinding
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.imagesearch.ImageSearchActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import com.google.android.material.checkbox.MaterialCheckBox.*
import com.google.android.material.checkbox.MaterialCheckBox.STATE_CHECKED
import com.google.android.material.checkbox.MaterialCheckBox.STATE_INDETERMINATE
import com.google.android.material.checkbox.MaterialCheckBox.STATE_UNCHECKED
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
@@ -41,6 +46,20 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
private lateinit var searchHistoryAdapter: SearchHistoryAdapter
private lateinit var binding: ItemSearchHeaderBinding
private fun updateFilterTextViewDrawable() {
val filterDrawable = when (activity.result.sort) {
Anilist.sortBy[0] -> R.drawable.ic_round_area_chart_24
Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24
Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24
Anilist.sortBy[3] -> R.drawable.ic_round_new_releases_24
Anilist.sortBy[4] -> R.drawable.ic_round_filter_list_24
Anilist.sortBy[5] -> R.drawable.ic_round_filter_list_24_reverse
Anilist.sortBy[6] -> R.drawable.ic_round_assist_walker_24
else -> R.drawable.ic_round_filter_alt_24
}
binding.filterTextView.setCompoundDrawablesWithIntrinsicBounds(filterDrawable, 0, 0, 0)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder {
val binding =
ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
@@ -89,16 +108,81 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
binding.searchAdultCheck.isChecked = adult
binding.searchList.isChecked = listOnly == true
binding.searchChipRecycler.adapter = SearchChipAdapter(activity).also {
binding.searchChipRecycler.adapter = SearchChipAdapter(activity, this).also {
activity.updateChips = { it.update() }
}
binding.searchChipRecycler.layoutManager =
LinearLayoutManager(binding.root.context, HORIZONTAL, false)
binding.searchFilter.setOnClickListener {
SearchFilterBottomDialog.newInstance().show(activity.supportFragmentManager, "dialog")
}
binding.searchFilter.setOnLongClickListener {
val popupMenu = PopupMenu(activity, binding.searchFilter)
popupMenu.menuInflater.inflate(R.menu.sortby_filter_menu, popupMenu.menu)
popupMenu.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.sort_by_score -> {
activity.result.sort = Anilist.sortBy[0]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_popular -> {
activity.result.sort = Anilist.sortBy[1]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_trending -> {
activity.result.sort = Anilist.sortBy[2]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_recent -> {
activity.result.sort = Anilist.sortBy[3]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_a_z -> {
activity.result.sort = Anilist.sortBy[4]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_z_a -> {
activity.result.sort = Anilist.sortBy[5]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_pure_pain -> {
activity.result.sort = Anilist.sortBy[6]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
}
true
}
popupMenu.show()
true
}
if (activity.result.type != "ANIME") {
binding.searchByImage.visibility = View.GONE
}
binding.searchByImage.setOnClickListener {
activity.startActivity(Intent(activity, ImageSearchActivity::class.java))
}
fun searchTitle() {
activity.result.apply {
search =
@@ -208,27 +292,28 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
binding.searchHistoryList.startAnimation(fadeInAnimation())
binding.searchResultLayout.visibility = View.GONE
binding.searchHistoryList.visibility = View.VISIBLE
binding.searchByImage.visibility = View.VISIBLE
} else {
if (binding.searchResultLayout.visibility != View.VISIBLE) {
binding.searchResultLayout.startAnimation(fadeInAnimation())
binding.searchHistoryList.startAnimation(fadeOutAnimation())
}
binding.searchResultLayout.visibility = View.VISIBLE
binding.searchHistoryList.visibility = View.GONE
binding.searchByImage.visibility = View.GONE
}
}
private fun fadeInAnimation(): Animation {
return AlphaAnimation(0f, 1f).apply {
duration = 150
fillAfter = true
}
}
private fun fadeOutAnimation(): Animation {
return AlphaAnimation(1f, 0f).apply {
duration = 150
fillAfter = true
}
}
@@ -247,7 +332,10 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
}
class SearchChipAdapter(val activity: SearchActivity) :
class SearchChipAdapter(
val activity: SearchActivity,
private val searchAdapter: SearchAdapter
) :
RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() {
private var chips = activity.result.toChipList()
@@ -264,11 +352,12 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
override fun onBindViewHolder(holder: SearchChipViewHolder, position: Int) {
val chip = chips[position]
holder.binding.root.apply {
text = chip.text
text = chip.text.replace("_", " ")
setOnClickListener {
activity.result.removeChip(chip)
update()
activity.search()
searchAdapter.updateFilterTextViewDrawable()
}
}
}
@@ -277,6 +366,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
fun update() {
chips = activity.result.toChipList()
notifyDataSetChanged()
searchAdapter.updateFilterTextViewDrawable()
}
override fun getItemCount(): Int = chips.size

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