Compare commits

...

939 Commits

Author SHA1 Message Date
rebel onion
a93b4f5b11 Merge branch 'main' of https://github.com/rebelonion/Dantotsu 2025-05-14 21:40:08 -05:00
rebel onion
69c44b7d20 chore: formatting changes 2025-05-14 21:40:06 -05:00
Rishvaish
a684aac0b1 To install multiple mangas (#582)
users can enter the value required to install as there is an EditText field instead of the Text View
2025-04-02 10:40:39 +05:30
Daniele Santoru
6c49839f87 Fixed missing manga pages when downloading (#586) 2025-04-02 10:39:33 +05:30
rebel onion
7053a7b4b2 Update README.md 2025-01-16 20:27:24 -06:00
rebel onion
1c156053d0 Merge pull request #565 from rebelonion/dev
Dev
2025-01-16 00:15:34 -06:00
rebel onion
6fa2f11db2 Merge branch 'main' into dev 2025-01-16 00:15:21 -06:00
rebel onion
a5babea27c chore: version bump 2025-01-16 00:14:25 -06:00
rebelonion
8a9b8cca7e fix: Serializable 2025-01-13 14:23:02 -06:00
rebel onion
7479f5f43b Update stable.md 2025-01-09 19:58:00 -06:00
Sadwhy
3ac9307329 Use custom alert builder for all dialogs [skip ci] 2025-01-09 18:04:22 +05:30
rebel onion
f606bef2a5 Merge pull request #559 from rebelonion/dev
Dev
2025-01-06 08:29:48 -06:00
rebel onion
f9f9767ecc chore: clean 2025-01-06 08:22:44 -06:00
rebel onion
31a67c8edb Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2025-01-05 20:23:59 -06:00
rebel onion
7fc69b4edd feat: alt update 2025-01-05 20:23:50 -06:00
rebel onion
9dd59bb592 Merge pull request #558 from rebelonion/dev
Dev
2025-01-05 08:56:36 -06:00
rebel onion
a8958a76cf Merge branch 'main' into dev 2025-01-05 08:56:20 -06:00
rebel onion
482e867516 Update RpcExternalAsset.kt 2025-01-05 08:51:29 -06:00
rebel onion
495322547e fix: null scanlator 2025-01-04 05:39:37 -06:00
rebel onion
19740c82f9 fix: search tiny shrink 2025-01-04 05:33:07 -06:00
rebel onion
3abfa821c7 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2025-01-04 05:21:22 -06:00
rebel onion
ef70fb5238 fix: character desc 2025-01-04 05:21:15 -06:00
rebel onion
4e8b6b5ff4 fix: bottom sheet oled mode 2025-01-04 05:17:19 -06:00
rebel onion
a90b7d5203 Merge pull request #556 from grayankit/dev
fix: discord embed
2025-01-04 05:08:27 -06:00
rebel onion
d422a1586f feat: search on home + setting 2025-01-04 05:06:06 -06:00
rebel onion
986d0fa4a8 fix: manga opening 2025-01-04 04:49:49 -06:00
Ankit Grai
66ed167bc8 fix: discord embed
there was somewhere where hex to decimal function was not used previous default color (also changed by me ) was out of color range hopefully it is finally fixed
2025-01-04 00:02:39 +05:30
aayush262
1bb5f4d0ab fix: padding somewhere 2025-01-03 23:08:53 +05:30
rebel onion
f6d05ec375 fix: only do stupid thing to manga 2025-01-03 10:51:46 -06:00
rebel onion
c48028f3cd fix: extensions not triggering update in app 2025-01-03 10:36:31 -06:00
rebel onion
e41ab2ddac fix: some scanlators not showing 2025-01-03 10:29:38 -06:00
rebel onion
0779c0ca71 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2025-01-03 10:06:53 -06:00
rebel onion
c229a5c717 chore: version bump 2025-01-03 10:06:44 -06:00
Ankit Grai
7c4689dea6 [skip ci] fix:oworkflow for rebel [maybe] (#554) 2025-01-03 21:33:53 +05:30
rebel onion
eec8605069 fix: extension installing 2025-01-03 10:01:45 -06:00
rebel onion
0d365d55c5 fix: ordering of search history 2025-01-03 09:54:29 -06:00
rebel onion
7b8af6ea8a feat: searching 2025-01-03 09:01:09 -06:00
rebel onion
38d68a7976 feat: allow partial urls 2025-01-02 03:14:59 -06:00
Ankit Grai
30d6f48d23 [skip ci] uncurse rebel (maybe) 2024-12-31 14:34:38 +05:30
rebel onion
88feb4d811 Merge pull request #552 from rebelonion/dev
Dev
2024-12-30 23:58:26 -06:00
rebel onion
116de6324e fix: separate update,delete buttons | TOS, privpol 2024-12-30 21:37:31 -06:00
rebel onion
b3e767d33d Create privacy_policy.md 2024-12-30 21:26:00 -06:00
rebel onion
43dee6ee49 feat: make repo adding easier 2024-12-30 19:25:22 -06:00
rebel onion
12c13be2aa Update README.md 2024-12-23 23:09:36 -06:00
Sadwhy
6f1bb10dec feat(subtitles): color picker. Clear data after this. (#547)
* feat(subtitle): custom color picker

* can't have two of the same buttons smh

* It was a misinput

* Too much
2024-12-19 10:03:15 +05:30
Itsmechinmoy
f0a8d9bfd4 [skip ci] Automatically Close Issues Related to Extensions and Repo Availability (#546) 2024-12-16 19:50:18 +05:30
Sadwhy
01a64c25fd fix: Kotlin fuckup with sdk 35 (#545)
https://youtrack.jetbrains.com/issue/KT-71375/Prevent-Kotlins-removeFirst-and-removeLast-from-causing-crashes-on-Android-14-and-below-after-upgrading-to-Android-API-Level-35
2024-12-16 00:07:42 -06:00
Sadwhy
b04a176870 feat(exoplayer): custom subtitle view (#544)
* branch

* Add Custom Subtitles
2024-12-15 20:49:40 +05:30
Sadwhy
eac4604b3d Prepare for Android 15 (#542)
* Bump Project gradle

* Bump dependencies

* Bump gradle to 8.7

* Bump gradle properties

* Add missing null safety

* Fix unresolved color

* Use alternative version
2024-12-14 01:33:01 +05:30
aayush262
1686854632 Merge pull request #534 from Sadwhy/patch-2
* Fix save state

* Add animation to dialog

* Clean redundant code
2024-12-11 22:50:16 +05:30
Sadwhy
8ad8637fce Clean redundant code 2024-12-11 15:25:19 +06:00
Sadwhy
6d32900568 Add animation to dialog 2024-12-11 13:40:08 +06:00
Vipul Tyagi
ce332d7ae5 feat(novel-reader): Add safe coroutine scope for EbookReaderView (#536) 2024-12-01 19:55:19 +05:30
rebel onion
0d22c92e3e Update beta.yml 2024-11-25 13:10:34 -06:00
rebel onion
0057363f4d Update beta.yml 2024-11-25 13:09:49 -06:00
Sadwhy
d1400ff422 Fix save state 2024-11-23 23:20:04 +06:00
Sadwhy
f13225e032 Feat(exoplayer): optional decoders (#533)
* Change Layout

* Add string

* Who did this?

* Add listener

* Made NextLib Conditional

* Add Preferences
2024-11-22 23:08:50 +05:30
aayush262
404d265a2d fix: feeds (hopefully) 2024-11-22 08:45:38 +05:30
aayush262
c2d69509fb fix: null story list for some users 2024-11-21 23:13:14 +05:30
Ankit Grai
d01e1c89e0 feat: Added anime clear progress (#531)
* feat: Added anime clear progress

* more stuff added
2024-11-20 23:27:03 +05:30
Sadwhy
ff3372754a feat(network): socks5 proxy support (#530) 2024-11-19 21:52:13 +05:30
Sadwhy
a4bd367f98 Feat(Exoplayer): Added additional codec support. (#528)
* Add dependency

* Add Decoders to Builder

* Remove Comments
2024-11-18 13:27:53 -06:00
tutel
9fa326c571 [skip ci] Added option to select the preferred language for subs (Resolves #239) (#393) 2024-11-18 23:32:58 +05:30
Sadwhy
3d4f5aaf4a [skip ci] fix(layout): Manga RTL fix (#527) 2024-11-17 23:37:41 +05:30
Ankit Grai
90c6c08b48 fix: Workflow hopefully (#526) 2024-11-17 19:11:46 +05:30
Sadwhy
d1e2ca8b5e feat(Media): Toggleable Comments (#521)
* Smooth theme transitions
2024-11-17 11:51:44 +05:30
Ankit Grai
56e557738c [skip ci] feat:Added button to view rules (#525)
* feat:added way to clear saved manga progress

* feat:Added button to view rules

* changed something
2024-11-17 10:29:07 +05:30
Itsmechinmoy
489dcc0b52 [skip ci] Updated Comment Rules (#524)
* Updated Comment Rules

* Update CommentsFragment.kt

* Update CommentsFragment.kt
2024-11-15 22:29:04 +05:30
Ankit Grai
a993935433 feat:added way to clear saved manga progress (#522) 2024-11-12 22:49:03 +05:30
Dawn-used-yeet
5c1c639f53 [skip ci] chore: Dawn (#519) 2024-11-10 17:31:39 +05:30
Itsmechinmoy
02ec4a3605 [skip ci] Update Contributors.kt (#520) 2024-11-10 17:19:28 +05:30
Itsmechinmoy
441094ca17 [skip ci] Rename congif.yml to config.yml (#516) 2024-11-09 20:56:28 +05:30
Itsmechinmoy
b2d7af85c0 [skip ci] Nothing (#515) 2024-11-09 20:34:43 +05:30
Sadwhy
95b558118a feat(settings): Smooth theme transitions (#514) 2024-11-08 15:54:19 +05:30
Sadwhy
b703337a16 feat(Settings): Toggleable RPC (#513) 2024-11-07 22:44:01 +05:30
aayush262
3071f88681 fix: manga rearrangement (hope it works) 2024-11-07 22:06:54 +05:30
aayush262
c242770435 fix: manga crash 2024-11-07 21:50:20 +05:30
aayush262
0fa2cf98d8 fix: manga not reordering automatically 2024-11-07 21:38:34 +05:30
aayush262
ffd9fecf26 fix: commit counts (hopefully) 2024-11-05 22:58:11 +05:30
aayush262
7ec889a915 feat(rpc): progress bar (hope nothing breaks) 2024-11-03 23:07:09 +05:30
aayush262
949ab7e87b Merge remote-tracking branch 'origin/dev' into dev 2024-11-02 23:51:37 +05:30
aayush262
cddad8edf1 feat(rpc): animated dantotsu icon 2024-11-02 23:51:20 +05:30
Ikko Eltociear Ashimine
4e76e8e6e7 chore: update SubtitleDownloader.kt (#509)
reponse -> response
2024-10-30 18:17:53 +05:30
Ankit Grai
a9331ffa32 changed telegram channel (#507) 2024-10-29 15:13:46 +05:30
aayush262
545abf1f9a fix(rpc): anilist icon 2024-10-29 13:58:36 +05:30
Dawn-used-yeet
f191502a97 fix: Swipy (#506) 2024-10-29 13:04:34 +05:30
Ankit Grai
652ef219dd Fix: Telegram Workflow (#505)
* Fix: telegram upload

* one more thing to check

* added dev's
2024-10-28 23:38:09 +05:30
Toby Bridle
e8ca3f2222 fix: deletion queries (#504)
* feat: introduce `deleteFromList` extension function for `Media`

* refactor: remove redundant deletion code, migrate to `deleteFromList`
2024-10-28 19:57:52 +05:30
Dawn-used-yeet
bd1f3388f7 fix: swipy (#501) 2024-10-27 22:56:11 +05:30
Ankit Grai
c37fefde73 Fix:Workflow (#503)
* workflow fix try

* AA Chart course
2024-10-27 22:40:24 +05:30
Ankit Grai
74e88838f0 fix: workflow fix try (#502) 2024-10-27 21:57:07 +05:30
aayush262
2cf73be675 test 2024-10-19 00:30:23 +05:30
Ankit Grai
b594258d28 fix : You can't scroll to next chapter if the manga only has one page (verified by shivam dont spam me is anything goes wrong) 2024-10-19 00:21:09 +05:30
Ankit Grai
f9ce897197 fixed the local dev problem with the injekt dependency (#482) 2024-10-18 13:37:41 -05:00
rebel onion
68b6fd030f Update strings.xml 2024-10-05 13:35:31 -05:00
aayush262
f0536b3cad Fix: thumbnails 2024-08-29 00:16:29 +05:30
Toby Bridle
6a077fa48d fix: remove unneeded (& problematic) logs (#473) 2024-08-28 12:51:43 -05:00
Sadwhy
ebb61d94dd [skip ci] chore: Update Kitsu URL 2024-08-12 12:37:34 +05:30
aayush262
a7cef9323e fix: nothing 2024-07-21 01:21:27 +05:30
aayush262
a8f7ff2a19 fix(rpc): no images 2024-07-21 01:05:43 +05:30
ibo
0214e6611b fix(ALsettings): staffNameLanguage crash (thx <@977936340186443826>) (#455) 2024-07-09 08:01:53 +05:30
ibo
04b9b9e7ff feat(settings): fully fledged AniList settings (#453) 2024-07-08 23:43:30 +05:30
ibo
7366aa1bf2 [skip ci] feat: copy username and better profile dropdown menu (#429) 2024-07-08 18:26:34 +05:30
ibo
09c5d9ce91 [skip ci] feat(github): create releases on forks (#449) 2024-07-08 18:23:08 +05:30
aayush262
79742f415b feat: something 2024-06-30 22:29:30 +05:30
rebelonion
b09f26ed34 fix: some markdown fixes 2024-06-29 10:59:18 -05:00
rebelonion
6eb654bf51 fix start keyboard expanded smh 2024-06-26 15:02:12 -05:00
rebelonion
b109d50d89 fix: markdown options hidden behind keyboard 2024-06-26 14:55:37 -05:00
aayush262
f62bdf9360 feat: lil faster home screen? idk tbh 2024-06-27 00:23:31 +05:30
aayush262
46d16be835 feat: delete comment and subscription notification 2024-06-26 19:27:26 +05:30
rebelonion
c22fd6b66d Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-06-26 07:13:50 -05:00
ibo
ae95b61298 feat: sort subscriptions in groups (#443) 2024-06-25 10:31:04 +05:30
ibo
2180086573 feat(discord): dynamic embed color 2024-06-25 09:59:10 +05:30
ibo
665b558b1f feat(discord,telegram): dev info and thumbnail ranking logic + hyperlink on tg 2024-06-24 22:29:32 +05:30
ibo
37ba9341cc feat(discord): added hash links + fix trailing 2024-06-22 09:46:19 +05:30
Sadwhy
feb765448b [skip ci] Fix: Removed extension lower limit 2024-06-22 09:43:34 +05:30
ibo
a8ccf8d246 [skip ci] fix: story buttons hitbox 2024-06-22 00:00:34 +05:30
ibo
2f06ac6071 [skip ci] feat: showOnlyLibrary button in Calendar 2024-06-21 23:59:38 +05:30
aayush262
ed24e64b78 feat(discord): no more here ping pt2 2024-06-21 23:51:24 +05:30
aayush262
43fc9c17f5 feat(discord): no more here ping 2024-06-21 23:43:04 +05:30
ibo
83e7e4591d feat(discord): send embeds through webhook for pretesters 2024-06-21 23:17:57 +05:30
Sadwhy
563a96cf98 [skip ci] Updated faq + Force LTR layout (#435)
* Updated faq + Force LTR layout

* Ibo merge issue
2024-06-20 21:01:51 +05:30
aayush262
124f4eb261 Merge remote-tracking branch 'origin/dev' into dev 2024-06-16 11:04:12 +05:30
aayush262
0052eba828 feat: remove predefined repo links 2024-06-16 11:01:32 +05:30
ibo
eda213a765 [skip ci] feat: better empty source dialog + bruh (#428)
* feat: better empty source dialog + bruh

* fix: itemMedia bindings
2024-06-16 10:41:11 +05:30
ibo
899af3ee1a feat: added clearhistory button (#416) 2024-06-14 16:37:38 +05:30
aayush262
124c8f5ed7 feat: optimize Alert Dialogs 2024-06-13 19:15:55 +05:30
aayush262
1670383619 feat(home): hive private media 2024-06-13 17:53:40 +05:30
aayush262
903423b842 fix: random things 2024-06-11 20:56:59 +05:30
aayush262
3ae59b8d22 fix(notifications): extra padding 2024-06-07 00:29:13 +05:30
aayush262
d488d11573 fix(notifications): extra padding 2024-06-06 21:03:43 +05:30
aayush262
6f685a4388 feat: private message 2024-06-02 02:11:56 +05:30
rebelonion
b644ba1866 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-06-01 08:27:41 -05:00
Sadwhy
74cab22eca Missing imports (#404) 2024-06-01 08:27:33 -05:00
ibo
ce7ae28e1e feat: revamping text activities (#406)
* feat(storyReply): redesigned components

* feat(storyReply): fixed all markdowns
2024-06-01 08:27:11 -05:00
rebelonion
fdc1b31c44 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-06-01 08:25:12 -05:00
rebelonion
1b4c8704ea fix: buffer manga image names 2024-06-01 08:25:08 -05:00
aayush262
5473ac8238 fix: some missing thumbnails 2024-05-30 23:11:34 +05:30
aayush262
e52ea2628a fix: fillers missing 2024-05-29 23:26:12 +05:30
aayush262
0f8218482a Merge remote-tracking branch 'origin/dev' into dev 2024-05-29 22:01:37 +05:30
aayush262
8822ef6805 feat: more thumbnails, descriptions (thanks to @yupcm) 2024-05-29 21:59:57 +05:30
aayush262
6ce41b8fbb fix: half cut text story (thanks to shivam) 2024-05-29 01:59:13 +05:30
aayush262
11655bd38d fix: hmm 2024-05-28 01:13:32 +05:30
aayush262
ea75197120 feat: Delete,edit activity 2024-05-28 01:04:07 +05:30
aayush262
6878d12b5c feat: more thumbnails 2024-05-27 23:09:19 +05:30
rebelonion
5800dcf3e7 fix: some download optimizations 2024-05-27 07:08:47 -05:00
rebelonion
b30047804a feat: setting to hide red dot 2024-05-27 05:58:51 -05:00
rebelonion
0b32636c1b Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-05-27 05:11:48 -05:00
rebelonion
43e560a893 fix: Synchronized 2024-05-27 05:11:39 -05:00
aayush262
b256f02f14 chore: optimized feed activity 2024-05-26 23:49:19 +05:30
aayush262
46c17dced1 fix: small bug fix 2024-05-26 23:48:40 +05:30
aayush262
72fe910c59 fix: story scrolling issue 2024-05-26 21:43:40 +05:30
aayush262
fb65cb601e feat: remove other tabs when opening page from notification 2024-05-26 21:43:32 +05:30
aayush262
5a78d68f67 feat: remove other tabs when opening page from notification 2024-05-26 21:43:25 +05:30
aayush262
f205463a51 feat: refresh reply dialog after new message 2024-05-26 21:43:19 +05:30
aayush262
2de8ffd367 feat: optimize activity page 2024-05-26 21:43:12 +05:30
aayush262
f3f0daf7e7 Merge remote-tracking branch 'origin/dev' into dev 2024-05-26 00:41:28 +05:30
aayush262
2b4c9bf7a9 feat: notifications page rework 2024-05-26 00:40:46 +05:30
Sadwhy
21e25fe7a7 Fix commit messages in discord and telegram upload (#403)
It will fix it in its next build.
2024-05-25 22:13:17 +05:30
rebelonion
7b36cd0d29 fix: disallow screenshots in crash activity 2024-05-25 10:11:24 -05:00
rebelonion
ce488ea536 feat: biometric | etc 2024-05-25 10:08:11 -05:00
rebelonion
7717974b9e fix: what does the fix say? 🦊 2024-05-25 08:37:16 -05:00
rebelonion
37949c7e8e fix: move some stuffs around 2024-05-24 14:51:25 -05:00
rebelonion
e7a60e07d8 fix: destroyed activity crash on slower phones 2024-05-24 14:03:33 -05:00
rebelonion
7bce053202 fix: notification setting formatting 2024-05-24 14:02:36 -05:00
rebelonion
a5304477c7 fix: move try inside withContext 2024-05-24 13:34:39 -05:00
rebelonion
945018653e fix: some network stuff 2024-05-24 13:28:26 -05:00
rebelonion
5e38b00c1f fix: separate status query 2024-05-24 12:39:03 -05:00
rebel onion
dec990c24c Update README.md 2024-05-23 13:47:28 -05:00
rebel onion
e7cf0f7b03 Merge pull request #397 from rebelonion/dev
Dev
2024-05-23 13:24:09 -05:00
rebel onion
560eef491f Merge branch 'main' into dev 2024-05-23 13:23:36 -05:00
rebelonion
94ffc2595c feat: open profile links 2024-05-23 13:20:43 -05:00
rebelonion
3d187a01ec fix: smol fixes 2024-05-23 12:48:34 -05:00
aayush262
773b7f5dd0 feat: remove 18+ media and anilist adult is off 2024-05-23 22:00:47 +05:30
rebelonion
d06c980a57 fix: you're welcome <@1193590680308699266> 2024-05-23 02:33:43 -05:00
rebelonion
e5ec6a6526 fix: various small fixes 2024-05-22 11:26:48 -05:00
rebelonion
4ccf6fa1c8 fix: rearrange some stuff 2024-05-22 05:35:26 -05:00
rebelonion
143eed8cb2 feat: add calculator to app 2024-05-22 05:08:43 -05:00
aayush262
0008da200a Merge remote-tracking branch 'origin/dev' into dev 2024-05-22 00:21:11 +05:30
aayush262
48ccb2c581 feat : qol things 2024-05-22 00:20:59 +05:30
aayush262
039e3d63fe feat: banner and cover for airing notifications 2024-05-22 00:20:40 +05:30
rebelonion
fd42533b40 fix: attribution 2024-05-21 12:20:31 -05:00
rebelonion
66805bdf05 fix: list view crash 2024-05-21 11:28:46 -05:00
rebel onion
b3ed8acd5b Update LICENSE.md 2024-05-21 08:33:08 -05:00
rebelonion
fe1a7af7ac feat: test ms response 2024-05-20 11:49:57 -05:00
rebelonion
10df1986e8 feat: video fixing options 2024-05-20 11:15:11 -05:00
rebelonion
c2a10c233d fix: null safe cast 2024-05-20 11:14:00 -05:00
rebelonion
1c736640b2 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-05-20 08:49:38 -05:00
Itsmechinmoy
212bce09c8 Invisible Status Discord Rpc (#394)
* Update SettingsAccountActivity.kt

* Create discord_status_invisible.xml

* Update discord_status_invisible.xml

* Update discord_status_invisible.xml

* Update SettingsAccountActivity.kt

* Update SettingsAccountActivity.kt
2024-05-20 08:42:32 -05:00
rebelonion
ea045c185d fix: keep ui on the main thread 2024-05-20 08:34:46 -05:00
rebelonion
41ed5a66da Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-05-20 07:29:04 -05:00
rebelonion
2c3eb82e4b fix: show subscription name 2024-05-20 07:29:02 -05:00
rebelonion
1dd3bddeb9 fix: show only trusted 2024-05-20 07:28:35 -05:00
aayush262
d12ddc9c0d feat: reviews in info page 2024-05-20 14:45:32 +05:30
rebelonion
91f728150c Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-05-19 14:18:16 -05:00
rebelonion
ab360b3a75 feat: extension testing 2024-05-19 14:17:58 -05:00
aayush262
114be6fe5a fix: review rework 2024-05-19 14:47:43 +05:30
aayush262
f53d27bd53 Merge remote-tracking branch 'origin/dev' into dev 2024-05-18 23:50:51 +05:30
aayush262
bfa847130e fix: idr 2024-05-18 23:49:43 +05:30
rebelonion
949bcc418a fix: some error checking 2024-05-18 11:43:30 -05:00
rebelonion
df2867c7db feat: subscriptions in notifications 2024-05-17 10:11:42 -05:00
rebelonion
6c1176a182 feat: view subscriptions in settings 2024-05-17 08:57:59 -05:00
rebelonion
f1d16ba16a feat: support for multiple audio/subtitle downloads 2024-05-16 14:51:35 -05:00
rebelonion
fd8dd26435 fix: broken neutral button 2024-05-16 10:13:46 -05:00
rebelonion
ac531cd3e8 fix: string sanitizer 2024-05-16 10:05:47 -05:00
rebelonion
2c521b4ac6 feat: anilist post confirmation 2024-05-16 10:05:31 -05:00
aayush262
a3b1d3db57 fix: reply text background color 2024-05-14 00:33:14 +05:30
rebelonion
b0b51c4347 feat: reply count in story 2024-05-13 06:38:12 -05:00
rebelonion
001c384d11 feat: replies in stories 2024-05-12 21:44:10 -05:00
rebelonion
d355cc561e feat: replying to activities 2024-05-12 08:46:51 -05:00
rebelonion
6e3a3bb6f8 fix: blank text check 2024-05-12 08:19:12 -05:00
rebelonion
fa659c7da0 feat: creating activities in app 2024-05-12 06:01:51 -05:00
rebelonion
a0fabd3ca6 feat: reviews 2024-05-12 03:37:41 -05:00
rebelonion
831b99ae40 feat: message when downloader not installed 2024-05-11 08:27:02 -05:00
rebelonion
13e2e37225 fix: searchHistoryAdapter call before initialized 2024-05-11 00:11:13 -05:00
rebelonion
988e4def64 feat: list searching 2024-05-10 22:02:30 -05:00
rebelonion
e1a865c973 feat: notification filtering 2024-05-09 06:02:17 -05:00
rebelonion
be97229618 chore: AGP 2024-05-09 04:51:39 -05:00
rebelonion
b3d3913d56 feat: log last loaded activity 2024-05-08 23:13:52 -05:00
rebelonion
e7b6ba80c3 fix: broken default home screen 2024-05-08 23:04:17 -05:00
rebelonion
4ac53da4b8 fix: addon race condition 2024-05-08 22:36:28 -05:00
rebelonion
7e504df55a fix: comment api change 2024-05-08 20:05:12 -05:00
rebelonion
9d13920f63 fix: audio track names scrambled 2024-05-07 06:50:00 -05:00
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
rebel onion
b5b7dac247 Update beta.yml 2024-04-04 04:02:11 -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
rebel onion
59784de727 Merge pull request #269 from rebelonion/dev
Dev
2024-03-20 14:54:01 -05:00
aayush262
42f23e4345 dix: many small changes 2024-03-21 01:18:36 +05:30
rebelonion
adb304f138 fix: manga/anime page noti icon updating 2024-03-20 14:29:52 -05:00
rebelonion
3bbf9efe63 dix: comment scroll deadspace 2024-03-20 13:54:25 -05:00
rebelonion
b454a2e3d9 fix: comment notification at bottom 2024-03-20 13:08:46 -05:00
rebelonion
23e6323f92 fix: comment reply dead scrolling space 2024-03-20 04:29:14 -05:00
rebelonion
b0dbd7a348 fix: add a check for minimum poll time 2024-03-20 04:10:12 -05:00
rebel onion
0f9bf3c5b1 chore: Update stable.md 2024-03-20 01:33:54 -05:00
rebel onion
4035aee1f9 Merge pull request #264 from rebelonion/dev
Dev
2024-03-20 01:32:15 -05:00
rebelonion
f707f8cc33 fix: activity color tweaks 2024-03-20 01:23:48 -05:00
aayush262
fa7126d80d fix: better gradiant color 2024-03-20 11:13:09 +05:30
aayush262
7d5f69888a fix(profile): double usernames 2024-03-20 10:57:50 +05:30
rebelonion
51841cf05f feat: error message snack -> toast 2024-03-20 00:23:42 -05:00
rebelonion
6d2c01ff2b chore: version bump 2024-03-20 00:19:39 -05:00
rebelonion
0bd4755814 fix: remove snack spam 2024-03-20 00:17:27 -05:00
rebelonion
927ba5ac86 fix: AAChartCore library not found tempfix 2024-03-19 20:18:39 -05:00
rebelonion
808d4e6bf5 feat: move subscriptions to new notification method 2024-03-19 19:30:12 -05:00
rebelonion
a39db5ea93 fix: cleaner spoiler text in comments 2024-03-19 17:09:34 -05:00
rebelonion
ca2409ef91 fix: fav workaround for broken anilist api 2024-03-19 16:50:52 -05:00
rebelonion
7b1f1a1357 fix: more robust notification loading 2024-03-19 16:02:52 -05:00
rebelonion
9471683501 feat: AlarmManager option for notifications 2024-03-18 23:51:00 -05:00
rebelonion
deeefb8e35 fix: don't show 500 error code 2024-03-18 17:55:12 -05:00
rebelonion
c777888fdb Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-03-18 17:45:36 -05:00
rebelonion
ce50627989 fix: add missing ui SharedPreference 2024-03-18 17:45:17 -05:00
TwistedUmbrellaX
9f84845ada fix: login and navigation < API 23 (#258)
* fix: compensate for old nav (48dp)

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

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

* fix: use the convenience method

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

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

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

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

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

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

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

* Resolved Merge Conflicts and Removed Unnecessary Imports

* Resolved Merge Conflicts

* Resolved Merge Conflicts

* Resolved Merge Conflicts

* Resolved problems

* Fixed a little mistake

* Made Requested Changes

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

* fix: clean up the overlapping decor

* feat: match theme color with navbar

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

* feat: hide navigation bar until swiped

* fix: limit announcements to official

* feat: keep navigation visible for back

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

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

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

* feet: Attached strings

* feet(fix)

---------

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

* feat: scanlation mass tick

* fix: togglebutton on scanlation scrollview

* fix: fix ImageButton padding + overlay

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

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

added oriax in picker + applier

* Update colors.xml

Added new color seed

* Update themes.xml

Added Oriax Lightmode

* Update themes.xml

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

* Changed Launch Animation

* Update strings.xml

---------

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

fix: multiline beta.yml

Update beta.yml

Update beta.yml

Update beta.yml

Update beta.yml

fix: beta.yml newline

Update beta.yml

fix: finding last sha

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

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

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

9
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,9 @@
blank_issues_enabled: false
contact_links:
- name: 🧑‍💻 Dantotsu Help on Discord
url: https://discord.com/invite/4HPZ5nAWwM
about: Get support, ask questions, and join the community discussions.
- name: 📱 Dantotsu Help on Telegram
url: https://t.me/dantotsuapp
about: Connect with the community, ask questions, and get help directly on Telegram.

35
.github/ISSUE_TEMPLATE/question.yml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: ❓ Question
description: Submit a question or query related to Dantotsu
labels: [question]
body:
- type: textarea
id: question-details
attributes:
label: Question Details
description: Provide a detailed explanation of your question or query.
placeholder: |
Example:
"How do I customize the settings in Dartotsu to optimize performance?"
validations:
required: true
- type: input
id: related-features
attributes:
label: Related Features (if applicable)
description: Mention any specific feature or section of Dantotsu related to your question.
placeholder: |
Example: "Settings > Performance"
- type: checkboxes
id: submission-checklist
attributes:
label: Submission Checklist
description: Review the following items before submitting your question.
options:
- label: I have searched existing issues to see if this question has already been answered.
required: true
- label: I have provided a clear and concise question title.
required: true
- label: I have provided all relevant details to understand my question fully.
required: true

101
.github/ISSUE_TEMPLATE/report_issue.yml vendored Normal file
View File

@@ -0,0 +1,101 @@
name: 🐛 Issue Report
description: Report a bug or problem in Dantotsu
labels: [bug]
body:
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to Reproduce
description: Outline the steps needed to trigger the issue.
placeholder: |
Example:
1. Navigate to the home screen.
2. Click on "Start."
3. Observe the error message.
validations:
required: true
- type: textarea
id: expected-outcome
attributes:
label: Expected Outcome
description: Describe what you expected to happen.
placeholder: |
Example:
"The application should have successfully loaded the dashboard..."
validations:
required: true
- type: textarea
id: actual-outcome
attributes:
label: Actual Outcome
description: Detail what actually occurred when following the steps.
placeholder: |
Example:
"The app crashed and displayed an error message instead..."
validations:
required: true
- type: textarea
id: error-logs
attributes:
label: Error Logs (if applicable)
description: |
If the issue involves crashes, please attach relevant logs. Access them via **Settings → About → Log to file → Share**.
placeholder: |
Paste the logs here or upload as an attachment.
- type: input
id: dartotsu-version
attributes:
label: Dartotsu Version
description: Specify the version of Dartotsu in which the issue occurred.
placeholder: |
Example: "1.2.3"
validations:
required: true
- type: input
id: os-version
attributes:
label: Operating System Version
description: Mention the OS version you are using.
placeholder: |
Example: "Android 12"
validations:
required: true
- type: input
id: device-info
attributes:
label: Device Information
description: Provide your device name and model.
placeholder: |
Example: "Samsung Galaxy S21"
validations:
required: true
- type: textarea
id: additional-information
attributes:
label: Additional Information
placeholder: |
Include any other relevant details or attachments that may help diagnose the issue.
- type: checkboxes
id: submission-checklist
attributes:
label: Submission Checklist
description: Ensure you've reviewed these items before submitting your report.
options:
- label: I have searched existing issues to confirm this is not a duplicate.
required: true
- label: I have provided a clear and descriptive title.
required: true
- label: I am using the **[latest](https://github.com/rebelonion/Dantotsu/latest)** version of Dantotsu. If not, I have provided a reason for not updating.
required: true
- label: I have updated all relevant extensions or dependencies.
required: true
- label: I have filled out all the requested information accurately.
required: true

View File

@@ -0,0 +1,54 @@
name: 🚀 Feature Request
description: Propose a new feature to enhance Dantotsu
labels: [enhancement]
body:
- type: textarea
id: feature-summary
attributes:
label: Feature Summary
description: Provide a concise summary of the feature you'd like to see.
placeholder: |
Example:
"Add support for dark mode..."
validations:
required: true
- type: textarea
id: detailed-description
attributes:
label: Detailed Description
description: Elaborate on how this feature should function and its potential impact.
placeholder: |
Example:
"The dark mode should automatically activate based on system settings..."
value: |
### Current Behavior
- Describe the current functionality or lack thereof.
### Proposed Solution
- Detail how the feature should work and any potential benefits.
### Considerations
- Mention any potential challenges or alternatives.
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Provide any other information, relevant screenshots, or references.
placeholder: "Include links to relevant resources, external tools, or related issues."
- type: checkboxes
id: checklist
attributes:
label: Submission Checklist
description: Ensure you've completed these before submitting.
options:
- label: I have searched the existing issues and confirm that this feature has not been requested before.
required: true
- label: I have provided a clear and descriptive title.
required: true
- label: I am using the **[latest](https://github.com/rebelonion/Dantotsu/releases/latest)** version of Dantotsu. If not, I have provided a reason for using an older version.
required: true
- label: I understand that not all feature requests will be accepted, and if declined, I won't resubmit the same request.
required: true

View File

@@ -1,67 +1,366 @@
name: Build APK and Notify Discord
on:
push:
branches:
- dev
branches-ignore:
- main
- l10n_dev_crowdin
- custom-download-location
paths-ignore:
- '**/README.md'
tags:
- "v*.*.*"
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
env:
CI: true
SKIP_BUILD: false
steps:
- name: Checkout repo
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download last SHA artifact
uses: dawidd6/action-download-artifact@v6
with:
workflow: beta.yml
name: last-sha
path: .
continue-on-error: true
- name: Get Commits Since Last Run
run: |
if [ -f last_sha.txt ]; then
LAST_SHA=$(cat last_sha.txt)
else
# Fallback to first commit if no previous SHA available
LAST_SHA=$(git rev-list --max-parents=0 HEAD)
fi
echo "Commits since $LAST_SHA:"
# Accumulate commit logs in a shell variable
COMMIT_LOGS=$(git log $LAST_SHA..HEAD --pretty=format:"● %s ~%an [֍](https://github.com/${{ github.repository }}/commit/%H)" --max-count=10)
# Replace commit messages with pull request links
COMMIT_LOGS=$(echo "$COMMIT_LOGS" | sed -E 's/#([0-9]+)/[#\1](https:\/\/github.com\/rebelonion\/Dantotsu\/pull\/\1)/g')
# URL-encode the newline characters for GitHub Actions
COMMIT_LOGS="${COMMIT_LOGS//'%'/'%25'}"
COMMIT_LOGS="${COMMIT_LOGS//$'\n'/'%0A'}"
COMMIT_LOGS="${COMMIT_LOGS//$'\r'/'%0D'}"
# Append the encoded commit logs to the COMMIT_LOG environment variable
echo "COMMIT_LOG=${COMMIT_LOGS}" >> $GITHUB_ENV
# Debugging: Print the variable to check its content
echo "$COMMIT_LOGS"
echo "$COMMIT_LOGS" > commit_log.txt
# Extract branch name from github.ref
BRANCH=${{ github.ref }}
BRANCH=${BRANCH#refs/heads/}
echo "BRANCH=${BRANCH}" >> $GITHUB_ENV
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
- name: Set variables
run: |
VER=$(grep -E -o "versionName \".*\"" app/build.gradle | sed -e 's/versionName //g' | tr -d '"')
SHA=${{ github.sha }}
VERSION="$VER.${SHA:0:7}"
VERSION="$VER+${SHA:0:7}"
echo "Version $VERSION"
echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: List files in the directory
run: ls -l
- name: Setup JDK 17
uses: actions/setup-java@v3
if: ${{ env.SKIP_BUILD != 'true' }}
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 17
cache: gradle
- name: Decode Keystore File
if: ${{ github.repository == 'rebelonion/Dantotsu' }}
run: echo "${{ secrets.KEYSTORE_FILE }}" | base64 -d > $GITHUB_WORKSPACE/key.keystore
- name: List files in the directory
run: ls -l
- name: Make gradlew executable
if: ${{ env.SKIP_BUILD != 'true' }}
run: chmod +x ./gradlew
- name: Build with Gradle
run: ./gradlew assembleDebug -Pandroid.injected.signing.store.file=$GITHUB_WORKSPACE/key.keystore -Pandroid.injected.signing.store.password=${{ secrets.KEYSTORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.KEY_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.KEY_PASSWORD }}
if: ${{ env.SKIP_BUILD != 'true' }}
run: |
if [ "${{ github.repository }}" == "rebelonion/Dantotsu" ]; then
./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 }};
else
./gradlew assembleGoogleAlpha;
fi
- name: Upload a Build Artifact
uses: actions/upload-artifact@v3.0.0
if: ${{ env.SKIP_BUILD != 'true' }}
uses: actions/upload-artifact@v4
with:
name: Dantotsu
path: "app/build/outputs/apk/debug/app-debug.apk"
- name: Upload APK to Discord
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: |
contentbody=$( jq -Rsa . <<< "${{ github.event.head_commit.message }}" )
curl -F "payload_json={\"content\":\" Debug-Build: <@719439449423085569> **${{ env.VERSION }}**\n\n${contentbody:1:-1}\"}" -F "dantotsu_debug=@app/build/outputs/apk/debug/app-debug.apk" ${{ secrets.DISCORD_WEBHOOK }}
# Prepare Discord embed
fetch_user_details() {
local login=$1
user_details=$(curl -s "https://api.github.com/users/$login")
name=$(echo "$user_details" | jq -r '.name // .login')
login=$(echo "$user_details" | jq -r '.login')
avatar_url=$(echo "$user_details" | jq -r '.avatar_url')
echo "$name|$login|$avatar_url"
}
# Additional information for the goats
declare -A additional_info
additional_info["ibo"]="\n Discord: <@951737931159187457>\n AniList: [takarealist112](<https://anilist.co/user/5790266/>)"
additional_info["aayush262"]="\n Discord: <@918825160654598224>\n AniList: [aayush262](<https://anilist.co/user/5144645/>)"
additional_info["rebel onion"]="\n Discord: <@714249925248024617>\n AniList: [rebelonion](<https://anilist.co/user/6077251/>)\n PornHub: [rebelonion](<https://www.cornhub.com/model/rebelonion>)"
additional_info["Ankit Grai"]="\n Discord: <@1125628254330560623>\n AniList: [bheshnarayan](<https://anilist.co/user/6417303/>)"
# Decimal color codes for contributors
declare -A contributor_colors
default_color="#bf2cc8"
contributor_colors["ibo"]="#ff9b46"
contributor_colors["aayush262"]="#5d689d"
contributor_colors["Sadwhy"]="#ff7e95"
contributor_colors["grayankit"]="#c51aa1"
contributor_colors["rebelonion"]="#d4e5ed"
- name: Delete Old Pre-Releases
id: delete-pre-releases
uses: sgpublic/delete-release-action@master
with:
pre-release-drop: true
pre-release-keep-count: 3
pre-release-drop-tag: true
hex_to_decimal() { printf '%d' "0x${1#"#"}"; }
# Count recent commits and create an associative array Okay
declare -A recent_commit_counts
echo "Debug: Processing COMMIT_LOG:"
echo "$COMMIT_LOG"
while read -r count name; do
recent_commit_counts["$name"]=$count
echo "Debug: Commit count for $name: $count"
done < <(echo "$COMMIT_LOG" | sed 's/%0A/\n/g' | grep -oP '(?<=~)[^[]*' | sort | uniq -c | sort -rn)
echo "Debug: Fetching contributors from GitHub"
# Fetch contributors from GitHub
contributors=$(curl -s "https://api.github.com/repos/${{ github.repository }}/contributors")
echo "Debug: Contributors response:"
echo "$contributors"
# Create a sorted list of contributors based on recent commit counts
sorted_contributors=$(for login in $(echo "$contributors" | jq -r '.[].login'); do
user_info=$(fetch_user_details "$login")
name=$(echo "$user_info" | cut -d'|' -f1)
count=${recent_commit_counts["$name"]:-0}
echo "$count|$login"
done | sort -rn | cut -d'|' -f2)
# Initialize needed variables
developers=""
committers_count=0
max_commits=0
top_contributor=""
top_contributor_count=0
top_contributor_avatar=""
embed_color=$(hex_to_decimal "$default_color")
# Process contributors in the new order
while read -r login; do
user_info=$(fetch_user_details "$login")
name=$(echo "$user_info" | cut -d'|' -f1)
login=$(echo "$user_info" | cut -d'|' -f2)
avatar_url=$(echo "$user_info" | cut -d'|' -f3)
# Only process if they have recent commits
commit_count=${recent_commit_counts["$name"]:-0}
if [ $commit_count -gt 0 ]; then
# Update top contributor information
if [ $commit_count -gt $max_commits ]; then
max_commits=$commit_count
top_contributors=("$login")
top_contributor_count=1
top_contributor_avatar="$avatar_url"
embed_color=$(hex_to_decimal "${contributor_colors[$name]:-$default_color}")
elif [ $commit_count -eq $max_commits ]; then
top_contributors+=("$login")
top_contributor_count=$((top_contributor_count + 1))
embed_color=$(hex_to_decimal "$default_color")
fi
echo "Debug top contributors:"
echo "$top_contributors"
# Get commit count for this contributor on the dev branch
branch_commit_count=$(git log --author="$login" --author="$name" --oneline | awk '!seen[$0]++' | wc -l)
# Debug: Print recent_commit_counts
echo "Debug: recent_commit_counts contents:"
for key in "${!recent_commit_counts[@]}"; do
echo "$key: ${recent_commit_counts[$key]}"
done
extra_info="${additional_info[$name]}"
if [ -n "$extra_info" ]; then
extra_info=$(echo "$extra_info" | sed 's/\\n/\n- /g')
fi
# Construct the developer entry
developer_entry="◗ **${name}** ${extra_info}
- Github: [${login}](https://github.com/${login})
- Commits: ${branch_commit_count}"
# Add the entry to developers, with a newline if it's not the first entry
if [ -n "$developers" ]; then
developers="${developers}
${developer_entry}"
else
developers="${developer_entry}"
fi
committers_count=$((committers_count + 1))
fi
done <<< "$sorted_contributors"
# Set the thumbnail URL and color based on top contributor(s)
if [ $top_contributor_count -eq 1 ]; then
thumbnail_url="$top_contributor_avatar"
else
thumbnail_url="https://i.imgur.com/5o3Y9Jb.gif"
embed_color=$(hex_to_decimal "$default_color")
fi
# Truncate field values
max_length=1000
commit_messages=$(echo "$COMMIT_LOG" | sed 's/%0A/\n/g; s/^/\n/')
if [ ${#developers} -gt $max_length ]; then
developers="${developers:0:$max_length}... (truncated)"
fi
if [ ${#commit_messages} -gt $max_length ]; then
commit_messages="${commit_messages:0:$max_length}... (truncated)"
fi
# Construct Discord payload
discord_data=$(jq -nc \
--arg field_value "$commit_messages" \
--arg author_value "$developers" \
--arg footer_text "Version $VERSION" \
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" \
--arg thumbnail_url "$thumbnail_url" \
--arg embed_color "$embed_color" \
'{
"content": "<@&1225347048321191996>",
"embeds": [
{
"title": "New Alpha-Build dropped",
"color": $embed_color,
"fields": [
{
"name": "Commits:",
"value": $field_value,
"inline": true
},
{
"name": "Developers:",
"value": $author_value,
"inline": false
}
],
"footer": {
"text": $footer_text
},
"timestamp": $timestamp,
"thumbnail": {
"url": $thumbnail_url
}
}
],
"attachments": []
}')
echo "Debug: Final Discord payload:"
echo "$discord_data"
# Send Discord message
curl -H "Content-Type: application/json" \
-d "$discord_data" \
${{ secrets.DISCORD_WEBHOOK }}
echo "You have only send an embed to discord due to SKIP_BUILD being set to true"
# Upload APK to Discord
if [ "$SKIP_BUILD" != "true" ]; then
curl -F "payload_json=${contentbody}" \
-F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" \
${{ secrets.DISCORD_WEBHOOK }}
else
echo "Skipping APK upload to Discord due to SKIP_BUILD being set to true"
fi
# Format commit messages for Telegram
telegram_commit_messages=$(echo "$COMMIT_LOG" | sed 's/%0A/\n/g' | while read -r line; do
message=$(echo "$line" | sed -E 's/● (.*) ~(.*) \[֍\]\((.*)\)/● \1 ~\2 <a href="\3">֍<\/a>/')
message=$(echo "$message" | sed -E 's/\[#([0-9]+)\]\((https:\/\/github\.com\/[^)]+)\)/<a href="\2">#\1<\/a>/g')
echo "$message"
done)
telegram_commit_messages="<blockquote>${telegram_commit_messages}</blockquote>"
# Configuring dev info
echo "$developers" > dev_info.txt
echo "$developers"
# making the file executable
chmod +x workflowscripts/tel_parser.sed
./workflowscripts/tel_parser.sed dev_info.txt >> output.txt
dev_info_tel=$(< output.txt)
telegram_dev_info="<blockquote>${dev_info_tel}</blockquote>"
echo "$telegram_dev_info"
# Upload APK to Telegram
if [ "$SKIP_BUILD" != "true" ]; then
APK_PATH="app/build/outputs/apk/google/alpha/app-google-alpha.apk"
response=$(curl -sS -f -X POST \
"https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument" \
-F "chat_id=-1002117798698" \
-F "message_thread_id=7044" \
-F "document=@$APK_PATH" \
-F "caption=New Alpha-Build dropped 🔥
Commits:
${telegram_commit_messages}
Dev:
${telegram_dev_info}
version: ${VERSION}" \
-F "parse_mode=HTML")
else
echo "skipping because skip build set to true"
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COMMIT_LOG: ${{ env.COMMIT_LOG }}
VERSION: ${{ env.VERSION }}
- name: Upload Current SHA as Artifact
uses: actions/upload-artifact@v4
with:
name: last-sha
path: last_sha.txt
- name: Upload Commit log as Artifact
uses: actions/upload-artifact@v4
with:
name: commit-log
path: commit_log.txt

84
.github/workflows/bug_greetings.yml vendored Normal file
View File

@@ -0,0 +1,84 @@
name: Bug Report Greeting
on:
issues:
types: [opened]
jobs:
greeting:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Checkout Repository
uses: actions/checkout@v2
- name: Check if the issue is labeled as a Bug Report
id: check_bug_label
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ISSUE_NUMBER=$(jq -r '.issue.number' "$GITHUB_EVENT_PATH")
LABELS=$(gh issue view $ISSUE_NUMBER --json labels --jq '.labels[].name')
if echo "$LABELS" | grep -q 'bug'; then
echo "This issue is labeled as a bug report. Checking if the issue creator is the repository owner."
echo "skip_label_check=false" >> $GITHUB_ENV
else
echo "This issue is not labeled as a bug report. Skipping greeting message."
echo "skip_label_check=true" >> $GITHUB_ENV
fi
- name: Check if the issue creator is the repo owner
if: env.skip_label_check == 'false'
id: check_owner
run: |
ISSUE_AUTHOR=$(jq -r '.issue.user.login' "$GITHUB_EVENT_PATH")
REPO_OWNER=$(jq -r '.repository.owner.login' "$GITHUB_EVENT_PATH")
if [ "$ISSUE_AUTHOR" = "$REPO_OWNER" ]; then
echo "The issue creator is the repository owner. Skipping greeting message."
echo "skip=true" >> $GITHUB_ENV
else
echo "The issue creator is not the repository owner. Checking for previous bug reports..."
echo "skip=false" >> $GITHUB_ENV
fi
- name: Check if the user has submitted a bug report before
if: env.skip == 'false'
id: check_first_bug_report
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ISSUE_AUTHOR=$(jq -r '.issue.user.login' "$GITHUB_EVENT_PATH")
ISSUE_NUMBER=$(jq -r '.issue.number' "$GITHUB_EVENT_PATH")
# Get all issues (both open and closed) by the author except the current one
PREVIOUS_REPORTS=$(gh issue list --author "$ISSUE_AUTHOR" --label "Bug" --state all --json number --jq '. | map(select(.number != '$ISSUE_NUMBER')) | length')
echo "User $ISSUE_AUTHOR has submitted $PREVIOUS_REPORTS bug report(s) previously"
if [ "$PREVIOUS_REPORTS" -eq 0 ]; then
echo "This is the user's first bug report. Sending greeting message."
echo "skip_first_report=false" >> $GITHUB_ENV
else
echo "User has previous bug reports. Skipping greeting message."
echo "skip_first_report=true" >> $GITHUB_ENV
fi
- name: Send Greeting Message
if: env.skip_label_check == 'false' && env.skip != 'true' && env.skip_first_report != 'true'
uses: actions/github-script@v6
with:
script: |
const issueNumber = context.payload.issue.number;
const message = `
**🛠️ Thank you for reporting a bug!**
Your issue has been successfully submitted and is now awaiting review. We appreciate your help in making Dantotsu better.
**🔍 What Happens Next**
- Our team will investigate the issue and provide updates as soon as possible.
- You may be asked for additional details or clarification if needed.
- Once resolved, we'll notify you of the fix or provide a workaround.
**👥 Connect with Us**
- **[Discord](https://discord.com/invite/4HPZ5nAWwM)**: Engage with our community and ask questions.
- **[Telegram](https://t.me/dantotsuapp)**: Reach out for real-time discussions and updates.
We're working hard to resolve the issue and appreciate your patience!
`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: message
});

View File

@@ -0,0 +1,112 @@
name: Extension Issue Handling
on:
issues:
types: [opened, labeled]
jobs:
handle-extension-issues:
runs-on: ubuntu-latest
steps:
- name: Check Issue Content
id: check-issue
env:
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_BODY: ${{ github.event.issue.body }}
run: |
# Regex patterns for extension-related issues
EXTENSION_REGEX_PATTERNS=(
# Extension not working (more flexible match)
".*(\w+)\s*(extension)?\s*(not working|doesn't work|does not work|cant work|can't work).*"
# No extension available
".*(no|can't find|cannot find|missing).*extension.*"
# No repo or repositories available
".*(no|can't find|cannot find|missing).*repo(s)?\s*(available|found|accessible).*"
# Specific server/stream issues
".*(no streams|server).*(available|working).*"
# Variants of extension problems
".*{.*}.*not working.*"
".*{.*}.*extension.*(issue|problem).*"
)
# Convert to lowercase for case-insensitive matching
LOWER_TITLE=$(echo "$ISSUE_TITLE" | tr '[:upper:]' '[:lower:]')
LOWER_BODY=$(echo "$ISSUE_BODY" | tr '[:upper:]' '[:lower:]')
# Flag to track issue type
IS_EXTENSION_ISSUE=false
IS_NO_EXTENSION_ISSUE=false
# Check title and body against regex patterns
for pattern in "${EXTENSION_REGEX_PATTERNS[@]}"; do
if [[ "$LOWER_TITLE" =~ $pattern ]] || [[ "$LOWER_BODY" =~ $pattern ]]; then
IS_EXTENSION_ISSUE=true
# Special check for no extensions available
if [[ "$LOWER_TITLE" =~ "no extension" ]] || [[ "$LOWER_TITLE" =~ "can't find extension" ]]; then
IS_NO_EXTENSION_ISSUE=true
fi
break
fi
done
# Explicitly output boolean values
if [ "$IS_EXTENSION_ISSUE" = true ]; then
echo "is_extension_issue=true" >> $GITHUB_OUTPUT
else
echo "is_extension_issue=false" >> $GITHUB_OUTPUT
fi
if [ "$IS_NO_EXTENSION_ISSUE" = true ]; then
echo "is_no_extension_issue=true" >> $GITHUB_OUTPUT
else
echo "is_no_extension_issue=false" >> $GITHUB_OUTPUT
fi
- name: Comment and Close Extension Issue
if: steps.check-issue.outputs.is_extension_issue == 'true'
uses: actions/github-script@v6
with:
github-token: ${{secrets.GITHUB_TOKEN}}
script: |
const issueNumber = context.issue.number;
// Check if it's a "No Extension" issue
if (${{ steps.check-issue.outputs.is_no_extension_issue }}) {
// DMCA notice message
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: "# Automated Message\n" +
"On 13 June 2024, the official Aniyomi repository got a DMCA notice and had to remove all of their extensions. Because of this, we will not be providing anyone with any links or extensions to avoid legal problems.\n" +
"# How to add repos?\n" +
"Although we do not give or maintain any repositories, we support adding custom repository links to your Dantotsu. \n" +
"Go to `Profile > Settings > Extensions` then paste your anime or manga links there.\n" +
"# How to find repos?\n" +
"It's very easy. Search on Google. But remember that the URL must end with <u><b>index.min.json</b></u> or else it won't work.\n" +
"`TLDR: We will not give repo links.`"
});
} else {
// Standard extension issue message
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: `Dantotsu doesn't maintain extensions.
If the extension doesn't work we cannot help you.
Contact the owner of Respective Repo for extension-related problems`
});
}
// Close the issue
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
state: 'closed'
});

86
.github/workflows/feature_greetings.yml vendored Normal file
View File

@@ -0,0 +1,86 @@
name: Feature Request Greeting
on:
issues:
types: [opened]
jobs:
greeting:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Check if the issue is labeled as a Feature Request
id: check_feature_label
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ISSUE_NUMBER=$(jq -r '.issue.number' "$GITHUB_EVENT_PATH")
LABELS=$(gh issue view $ISSUE_NUMBER --json labels --jq '.labels[].name')
if echo "$LABELS" | grep -q 'enhancement'; then
echo "This issue is labeled as a feature request. Checking if the issue creator is the repository owner."
echo "skip_label_check=false" >> $GITHUB_ENV
else
echo "This issue is not labeled as a feature request. Skipping greeting message."
echo "skip_label_check=true" >> $GITHUB_ENV
fi
- name: Check if the user has submitted a feature request before
if: env.skip_label_check == 'false'
id: check_first_request
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ISSUE_AUTHOR=$(jq -r '.issue.user.login' "$GITHUB_EVENT_PATH")
REPO_OWNER=$(jq -r '.repository.owner.login' "$GITHUB_EVENT_PATH")
ISSUE_NUMBER=$(jq -r '.issue.number' "$GITHUB_EVENT_PATH")
if [ "$ISSUE_AUTHOR" = "$REPO_OWNER" ]; then
echo "The issue creator is the repository owner. Skipping greeting message."
echo "skip_first_request=true" >> $GITHUB_ENV
else
echo "Checking for previous feature requests..."
# Get all issues (both open and closed) by the author except the current one
PREVIOUS_REQUESTS=$(gh issue list --author "$ISSUE_AUTHOR" --label "New Feature" --state all --json number --jq '. | map(select(.number != '$ISSUE_NUMBER')) | length')
echo "User $ISSUE_AUTHOR has submitted $PREVIOUS_REQUESTS feature request(s) previously"
if [ "$PREVIOUS_REQUESTS" -eq 0 ]; then
echo "This is the user's first feature request. Sending greeting message."
echo "skip_first_request=false" >> $GITHUB_ENV
else
echo "User has previous feature requests. Skipping greeting message."
echo "skip_first_request=true" >> $GITHUB_ENV
fi
fi
- name: Send Greeting Message
if: env.skip_label_check == 'false' && env.skip_first_request == 'false'
uses: actions/github-script@v6
with:
script: |
const issueNumber = context.payload.issue.number;
const message = `
**💡 Thank you for your feature request!**
Your request has been successfully submitted and is now under consideration. We value your input in shaping the future of Dantotsu.
**📈 What to Expect Next**
- Our team will review your request and assess its feasibility.
- We may reach out for additional details or clarification.
- Updates on the request will be provided, and it may be scheduled for future development.
**👥 Stay Connected**
- **[Discord](https://discord.com/invite/4HPZ5nAWwM)**: Join our community to discuss ideas and stay updated.
- **[Telegram](https://t.me/dantotsuapp)**: Connect with us directly for real-time updates.
We appreciate your suggestion and look forward to potentially implementing it!
`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: message
});

80
.github/workflows/pr_greetings.yml vendored Normal file
View File

@@ -0,0 +1,80 @@
name: PR Greetings
on:
pull_request:
types: [opened]
pull_request_target:
types: [opened]
jobs:
greeting:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Check if the PR creator is the repo owner or Weblate
id: check_owner
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_AUTHOR=$(jq -r '.pull_request.user.login' "$GITHUB_EVENT_PATH")
REPO_OWNER=$(jq -r '.repository.owner.login' "$GITHUB_EVENT_PATH")
if [ "$PR_AUTHOR" = "$REPO_OWNER" ] || [ "$PR_AUTHOR" = "weblate" ]; then
echo "The PR creator is the repository owner or Weblate. Skipping greeting message."
echo "skip=true" >> $GITHUB_ENV
else
echo "The PR creator is not the repository owner or Weblate. Checking for previous PRs..."
# Check for both open and closed pull requests by the author
OPEN_PRS=$(gh pr list --author "$PR_AUTHOR" --state open --json number --jq '. | length')
CLOSED_PRS=$(gh pr list --author "$PR_AUTHOR" --state closed --json number --jq '. | length')
TOTAL_PRS=$((OPEN_PRS + CLOSED_PRS))
echo "User $PR_AUTHOR has created $TOTAL_PRS pull request(s) in total"
echo "Open PRs: $OPEN_PRS"
echo "Closed PRs: $CLOSED_PRS"
if [ "$TOTAL_PRS" -eq 1 ]; then
echo "This is the user's first pull request. Sending greeting message."
echo "skip=false" >> $GITHUB_ENV
else
echo "User has previous pull requests. Skipping greeting message."
echo "skip=true" >> $GITHUB_ENV
fi
fi
- name: Send Greeting Message
if: env.skip != 'true'
uses: actions/github-script@v6
with:
script: |
const prNumber = context.payload.pull_request.number;
const message = `
**🎉 Thank you for your contribution!**
Your Pull Request has been successfully submitted and is now awaiting review. We truly appreciate your efforts to improve Dantotsu.
**👥 Connect with the Community**
While you're here, why not join our communities to stay engaged?
- **[Discord](https://discord.com/invite/4HPZ5nAWwM)**: Chat with fellow developers, ask questions, and get the latest updates.
- **[Telegram](https://t.me/dantotsuapp)**: Connect directly with us for real-time discussions and updates.
**📋 What to Expect Next**
- Our team will review your pull request as soon as possible.
- You'll receive notifications if further information or changes are needed.
- Once approved, your changes will be merged into the main project.
We're excited to collaborate with you. Stay tuned for updates!
`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: message
});

12
.gitignore vendored
View File

@@ -2,12 +2,18 @@
.gradle/
build/
#kotlin
.kotlin/
# Local configuration file (sdk path, etc)
local.properties
# Log/OS Files
*.log
# Secrets
apikey.properties
# Android Studio generated files and folders
captures/
.externalNativeBuild/
@@ -28,3 +34,9 @@ output.json
#other
scripts/
#crowdin
crowdin.yml
#vscode
.vscode

View File

@@ -1,674 +1,17 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
## Unabandon Public License (UPL)
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
**Preamble**
Preamble
This Unabandon Public License (UPL) is designed to ensure the continued development and public availability of source code based on works released under the GNU General Public License Version 3 (GPLv3) while upholding the core principles of GPLv3. This license extends GPLv3 by mandating public accessibility of source code for any derivative works.
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
**Body**
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
1. **Incorporation of GPLv3:** This UPL incorporates all terms and conditions of the GNU General Public License Version 3 (GPLv3) as published by the Free Software Foundation. You can find the complete text of GPLv3 at [https://www.gnu.org/licenses/licenses.en.html](https://www.gnu.org/licenses/licenses.en.html).
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
2. **Public Source Requirement:** In addition to the terms of GPLv3, the source code for any software distributed under this license, including modifications and derivative works, must be publicly available. Public availability means the source code must be accessible to anyone through a publicly accessible repository or download link without any access restrictions or fees.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
3. **Source Code Availability:** The source code must be made publicly available using a recognized open-source hosting platform (e.g., GitHub, GitLab) or be downloadable from a publicly accessible website. The chosen method must clearly identify the source code and its corresponding licensed work.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
**Termination**
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
This UPL terminates automatically if the terms and conditions are not followed by the licensee.

View File

@@ -14,7 +14,26 @@ Dantotsu is an [Anilist](https://anilist.co/) only client.
> **Dantotsu (断トツ; Dan-totsu)** literally means "the best of the best" in Japanese. Try it out for yourself and be the judge!
<a href="https://www.buymeacoffee.com/rebelonion"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=rebelonion&button_colour=FFDD00&font_colour=030201&font_family=Poppins&outline_colour=000000&coffee_colour=ffffff" /></a>
<a href="https://www.buymeacoffee.com/rebelonion"><img src="https://img.buymeacoffee.com/button-api/?text=Buy me a coffee&emoji=&slug=rebelonion&button_colour=FFDD00&font_colour=000000&font_family=Cookie&outline_colour=000000&coffee_colour=ffffff" /></a>
## Terms of Use
By downloading, installing, or using this application, you agree to:
- Use the application in compliance with all applicable laws
- Not use the application to infringe on copyrighted content
- Take full responsibility for any extensions you install or use
- Understand that the developer(s) are not responsible for third-party extensions or user actions
This application is designed for anime tracking and legal streaming service integration. The developers do not provide, maintain, or endorse any extensions that enable access to unauthorized content.
## Important Notice
This application is an anime tracking and management tool. The extension system is designed to integrate with legal streaming services like Jellyfin.
We do not:
- Provide or maintain any streaming extensions
- Host or distribute copyrighted content
- Endorse or encourage copyright infringement
Users are responsible for ensuring their use of this software complies with local laws and regulations.
### 🚀 STAR THIS REPOSITORY TO SUPPORT THE DEVELOPER AND ENCOURAGE THE DEVELOPMENT OF THE APPLICATION!
@@ -38,4 +57,4 @@ You can come hang out with our awesome community, request new features, and repo
## LICENSE 📜
Dantotsu is licensed under the [GNU General Public License v3.0](LICENSE.md)
Dantotsu is licensed under the Unabandon Public License (UPL). More info can be found [here.](LICENSE.md)

4
app/.gitignore vendored
View File

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

View File

@@ -1,12 +1,9 @@
plugins {
id 'com.android.application'
id 'com.google.gms.google-services'
id 'com.google.firebase.crashlytics'
id 'kotlin-android'
id 'kotlinx-serialization'
id 'org.jetbrains.kotlin.android'
id 'com.google.devtools.ksp'
}
def gitCommitHash = providers.exec {
@@ -14,31 +11,60 @@ def gitCommitHash = providers.exec {
}.standardOutput.asText.get().trim()
android {
compileSdk 34
compileSdk 35
defaultConfig {
applicationId "ani.dantotsu"
minSdk 23
targetSdk 34
versionCode ((System.currentTimeMillis() / 60000).toInteger())
versionName "2.0.0-beta01-iv1"
minSdk 21
targetSdk 35
versionName "3.2.2"
versionCode 300200200
signingConfig signingConfigs.debug
}
flavorDimensions += "store"
productFlavors {
fdroid {
// F-Droid specific configuration
dimension "store"
versionNameSuffix "-fdroid"
}
google {
// Google Play specific configuration
dimension "store"
isDefault true
apply plugin: 'com.google.gms.google-services'
apply plugin: 'com.google.firebase.crashlytics'
}
}
buildTypes {
alpha {
applicationIdSuffix ".beta" // keep as beta by popular request
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"
manifestPlaceholders = [icon_placeholder: "@mipmap/ic_launcher_beta", icon_placeholder_round: "@mipmap/ic_launcher_beta_round"]
debuggable System.getenv("CI") == null
versionNameSuffix "-beta01"
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'
}
}
buildFeatures {
viewBinding true
buildConfig true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
@@ -52,10 +78,15 @@ android {
}
dependencies {
// 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.browser:browser:1.8.0'
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.fragment:fragment-ktx:1.6.2'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
@@ -63,13 +94,16 @@ dependencies {
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.9.0'
implementation 'androidx.webkit:webkit:1.11.0'
implementation "com.anggrayudi:storage:1.5.5"
implementation "androidx.biometric:biometric:1.1.0"
// Glide
// Glide
ext.glide_version = '4.16.0'
api "com.github.bumptech.glide:glide:$glide_version"
implementation "com.github.bumptech.glide:glide:$glide_version"
@@ -77,51 +111,63 @@ dependencies {
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
implementation 'jp.wasabeef:glide-transformations:4.3.0'
// FireBase
implementation platform('com.google.firebase:firebase-bom:32.2.3')
implementation 'com.google.firebase:firebase-analytics-ktx:21.5.0'
implementation 'com.google.firebase:firebase-crashlytics-ktx:18.6.0'
// Exoplayer
ext.exo_version = '1.2.0'
// Exoplayer
ext.exo_version = '1.5.0'
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"
// Media3 extension
implementation "com.github.anilbeesetti.nextlib:nextlib-media3ext:0.8.3"
// 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.3'
// 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'
implementation 'ca.gosyer:voyager-navigator:1.0.0-rc07'
implementation 'com.squareup.logcat:logcat:0.1'
implementation 'com.github.inorichi.injekt:injekt-core:65b0440'
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.11'
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
implementation 'uy.kohesive.injekt:injekt-core:1.16.+'
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 '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 'com.squareup.okio:okio:3.8.0'
implementation 'com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.12'
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 @@
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:1039200814590:android:40e14720ee97917e1aacaf",
"android_client_info": {
"package_name": "ani.dantotsu.alpha"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyCiXo_q4S2ofA5oCztsoLnlDqJi3GtTJjY"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:1039200814590:android:40e14720ee97917e1aacaf",

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

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,40 @@
package ani.dantotsu.others
import androidx.fragment.app.FragmentActivity
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.text.SimpleDateFormat
import java.util.Locale
object AppUpdater {
suspend fun check(activity: FragmentActivity, post: Boolean = false) {
// no-op
}
@Serializable
data class GithubResponse(
@SerialName("html_url")
val htmlUrl: String,
@SerialName("tag_name")
val tagName: String,
val prerelease: Boolean,
@SerialName("created_at")
val createdAt: String,
val body: String? = null,
val assets: List<Asset>? = null
) {
@Serializable
data class Asset(
@SerialName("browser_download_url")
val browserDownloadURL: String
)
fun timeStamp(): Long {
return dateFormat.parse(createdAt)!!.time
}
companion object {
private val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault())
}
}
}

View File

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

View File

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

View File

@@ -11,12 +11,23 @@ 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 io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin
import ani.dantotsu.BuildConfig
import ani.dantotsu.Mapper
import ani.dantotsu.R
import ani.dantotsu.buildMarkwon
import ani.dantotsu.client
import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.currContext
import ani.dantotsu.decodeBase64ToString
import ani.dantotsu.logError
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.snackString
import ani.dantotsu.toast
import ani.dantotsu.tryWithSuspend
import ani.dantotsu.util.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
@@ -24,33 +35,95 @@ 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 {
private val fallbackStableUrl: String
get() = "aHR0cHM6Ly9hcGkuZGFudG90c3UuYXBwL3VwZGF0ZXMvc3RhYmxl".decodeBase64ToString()
private val fallbackBetaUrl: String
get() = "aHR0cHM6Ly9hcGkuZGFudG90c3UuYXBwL3VwZGF0ZXMvYmV0YQ==".decodeBase64ToString()
@Serializable
data class FallbackResponse(
val version: String,
val changelog: String,
val downloadUrl: String? = null
)
private suspend fun fetchUpdateInfo(repo: String, isDebug: Boolean): Pair<String, String>? {
return try {
fetchFromGithub(repo, isDebug)
} catch (e: Exception) {
Logger.log("Github fetch failed, trying fallback: ${e.message}")
try {
fetchFromFallback(isDebug)
} catch (e: Exception) {
Logger.log("Fallback fetch failed: ${e.message}")
null
}
}
}
private suspend fun fetchFromGithub(repo: String, isDebug: Boolean): Pair<String, String> {
return if (isDebug) {
val res = client.get("https://api.github.com/repos/$repo/releases")
.parsed<JsonArray>().map {
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
}
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 {
val res = client.get("https://raw.githubusercontent.com/$repo/main/stable.md").text
res to res.substringAfter("# ").substringBefore("\n")
}
}
private suspend fun fetchFromFallback(isDebug: Boolean): Pair<String, String> {
val url = if (isDebug) fallbackBetaUrl else fallbackStableUrl
val response = CommentsAPI.requestBuilder().get(url).parsed<FallbackResponse>()
return response.changelog to response.version
}
private suspend fun fetchApkUrl(repo: String, version: String, isDebug: Boolean): String? {
return try {
fetchApkUrlFromGithub(repo, version)
} catch (e: Exception) {
Logger.log("Github APK fetch failed, trying fallback: ${e.message}")
try {
fetchApkUrlFromFallback(version, isDebug)
} catch (e: Exception) {
Logger.log("Fallback APK fetch failed: ${e.message}")
null
}
}
}
private suspend fun fetchApkUrlFromGithub(repo: String, version: String): String? {
val apks = client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
.parsed<GithubResponse>().assets?.filter {
it.browserDownloadURL.endsWith(".apk")
}
return apks?.firstOrNull()?.browserDownloadURL
}
private suspend fun fetchApkUrlFromFallback(version: String, isDebug: Boolean): String? {
val url = if (isDebug) fallbackBetaUrl else fallbackStableUrl
return CommentsAPI.requestBuilder().get("$url/$version").parsed<FallbackResponse>().downloadUrl
}
suspend fun check(activity: FragmentActivity, post: Boolean = false) {
if (post) snackString(currContext()?.getString(R.string.checking_for_update))
val repo = activity.getString(R.string.repo)
tryWithSuspend {
val (md, version) = if (BuildConfig.DEBUG) {
val res = client.get("https://api.github.com/repos/$repo/releases")
.parsed<JsonArray>().map {
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
}
val r = res.filter { it.prerelease }.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 {
val res =
client.get("https://raw.githubusercontent.com/$repo/main/stable.md").text
res to res.substringAfter("# ").substringBefore("\n")
}
val (md, version) = fetchUpdateInfo(repo, BuildConfig.DEBUG) ?: return@tryWithSuspend
logger("Git Version : $version")
val dontShow = loadData("dont_ask_for_update_$version") ?: false
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 {
setTitleText(
@@ -60,8 +133,11 @@ object AppUpdater {
)
addView(
TextView(activity).apply {
val markWon = Markwon.builder(activity)
.usePlugin(SoftBreakAddsNewLinePlugin.create()).build()
val markWon = try {
buildMarkwon(activity, false)
} catch (e: IllegalArgumentException) {
return@runOnUiThread
}
markWon.setMarkdown(this, md)
}
)
@@ -71,19 +147,18 @@ object AppUpdater {
false
) { isChecked ->
if (isChecked) {
saveData("dont_ask_for_update_$version", true)
PrefManager.setCustomVal("dont_ask_for_update_$version", true)
}
}
setPositiveButton(currContext()!!.getString(R.string.lets_go)) {
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 apkUrl = fetchApkUrl(repo, version, BuildConfig.DEBUG)
if (apkUrl != null) {
activity.downloadUpdate(version, apkUrl)
} else {
openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
}
} catch (e: Exception) {
logError(e)
}
@@ -95,39 +170,38 @@ object AppUpdater {
}
show(activity.supportFragmentManager, "dialog")
}
}
else {
} else {
if (post) snackString(currContext()?.getString(R.string.no_update_found))
}
}
}
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
}
}
//Blatantly kanged from https://github.com/LagradOst/CloudStream-3/blob/master/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt
private fun Activity.downloadUpdate(version: String, url: String): Boolean {
private fun Activity.downloadUpdate(version: String, url: String) {
toast(getString(R.string.downloading_update, version))
val downloadManager = this.getSystemService<DownloadManager>()!!
@@ -149,7 +223,7 @@ object AppUpdater {
logError(e)
-1
}
if (id == -1L) return true
if (id == -1L) return
ContextCompat.registerReceiver(
this,
object : BroadcastReceiver() {
@@ -160,21 +234,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)
@@ -183,22 +244,16 @@ object AppUpdater {
}, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
ContextCompat.RECEIVER_EXPORTED
)
return true
}
fun openApk(context: Context, uri: Uri) {
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" />
@@ -10,15 +12,17 @@
android:required="false" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"
<uses-permission
android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"
tools:ignore="LeanbackUsesWifi" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.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.MODIFY_AUDIO_SETTINGS" />
<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 -->
@@ -37,6 +41,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" />
@@ -48,6 +63,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"
@@ -56,9 +72,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" />
@@ -66,10 +103,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"
@@ -77,10 +113,9 @@
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/epub+zip" />
<data android:mimeType="application/epub+zip"/>
<data android:mimeType="application/x-mobipocket-ebook" />
<data android:mimeType="application/vnd.amazon.ebook" />
<data android:mimeType="application/fb2+zip" />
@@ -94,19 +129,89 @@
<data android:scheme="file" />
</intent-filter>
</activity>
<activity android:name=".settings.FAQActivity" />
<activity android:name=".settings.ReaderSettingsActivity" />
<activity android:name=".others.calc.CalcActivity"
android:parentActivityName=".MainActivity" />
<activity android:name=".settings.AnilistSettingsActivity"/>
<activity android:name=".settings.UserInterfaceSettingsActivity" />
<activity android:name=".settings.PlayerSettingsActivity" />
<activity android:name=".settings.ReaderSettingsActivity" />
<activity android:name=".settings.FAQActivity" />
<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"
android:windowSoftInputMode="adjustPan"/>
<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.notification.NotificationActivity"
android:label="Inbox Activity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".others.imagesearch.ImageSearchActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".util.ActivityMarkdownCreator"
android:windowSoftInputMode="adjustResize|stateVisible" />
<activity android:name=".parsers.ParserTestActivity" />
<activity
android:name=".media.ReviewActivity"
android:parentActivityName=".media.MediaDetailsActivity" />
<activity
android:name=".media.ReviewViewActivity"
android:parentActivityName=".media.ReviewActivity" />
<activity
android:name=".media.SearchActivity"
android:parentActivityName=".MainActivity" />
@@ -116,6 +221,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"
@@ -123,12 +231,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"
@@ -238,6 +355,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"
@@ -245,32 +373,53 @@
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.Main" />
<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:scheme="file" />
<data android:mimeType="*/*" />
<data android:pathPattern=".*\\.ani" />
<data android:pathPattern=".*\\.sani" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:host="add-repo"/>
<data android:scheme="tachiyomi"/>
<data android:scheme="aniyomi"/>
<data android:scheme="novelyomi"/>
</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"
@@ -288,25 +437,11 @@
</provider>
<service
android:name=".widgets.CurrentlyAiringRemoteViewsService"
android:permission="android.permission.BIND_REMOTEVIEWS"
android:exported="true" />
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
@@ -317,19 +452,27 @@
android:name=".download.novel.NovelDownloaderService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service android:name=".download.anime.AnimeDownloaderService"
<service
android:name=".download.anime.AnimeDownloaderService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service
android:name=".connections.discord.DiscordService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service android:name="androidx.media3.exoplayer.scheduler.PlatformScheduler$PlatformSchedulerService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true"/>
<service
android:name="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"
android:value="androidx.media3.cast.DefaultCastOptionsProvider"/>
<meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="androidx.media3.cast.DefaultCastOptionsProvider" />
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -4,32 +4,41 @@ 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
import ani.dantotsu.parsers.NovelSources
import ani.dantotsu.parsers.novel.NovelExtensionManager
import ani.dantotsu.settings.SettingsActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.util.FinalExceptionHandler
import ani.dantotsu.util.Logger
import com.google.android.material.color.DynamicColors
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.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.addSingletonFactory
import uy.kohesive.injekt.api.get
@@ -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,82 +61,112 @@ class App : MultiDexApplication() {
val mFTActivityLifecycleCallbacks = FTActivityLifecycleCallbacks()
@OptIn(DelicateCoroutinesApi::class)
override fun onCreate() {
super.onCreate()
val sharedPreferences = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
val useMaterialYou = sharedPreferences.getBoolean("use_material_you", false)
if (useMaterialYou) {
DynamicColors.applyToActivitiesIfAvailable(this)
//TODO: HarmonizedColors
}
registerActivityLifecycleCallbacks(mFTActivityLifecycleCallbacks)
PrefManager.init(this)
Firebase.crashlytics.setCrashlyticsCollectionEnabled(!DisabledReports)
getSharedPreferences(
getString(R.string.preference_file_key),
Context.MODE_PRIVATE
).getBoolean("shared_user_id", true).let {
if (!it) return@let
val dUsername = getSharedPreferences(
getString(R.string.preference_file_key),
Context.MODE_PRIVATE
).getString("discord_username", null)
val aUsername = getSharedPreferences(
getString(R.string.preference_file_key),
Context.MODE_PRIVATE
).getString("anilist_username", null)
if (dUsername != null || aUsername != null) {
Firebase.crashlytics.setUserId("$dUsername - $aUsername")
}
}
FirebaseCrashlytics.getInstance().setCustomKey("device Info", SettingsActivity.getDeviceInfo())
val crashlytics =
ani.dantotsu.connections.crashlytics.CrashlyticsFactory.createCrashlytics()
Injekt.addSingletonFactory<CrashlyticsInterface> { crashlytics }
crashlytics.initialize(this)
Logger.init(this)
Thread.setDefaultUncaughtExceptionHandler(FinalExceptionHandler())
Logger.log(Log.WARN, "App: Logging started")
Injekt.importModule(AppModule(this))
Injekt.importModule(PreferenceModule(this))
initializeNetwork(baseContext)
val useMaterialYou: Boolean = PrefManager.getVal(PrefName.UseMaterialYou)
if (useMaterialYou) {
DynamicColors.applyToActivitiesIfAvailable(this)
}
registerActivityLifecycleCallbacks(mFTActivityLifecycleCallbacks)
crashlytics.setCrashlyticsCollectionEnabled(!DisabledReports)
(PrefManager.getVal(PrefName.SharedUserID) as Boolean).let {
if (!it) return@let
val dUsername = PrefManager.getVal(PrefName.DiscordUserName, null as String?)
val aUsername = PrefManager.getVal(PrefName.AnilistUserName, null as String?)
if (dUsername != null) {
crashlytics.setCustomKey("dUsername", dUsername)
}
if (aUsername != null) {
crashlytics.setCustomKey("aUsername", aUsername)
}
}
crashlytics.setCustomKey("device Info", SettingsActivity.getDeviceInfo())
initializeNetwork()
setupNotificationChannels()
if (!LogcatLogger.isInstalled) {
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
}
animeExtensionManager = Injekt.get()
mangaExtensionManager = Injekt.get()
novelExtensionManager = Injekt.get()
if (PrefManager.getVal<Int>(PrefName.CommentsEnabled) == 0) {
if (BuildConfig.FLAVOR.contains("fdroid")) {
PrefManager.setVal(PrefName.CommentsEnabled, 2)
} else {
PrefManager.setVal(PrefName.CommentsEnabled, 1)
}
}
val animeScope = CoroutineScope(Dispatchers.Default)
animeScope.launch {
CoroutineScope(Dispatchers.IO).launch {
animeExtensionManager = Injekt.get()
animeExtensionManager.findAvailableExtensions()
logger("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
AnimeSources.init(animeExtensionManager.installedExtensionsFlow, this@App)
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()}")
MangaSources.init(mangaExtensionManager.installedExtensionsFlow, this@App)
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()
if (PrefManager.getVal<Int>(PrefName.CommentsEnabled) == 1) {
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)
}
}
inner class FTActivityLifecycleCallbacks : ActivityLifecycleCallbacks {
var currentActivity: Activity? = null
override fun onActivityCreated(p0: Activity, p1: Bundle?) {}
var lastActivity: String? = null
override fun onActivityCreated(p0: Activity, p1: Bundle?) {
lastActivity = p0.javaClass.simpleName
}
override fun onActivityStarted(p0: Activity) {
currentActivity = p0
}
@@ -140,7 +182,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

File diff suppressed because it is too large Load Diff

View File

@@ -2,8 +2,8 @@ package ani.dantotsu
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.graphics.drawable.Animatable
import android.graphics.drawable.GradientDrawable
import android.net.Uri
@@ -12,12 +12,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
@@ -26,42 +24,57 @@ 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.others.LangSet
import ani.dantotsu.others.SharedPreferenceBooleanLiveData
import ani.dantotsu.parsers.novel.NovelExtensionManager
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.subcriptions.Subscription.Companion.startSubscription
import ani.dantotsu.others.calc.CalcActivity
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.activity.FeedActivity
import ani.dantotsu.profile.notification.NotificationActivity
import ani.dantotsu.settings.AddRepositoryBottomSheet
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.settings.saving.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferencePackager
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.AudioHelper
import ani.dantotsu.util.Logger
import ani.dantotsu.util.customAlertDialog
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
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
@@ -73,37 +86,51 @@ class MainActivity : AppCompatActivity() {
private val scope = lifecycleScope
private var load = false
private var uiSettings = UserInterfaceSettings()
@kotlin.OptIn(DelicateCoroutinesApi::class)
@SuppressLint("InternalInsetResource", "DiscouragedApi")
@OptIn(UnstableApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
ThemeManager(this).applyTheme()
LangSet.setLocale(this)
super.onCreate(savedInstanceState)
//get FRAGMENT_CLASS_NAME from intent
val FRAGMENT_CLASS_NAME = intent.getStringExtra("FRAGMENT_CLASS_NAME")
val fragment = intent.getStringExtra("FRAGMENT_CLASS_NAME")
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
TaskScheduler.scheduleSingleWork(this)
if (!CalcActivity.hasPermission) {
val pin: String = PrefManager.getVal(PrefName.AppPassword)
if (pin.isNotEmpty()) {
ContextCompat.startActivity(
this@MainActivity,
Intent(this@MainActivity, CalcActivity::class.java)
.putExtra("code", pin)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK),
null
)
finish()
return
}
}
if (Intent.ACTION_VIEW == intent.action) {
handleViewIntent(intent)
}
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
}
val sharedPreferences = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
val colorOverflow = sharedPreferences.getBoolean("colorOverflow", false)
if (!colorOverflow) {
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
bottomNavBar.background = backgroundDrawable
}
bottomNavBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
val offset = try {
val statusBarHeightId = resources.getIdentifier("status_bar_height", "dimen", "android")
@@ -114,11 +141,10 @@ class MainActivity : AppCompatActivity() {
val layoutParams = binding.incognito.layoutParams as ViewGroup.MarginLayoutParams
layoutParams.topMargin = 11 * offset / 12
binding.incognito.layoutParams = layoutParams
incognitoLiveData = SharedPreferenceBooleanLiveData(
sharedPreferences,
"incognito",
incognitoLiveData = PrefManager.getLiveVal(
PrefName.Incognito,
false
)
).asLiveBool()
incognitoLiveData.observe(this) {
if (it) {
val slideDownAnim = ObjectAnimator.ofFloat(
@@ -154,20 +180,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
@@ -213,24 +233,96 @@ class MainActivity : AppCompatActivity() {
binding.root.doOnAttach {
initActivity(this)
uiSettings = loadData("ui_settings") ?: uiSettings
selectedOption = if (FRAGMENT_CLASS_NAME != null) {
when (FRAGMENT_CLASS_NAME) {
val preferences: SourcePreferences = Injekt.get()
if (preferences.animeExtensionUpdatesCount()
.get() > 0 || preferences.mangaExtensionUpdatesCount().get() > 0
) {
snackString(R.string.extension_updates_available)
?.setDuration(Snackbar.LENGTH_SHORT)
?.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
HomeFragment::class.java.name -> 1
MangaFragment::class.java.name -> 2
else -> 1
}
} else {
uiSettings.defaultStartUpTab
PrefManager.getVal(PrefName.DefaultStartUpTab)
}
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.selectTabAt(selectedOption)
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) {
mainViewPager.post {
mainViewPager.setCurrentItem(
selectedOption,
false
)
}
}
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
}
}
val offlineMode = getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
.getBoolean("offlineMode", false)
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("activityId", activityId)
}
launched = true
startActivity(notificationIntent)
}
}
val offlineMode: Boolean = PrefManager.getVal(PrefName.OfflineMode)
if (!isOnline(this)) {
snackString(this@MainActivity.getString(R.string.no_internet_connection))
startActivity(Intent(this, NoInternet::class.java))
@@ -240,45 +332,9 @@ 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(uiSettings))
navbar.setOnTabSelectListener(object :
AnimatedBottomBar.OnTabSelectListener {
override fun onTabSelected(
lastIndex: Int,
lastTab: AnimatedBottomBar.Tab?,
newIndex: Int,
newTab: AnimatedBottomBar.Tab
) {
navbar.animate().translationZ(12f).setDuration(200).start()
selectedOption = newIndex
mainViewPager.setCurrentItem(newIndex, false)
}
})
navbar.selectTabAt(selectedOption)
mainViewPager.post {
mainViewPager.setCurrentItem(
selectedOption,
false
)
}
} else {
binding.mainProgressBar.visibility = View.GONE
}
}
}
//Load Data
if (!load) {
if (!load && !launched) {
scope.launch(Dispatchers.IO) {
model.loadMain(this@MainActivity)
val id = intent.extras?.getInt("mediaId", 0)
@@ -298,14 +354,27 @@ 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
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (loadData<Boolean>("allow_opening_links", this) != true) {
if (!(PrefManager.getVal(PrefName.AllowOpeningLinks) as Boolean)) {
CustomBottomDialog.newInstance().apply {
title = "Allow Dantotsu to automatically open Anilist & MAL Links?"
val md = "Open settings & click +Add Links & select Anilist & Mal urls"
@@ -317,45 +386,161 @@ class MainActivity : AppCompatActivity() {
})
setNegativeButton(this@MainActivity.getString(R.string.no)) {
saveData("allow_opening_links", true, this@MainActivity)
PrefManager.setVal(PrefName.AllowOpeningLinks, true)
dismiss()
}
setPositiveButton(this@MainActivity.getString(R.string.yes)) {
saveData("allow_opening_links", true, this@MainActivity)
PrefManager.setVal(PrefName.AllowOpeningLinks, true)
tryWith(true) {
startActivity(
Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS)
.setData(Uri.parse("package:$packageName"))
)
}
dismiss()
}
}.show(supportFragmentManager, "dialog")
}
}
}
}
//TODO: Remove this
GlobalScope.launch(Dispatchers.IO) {
val index = Helper.downloadManager(this@MainActivity).downloadIndex
val downloadCursor = index.getDownloads()
while (downloadCursor.moveToNext()) {
val download = downloadCursor.download
Log.e("Downloader", download.request.uri.toString())
Log.e("Downloader", download.request.id.toString())
Log.e("Downloader", download.request.mimeType.toString())
Log.e("Downloader", download.request.data.size.toString())
Log.e("Downloader", download.bytesDownloaded.toString())
Log.e("Downloader", download.state.toString())
Log.e("Downloader", download.failureReason.toString())
if (download.state == Download.STATE_FAILED) { //simple cleanup
Helper.downloadManager(this@MainActivity).removeDownload(download.request.id)
if (PrefManager.getVal(PrefName.OC)) {
AudioHelper.run(this, R.raw.audio)
PrefManager.setVal(PrefName.OC, false)
}
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 handleViewIntent(intent: Intent) {
val uri: Uri? = intent.data
try {
if (uri == null) {
throw Exception("Uri is null")
}
if ((uri.scheme == "tachiyomi" || uri.scheme == "aniyomi" || uri.scheme == "novelyomi") && uri.host == "add-repo") {
val url = uri.getQueryParameter("url") ?: throw Exception("No url for repo import")
val (prefName, name) = when (uri.scheme) {
"tachiyomi" -> PrefName.MangaExtensionRepos to "Manga"
"aniyomi" -> PrefName.AnimeExtensionRepos to "Anime"
"novelyomi" -> PrefName.NovelExtensionRepos to "Novel"
else -> throw Exception("Invalid scheme")
}
val savedRepos: Set<String> = PrefManager.getVal(prefName)
val newRepos = savedRepos.toMutableSet()
AddRepositoryBottomSheet.addRepoWarning(this) {
newRepos.add(url)
PrefManager.setVal(prefName, newRepos)
toast("$name Extension Repo added")
}
return
}
if (intent.type == null) return
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 newIntent = Intent(this, this.javaClass)
this.finish()
startActivity(newIntent)
}
} else {
toast("Password cannot be empty")
}
}
} else if (name.endsWith(".ani")) {
val decryptedJson = jsonString.toString(Charsets.UTF_8)
if (PreferencePackager.unpack(decryptedJson)) {
val newIntent = Intent(this, this.javaClass)
this.finish()
startActivity(newIntent)
}
} else {
toast("Invalid file type")
}
} catch (e: Exception) {
e.printStackTrace()
toast("Error importing settings")
}
}
private fun passwordAlertDialog(callback: (CharArray?) -> Unit) {
val password = CharArray(16).apply { fill('0') }
// Inflate the dialog layout
val dialogView = DialogUserAgentBinding.inflate(layoutInflater).apply {
userAgentTextBox.hint = "Password"
subtitle.visibility = View.VISIBLE
subtitle.text = getString(R.string.enter_password_to_decrypt_file)
}
customAlertDialog().apply {
setTitle("Enter Password")
setCustomView(dialogView.root)
setPosButton(R.string.yes) {
val editText = dialogView.userAgentTextBox
if (editText.text?.isNotBlank() == true) {
editText.text?.toString()?.trim()?.toCharArray(password)
callback(password)
} else {
toast("Password cannot be empty")
}
}
setNegButton(R.string.cancel) {
password.fill('0')
callback(null)
}
show()
}
}
//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,13 +136,15 @@ suspend fun <T> tryWithSuspend(
* A url, which can also have headers
* **/
data class FileUrl(
val url: String,
val headers: Map<String, String> = mapOf()
var url: String,
var headers: Map<String, String> = mapOf()
) : Serializable {
companion object {
operator fun get(url: String?, headers: Map<String, String> = mapOf()): FileUrl? {
return FileUrl(url ?: return null, headers)
}
private const val serialVersionUID = 1L
}
}

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,176 @@
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.DownloadAddonApiV2
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 {
/**
* Load an extension from a package name with a specific class name
* @param context the context
* @param packageName the package name of the extension
* @param type the type of extension
* @return the loaded extension
* @throws IllegalStateException if the extension is not of the correct type
* @throws ClassNotFoundException if the extension class is not found
* @throws NoClassDefFoundError if the extension class is not found
* @throws Exception if any other error occurs
* @throws PackageManager.NameNotFoundException if the package is not found
* @throws IllegalStateException if the extension is not found
*/
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("ClassNotFoundException load error: $extName ($className)")
Logger.log(e)
throw e
} catch (e: NoClassDefFoundError) {
Logger.log("NoClassDefFoundError 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? DownloadAddonApiV2
?: throw IllegalStateException("Extension is not a DownloadAddonApiV2")
DownloadLoadResult.Success(
DownloadAddon.Installed(
name = extName,
pkgName = pkgName,
versionName = versionName,
versionCode = versionCode,
extension = extension,
icon = context.getApplicationIcon(pkgName),
)
)
}
}
}
/**
* Load an extension from a package name (class is determined by type)
* @param context the context
* @param packageName the package name of the extension
* @param type the type of extension
* @return the loaded extension
*/
fun loadFromPkgName(context: Context, packageName: String, type: AddonType): LoadResult? {
return try {
when (type) {
AddonType.TORRENT -> loadExtension(
context,
packageName,
TorrentAddonManager.TORRENT_CLASS,
type
)
AddonType.DOWNLOAD -> loadExtension(
context,
packageName,
DownloadAddonManager.DOWNLOAD_CLASS,
type
)
}
} catch (e: Exception) {
Logger.log("Error loading extension from package name: $packageName")
Logger.log(e)
null
}
}
/**
* Check if a package is an extension by comparing the package name
* @param type the type of extension
* @param pkgInfo the package info
* @return true if the package is an extension
*/
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: DownloadAddonApiV2,
val icon: Drawable?,
val hasUpdate: Boolean = false,
) : Addon.Installed(name, pkgName, versionName, versionCode)
}

View File

@@ -0,0 +1,48 @@
package ani.dantotsu.addons.download
import android.content.Context
import android.net.Uri
interface DownloadAddonApiV2 {
fun cancelDownload(sessionId: Long)
fun setDownloadPath(context: Context, uri: Uri): String
fun getReadPath(context: Context, uri: Uri): String
suspend fun executeFFProbe(
videoUrl: String,
headers: Map<String, String> = emptyMap(),
logCallback: (String) -> Unit
)
suspend fun executeFFMpeg(
videoUrl: String,
downloadPath: String,
headers: Map<String, String> = emptyMap(),
subtitleUrls: List<Pair<String, String>> = emptyList(),
audioUrls: List<Pair<String, String>> = emptyList(),
statCallback: (Double) -> Unit
): Long
suspend fun customFFMpeg(
command: String,
videoUrls: List<String>,
logCallback: (String) -> Unit
): Long
suspend fun customFFProbe(
command: String,
videoUrls: List<String>,
logCallback: (String) -> Unit
)
fun getState(sessionId: Long): String
fun getStackTrace(sessionId: Long): String?
fun hadError(sessionId: Long): Boolean
fun getFileExtension(): Pair<String, String> = Pair("mkv", "video/x-matroska")
}

View File

@@ -0,0 +1,135 @@
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(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)
}
}
Logger.log("Download addon initialized successfully")
withContext(Dispatchers.Main) {
_isInitialized.value = true
}
} catch (e: Exception) {
Logger.log("Error initializing Download addon")
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,142 @@
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.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.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(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)
}
}
Logger.log("Torrent addon initialized successfully")
withContext(Dispatchers.Main) {
_isInitialized.value = true
}
} catch (e: Exception) {
Logger.log("Error initializing torrent addon")
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,180 @@
/**
* modified source from
* https://github.com/rebelonion/Dantotsu/pull/305
* and https://github.com/LuftVerbot/kuukiyomi
* all credits to the original authors
*/
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

@@ -2,11 +2,12 @@ package ani.dantotsu.aniyomi.anime.custom
import android.app.Application
import android.content.Context
import androidx.annotation.OptIn
import androidx.core.content.ContextCompat
import androidx.media3.common.util.UnstableApi
import androidx.media3.database.StandaloneDatabaseProvider
import ani.dantotsu.addons.download.DownloadAddonManager
import ani.dantotsu.addons.torrent.TorrentAddonManager
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.media.manga.MangaCache
import ani.dantotsu.parsers.novel.NovelExtensionManager
@@ -16,9 +17,9 @@ import eu.kanade.tachiyomi.core.preference.AndroidPreferenceStore
import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.NetworkPreferences
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
@@ -30,24 +31,25 @@ 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)
addSingletonFactory { DownloadsManager(app) }
addSingletonFactory { NetworkHelper(app, get()) }
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()) }
val sharedPreferences = app.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
addSingleton(sharedPreferences)
addSingletonFactory {
Json {
ignoreUnknownKeys = true
@@ -72,13 +74,6 @@ class PreferenceModule(val application: Application) : InjektModule {
AndroidPreferenceStore(application)
}
addSingletonFactory {
NetworkPreferences(
preferenceStore = get(),
verboseLogging = false,
)
}
addSingletonFactory {
SourcePreferences(get())
}

View File

@@ -6,19 +6,20 @@ import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.currContext
import ani.dantotsu.media.Media
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.toast
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
fun updateProgress(media: Media, number: String) {
val incognito = currContext()?.getSharedPreferences("Dantotsu", 0)
?.getBoolean("incognito", false) ?: false
val incognito: Boolean = PrefManager.getVal(PrefName.Incognito)
if (!incognito) {
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

@@ -2,27 +2,42 @@ package ani.dantotsu.connections.anilist
import ani.dantotsu.R
import ani.dantotsu.currContext
import ani.dantotsu.media.Author
import ani.dantotsu.media.Character
import ani.dantotsu.media.Media
import ani.dantotsu.media.Studio
import ani.dantotsu.profile.User
import java.io.Serializable
data class SearchResults(
interface SearchResults<T> {
var search: String?
var page: Int
var results: MutableList<T>
var hasNextPage: Boolean
}
data class AniMangaSearchResults(
val type: String,
var isAdult: Boolean,
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>,
var hasNextPage: Boolean,
) : Serializable {
override var search: String? = null,
override var page: Int = 1,
override var results: MutableList<Media>,
override var hasNextPage: Boolean,
) : SearchResults<Media>, Serializable {
fun toChipList(): List<SearchChip> {
val list = mutableListOf<SearchChip>()
sort?.let {
@@ -37,12 +52,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 +101,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)
@@ -88,4 +119,33 @@ data class SearchResults(
val type: String,
val text: String
)
}
}
data class CharacterSearchResults(
override var search: String?,
override var page: Int = 1,
override var results: MutableList<Character>,
override var hasNextPage: Boolean,
) : SearchResults<Character>, Serializable
data class StudioSearchResults(
override var search: String?,
override var page: Int = 1,
override var results: MutableList<Studio>,
override var hasNextPage: Boolean,
) : SearchResults<Studio>, Serializable
data class StaffSearchResults(
override var search: String?,
override var page: Int = 1,
override var results: MutableList<Author>,
override var hasNextPage: Boolean,
) : SearchResults<Author>, Serializable
data class UserSearchResults(
override var search: String?,
override var page: Int = 1,
override var results: MutableList<User>,
override var hasNextPage: Boolean,
) : SearchResults<User>, Serializable

View File

@@ -6,11 +6,17 @@ import android.net.Uri
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.tryWithSuspend
import java.io.File
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 java.util.Calendar
import java.util.Locale
import kotlin.math.abs
object Anilist {
val query: AnilistQueries = AnilistQueries()
@@ -18,34 +24,84 @@ object Anilist {
var token: String? = null
var username: String? = null
var adult: Boolean = false
var userid: Int? = null
var avatar: String? = null
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
var adult: Boolean = false
var titleLanguage: String? = null
var staffNameLanguage: String? = null
var airingNotifications: Boolean = false
var restrictMessagesToFollowing: Boolean = false
var scoreFormat: String? = null
var rowOrder: String? = null
var activityMergeTime: Int? = null
var timezone: String? = null
var animeCustomLists: List<String>? = null
var mangaCustomLists: List<String>? = null
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"
)
@@ -53,6 +109,86 @@ object Anilist {
"Original Creator", "Story & Art", "Story"
)
val timeZone = listOf(
"(GMT-11:00) Pago Pago",
"(GMT-10:00) Hawaii Time",
"(GMT-09:00) Alaska Time",
"(GMT-08:00) Pacific Time",
"(GMT-07:00) Mountain Time",
"(GMT-06:00) Central Time",
"(GMT-05:00) Eastern Time",
"(GMT-04:00) Atlantic Time - Halifax",
"(GMT-03:00) Sao Paulo",
"(GMT-02:00) Mid-Atlantic",
"(GMT-01:00) Azores",
"(GMT+00:00) London",
"(GMT+01:00) Berlin",
"(GMT+02:00) Helsinki",
"(GMT+03:00) Istanbul",
"(GMT+04:00) Dubai",
"(GMT+04:30) Kabul",
"(GMT+05:00) Maldives",
"(GMT+05:30) India Standard Time",
"(GMT+05:45) Kathmandu",
"(GMT+06:00) Dhaka",
"(GMT+06:30) Cocos",
"(GMT+07:00) Bangkok",
"(GMT+08:00) Hong Kong",
"(GMT+08:30) Pyongyang",
"(GMT+09:00) Tokyo",
"(GMT+09:30) Central Time - Darwin",
"(GMT+10:00) Eastern Time - Brisbane",
"(GMT+10:30) Central Time - Adelaide",
"(GMT+11:00) Eastern Time - Melbourne, Sydney",
"(GMT+12:00) Nauru",
"(GMT+13:00) Auckland",
"(GMT+14:00) Kiritimati",
)
val titleLang = listOf(
"English (Attack on Titan)",
"Romaji (Shingeki no Kyojin)",
"Native (進撃の巨人)"
)
val staffNameLang = listOf(
"Romaji, Western Order (Killua Zoldyck)",
"Romaji (Zoldyck Killua)",
"Native (キルア=ゾルディック)"
)
val scoreFormats = listOf(
"100 Point (55/100)",
"10 Point Decimal (5.5/10)",
"10 Point (5/10)",
"5 Star (3/5)",
"3 Point Smiley :)"
)
val rowOrderMap = mapOf(
"Score" to "score",
"Title" to "title",
"Last Updated" to "updatedAt",
"Last Added" to "id"
)
val activityMergeTimeMap = mapOf(
"Never" to 0,
"30 mins" to 30,
"69 mins" to 69,
"1 hour" to 60,
"2 hours" to 120,
"3 hours" to 180,
"6 hours" to 360,
"12 hours" to 720,
"1 day" to 1440,
"2 days" to 2880,
"3 days" to 4320,
"1 week" to 10080,
"2 weeks" to 20160,
"Always" to 29160
)
private val cal: Calendar = Calendar.getInstance()
private val currentYear = cal.get(Calendar.YEAR)
private val currentSeason: Int = when (cal.get(Calendar.MONTH)) {
@@ -63,6 +199,33 @@ object Anilist {
else -> 0
}
fun getDisplayTimezone(apiTimezone: String, context: Context): String {
val noTimezone = context.getString(R.string.selected_no_time_zone)
val parts = apiTimezone.split(":")
if (parts.size != 2) return noTimezone
val hours = parts[0].toIntOrNull() ?: 0
val minutes = parts[1].toIntOrNull() ?: 0
val sign = if (hours >= 0) "+" else "-"
val formattedHours = String.format(Locale.US, "%02d", abs(hours))
val formattedMinutes = String.format(Locale.US, "%02d", minutes)
val searchString = "(GMT$sign$formattedHours:$formattedMinutes)"
return timeZone.find { it.contains(searchString) } ?: noTimezone
}
fun getApiTimezone(displayTimezone: String): String {
val regex = """\(GMT([+-])(\d{2}):(\d{2})\)""".toRegex()
val matchResult = regex.find(displayTimezone)
return if (matchResult != null) {
val (sign, hours, minutes) = matchResult.destructured
val formattedSign = if (sign == "+") "" else "-"
"$formattedSign$hours:$minutes"
} else {
"00:00"
}
}
private fun getSeason(next: Boolean): Pair<String, Int> {
var newSeason = if (next) currentSeason + 1 else currentSeason - 1
var newYear = currentYear
@@ -94,15 +257,12 @@ object Anilist {
}
}
fun getSavedToken(context: Context): Boolean {
if ("anilistToken" in context.fileList()) {
token = File(context.filesDir, "anilistToken").readText()
return true
}
return false
fun getSavedToken(): Boolean {
token = PrefManager.getVal(PrefName.AnilistToken, null as String?)
return !token.isNullOrEmpty()
}
fun removeSavedToken(context: Context) {
fun removeSavedToken() {
token = null
username = null
adult = false
@@ -111,9 +271,10 @@ object Anilist {
bg = null
episodesWatched = null
chapterRead = null
if ("anilistToken" in context.fileList()) {
File(context.filesDir, "anilistToken").delete()
}
PrefManager.removeVal(PrefName.AnilistToken)
//logout from comments api
CommentsAPI.logout()
}
suspend inline fun <reified T : Any> executeQuery(
@@ -124,13 +285,18 @@ 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")
}
val data = mapOf(
"query" to query,
"variables" to variables
)
val headers = mutableMapOf(
"Content-Type" to "application/json",
"Content-Type" to "application/json; charset=utf-8",
"Accept" to "application/json"
)
@@ -143,10 +309,27 @@ object Anilist {
data = data,
cacheTime = cache ?: 10
)
if (!json.text.startsWith("{")) throw Exception(currContext()?.getString(R.string.anilist_down))
if (show) println("Response : ${json.text}")
val remaining = json.headers["X-RateLimit-Remaining"]?.toIntOrNull() ?: -1
Logger.log("Remaining requests: $remaining")
if (json.code == 429) {
val retry = json.headers["Retry-After"]?.toIntOrNull() ?: -1
val passedLimitReset = json.headers["X-RateLimit-Reset"]?.toLongOrNull() ?: 0
if (retry > 0) {
rateLimitReset = passedLimitReset
}
toast("Rate limited. Try after $retry seconds")
throw Exception("Rate limited after $retry seconds")
}
if (!json.text.startsWith("{")) {
throw Exception(currContext()?.getString(R.string.anilist_down))
}
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

@@ -2,17 +2,179 @@ package ani.dantotsu.connections.anilist
import ani.dantotsu.connections.anilist.Anilist.executeQuery
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.connections.anilist.api.ToggleLike
import ani.dantotsu.currContext
import com.google.gson.Gson
import kotlinx.serialization.json.JsonObject
class AnilistMutations {
suspend fun updateSettings(
timezone: String? = null,
titleLanguage: String? = null,
staffNameLanguage: String? = null,
activityMergeTime: Int? = null,
airingNotifications: Boolean? = null,
displayAdultContent: Boolean? = null,
restrictMessagesToFollowing: Boolean? = null,
scoreFormat: String? = null,
rowOrder: String? = null,
) {
val query = """
mutation (
${"$"}timezone: String,
${"$"}titleLanguage: UserTitleLanguage,
${"$"}staffNameLanguage: UserStaffNameLanguage,
${"$"}activityMergeTime: Int,
${"$"}airingNotifications: Boolean,
${"$"}displayAdultContent: Boolean,
${"$"}restrictMessagesToFollowing: Boolean,
${"$"}scoreFormat: ScoreFormat,
${"$"}rowOrder: String
) {
UpdateUser(
timezone: ${"$"}timezone,
titleLanguage: ${"$"}titleLanguage,
staffNameLanguage: ${"$"}staffNameLanguage,
activityMergeTime: ${"$"}activityMergeTime,
airingNotifications: ${"$"}airingNotifications,
displayAdultContent: ${"$"}displayAdultContent,
restrictMessagesToFollowing: ${"$"}restrictMessagesToFollowing,
scoreFormat: ${"$"}scoreFormat,
rowOrder: ${"$"}rowOrder,
) {
id
options {
timezone
titleLanguage
staffNameLanguage
activityMergeTime
airingNotifications
displayAdultContent
restrictMessagesToFollowing
}
mediaListOptions {
scoreFormat
rowOrder
}
}
}
""".trimIndent()
val variables = """
{
${timezone?.let { """"timezone":"$it"""" } ?: ""}
${titleLanguage?.let { """"titleLanguage":"$it"""" } ?: ""}
${staffNameLanguage?.let { """"staffNameLanguage":"$it"""" } ?: ""}
${activityMergeTime?.let { """"activityMergeTime":$it""" } ?: ""}
${airingNotifications?.let { """"airingNotifications":$it""" } ?: ""}
${displayAdultContent?.let { """"displayAdultContent":$it""" } ?: ""}
${restrictMessagesToFollowing?.let { """"restrictMessagesToFollowing":$it""" } ?: ""}
${scoreFormat?.let { """"scoreFormat":"$it"""" } ?: ""}
${rowOrder?.let { """"rowOrder":"$it"""" } ?: ""}
}
""".trimIndent().replace("\n", "").replace(""" """, "").replace(",}", "}")
executeQuery<JsonObject>(query, variables)
}
suspend fun toggleFav(anime: Boolean = true, id: Int) {
val query =
"""mutation (${"$"}animeId: Int,${"$"}mangaId:Int) { ToggleFavourite(animeId:${"$"}animeId,mangaId:${"$"}mangaId){ anime { edges { id } } manga { edges { id } } } }"""
val query = """
mutation (${"$"}animeId: Int, ${"$"}mangaId: Int) {
ToggleFavourite(animeId: ${"$"}animeId, mangaId: ${"$"}mangaId) {
anime {
edges {
id
}
}
manga {
edges {
id
}
}
}
}
""".trimIndent()
val variables = if (anime) """{"animeId":"$id"}""" else """{"mangaId":"$id"}"""
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
}
}
}
}
""".trimIndent()
val result = executeQuery<JsonObject>(query)
return result?.get("errors") == null && result != null
}
enum class FavType {
ANIME, MANGA, CHARACTER, STAFF, STUDIO
}
suspend fun deleteCustomList(name: String, type: String): Boolean {
val query = """
mutation (${"$"}name: String, ${"$"}type: MediaType) {
DeleteCustomList(customList: ${"$"}name, type: ${"$"}type) {
deleted
}
}
""".trimIndent()
val variables = """
{
"name": "$name",
"type": "$type"
}
""".trimIndent()
val result = executeQuery<JsonObject>(query, variables)
return result?.get("errors") == null
}
suspend fun updateCustomLists(
animeCustomLists: List<String>?,
mangaCustomLists: List<String>?
): Boolean {
val query = """
mutation (${"$"}animeListOptions: MediaListOptionsInput, ${"$"}mangaListOptions: MediaListOptionsInput) {
UpdateUser(animeListOptions: ${"$"}animeListOptions, mangaListOptions: ${"$"}mangaListOptions) {
mediaListOptions {
animeList {
customLists
}
mangaList {
customLists
}
}
}
}
""".trimIndent()
val variables = """
{
${animeCustomLists?.let { """"animeListOptions": {"customLists": ${Gson().toJson(it)}}""" } ?: ""}
${if (animeCustomLists != null && mangaCustomLists != null) "," else ""}
${mangaCustomLists?.let { """"mangaListOptions": {"customLists": ${Gson().toJson(it)}}""" } ?: ""}
}
""".trimIndent().replace("\n", "").replace(""" """, "").replace(",}", "}")
val result = executeQuery<JsonObject>(query, variables)
return result?.get("errors") == null
}
suspend fun editList(
mediaID: Int,
progress: Int? = null,
@@ -25,14 +187,45 @@ class AnilistMutations {
completedAt: FuzzyDate? = null,
customList: List<String>? = null
) {
val query = """
mutation ( ${"$"}mediaID: Int, ${"$"}progress: Int,${"$"}private:Boolean,${"$"}repeat: Int, ${"$"}notes: String, ${"$"}customLists: [String], ${"$"}scoreRaw:Int, ${"$"}status:MediaListStatus, ${"$"}start:FuzzyDateInput${if (startedAt != null) "=" + startedAt.toVariableString() else ""}, ${"$"}completed:FuzzyDateInput${if (completedAt != null) "=" + completedAt.toVariableString() else ""} ) {
SaveMediaListEntry( mediaId: ${"$"}mediaID, progress: ${"$"}progress, repeat: ${"$"}repeat, notes: ${"$"}notes, private: ${"$"}private, scoreRaw: ${"$"}scoreRaw, status:${"$"}status, startedAt: ${"$"}start, completedAt: ${"$"}completed , customLists: ${"$"}customLists ) {
score(format:POINT_10_DECIMAL) startedAt{year month day} completedAt{year month day}
mutation (
${"$"}mediaID: Int,
${"$"}progress: Int,
${"$"}private: Boolean,
${"$"}repeat: Int,
${"$"}notes: String,
${"$"}customLists: [String],
${"$"}scoreRaw: Int,
${"$"}status: MediaListStatus,
${"$"}start: FuzzyDateInput${if (startedAt != null) "=" + startedAt.toVariableString() else ""},
${"$"}completed: FuzzyDateInput${if (completedAt != null) "=" + completedAt.toVariableString() else ""}
) {
SaveMediaListEntry(
mediaId: ${"$"}mediaID,
progress: ${"$"}progress,
repeat: ${"$"}repeat,
notes: ${"$"}notes,
private: ${"$"}private,
scoreRaw: ${"$"}scoreRaw,
status: ${"$"}status,
startedAt: ${"$"}start,
completedAt: ${"$"}completed,
customLists: ${"$"}customLists
) {
score(format: POINT_10_DECIMAL)
startedAt {
year
month
day
}
completedAt {
year
month
day
}
}
}
""".replace("\n", "").replace(""" """, "")
""".trimIndent()
val variables = """{"mediaID":$mediaID
${if (private != null) ""","private":$private""" else ""}
@@ -48,8 +241,194 @@ class AnilistMutations {
}
suspend fun deleteList(listId: Int) {
val query = "mutation(${"$"}id:Int){DeleteMediaListEntry(id:${"$"}id){deleted}}"
val query = """
mutation(${"$"}id: Int) {
DeleteMediaListEntry(id: ${"$"}id) {
deleted
}
}
""".trimIndent()
val variables = """{"id":"$listId"}"""
executeQuery<JsonObject>(query, variables)
}
suspend fun rateReview(reviewId: Int, rating: String): Query.RateReviewResponse? {
val query = """
mutation {
RateReview(reviewId: $reviewId, rating: $rating) {
id
mediaId
mediaType
summary
body(asHtml: true)
rating
ratingAmount
userRating
score
private
siteUrl
createdAt
updatedAt
user {
id
name
bannerImage
avatar {
medium
large
}
}
}
}
""".trimIndent()
return executeQuery<Query.RateReviewResponse>(query)
}
suspend fun toggleFollow(id: Int): Query.ToggleFollow? {
return executeQuery<Query.ToggleFollow>(
"""
mutation {
ToggleFollow(userId: $id) {
id
isFollowing
isFollower
}
}
""".trimIndent()
)
}
suspend fun toggleLike(id: Int, type: String): ToggleLike? {
return executeQuery<ToggleLike>(
"""
mutation Like {
ToggleLikeV2(id: $id, type: $type) {
__typename
}
}
""".trimIndent()
)
}
suspend fun postActivity(text: String, edit: Int? = null): String {
val encodedText = text.stringSanitizer()
val query = """
mutation {
SaveTextActivity(${if (edit != null) "id: $edit," else ""} text: $encodedText) {
siteUrl
}
}
""".trimIndent()
val result = executeQuery<JsonObject>(query)
val errors = result?.get("errors")
return errors?.toString() ?: (currContext()?.getString(ani.dantotsu.R.string.success)
?: "Success")
}
suspend fun postMessage(
userId: Int,
text: String,
edit: Int? = null,
isPrivate: Boolean = false
): String {
val encodedText = text.replace("", "").stringSanitizer()
val query = """
mutation {
SaveMessageActivity(
${if (edit != null) "id: $edit," else ""}
recipientId: $userId,
message: $encodedText,
private: $isPrivate
) {
id
}
}
""".trimIndent()
val result = executeQuery<JsonObject>(query)
val errors = result?.get("errors")
return errors?.toString() ?: (currContext()?.getString(ani.dantotsu.R.string.success)
?: "Success")
}
suspend fun postReply(activityId: Int, text: String, edit: Int? = null): String {
val encodedText = text.stringSanitizer()
val query = """
mutation {
SaveActivityReply(
${if (edit != null) "id: $edit," else ""}
activityId: $activityId,
text: $encodedText
) {
id
}
}
""".trimIndent()
val result = executeQuery<JsonObject>(query)
val errors = result?.get("errors")
return errors?.toString() ?: (currContext()?.getString(ani.dantotsu.R.string.success)
?: "Success")
}
suspend fun postReview(summary: String, body: String, mediaId: Int, score: Int): String {
val encodedSummary = summary.stringSanitizer()
val encodedBody = body.stringSanitizer()
val query = """
mutation {
SaveReview(
mediaId: $mediaId,
summary: $encodedSummary,
body: $encodedBody,
score: $score
) {
siteUrl
}
}
""".trimIndent()
val result = executeQuery<JsonObject>(query)
val errors = result?.get("errors")
return errors?.toString() ?: (currContext()?.getString(ani.dantotsu.R.string.success)
?: "Success")
}
suspend fun deleteActivityReply(activityId: Int): Boolean {
val query = """
mutation {
DeleteActivityReply(id: $activityId) {
deleted
}
}
""".trimIndent()
val result = executeQuery<JsonObject>(query)
val errors = result?.get("errors")
return errors == null
}
suspend fun deleteActivity(activityId: Int): Boolean {
val query = """
mutation {
DeleteActivity(id: $activityId) {
deleted
}
}
""".trimIndent()
val result = executeQuery<JsonObject>(query)
val errors = result?.get("errors")
return errors == null
}
private fun String.stringSanitizer(): String {
val sb = StringBuilder()
var i = 0
while (i < this.length) {
val codePoint = this.codePointAt(i)
if (codePoint > 0xFFFF) {
sb.append("&#").append(codePoint).append(";")
i += 2
} else {
sb.append(this[i])
i++
}
}
return Gson().toJson(sb.toString())
}
}

View File

@@ -5,47 +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.loadData
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 sharedPref = context.getSharedPreferences(
context.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
val token = sharedPref.getString("discord_token", null)
val userid = sharedPref.getString("discord_id", null)
if (userid == null && token != null) {
/*if (!Discord.getUserData())
snackString(context.getString(R.string.error_loading_discord_user_data))*/
//TODO: Discord.getUserData()
}
}
val anilist = if (Anilist.userid == null && Anilist.token != null) {
if (!Anilist.initialized && PrefManager.getVal<String>(PrefName.AnilistToken) != "") {
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() {
@@ -59,52 +46,77 @@ 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
suspend fun initUserStatus() {
val res = Anilist.query.getUserStatus()
res?.let { userStatus.postValue(it) }
}
private val hidden: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null)
fun getHidden(): LiveData<ArrayList<Media>> = hidden
suspend fun initHomePage() {
val res = Anilist.query.initHomePage()
res["currentAnime"]?.let { animeContinue.postValue(it) }
res["favoriteAnime"]?.let { animeFav.postValue(it) }
res["currentAnimePlanned"]?.let { animePlanned.postValue(it) }
res["currentManga"]?.let { mangaContinue.postValue(it) }
res["favoriteManga"]?.let { mangaFav.postValue(it) }
res["currentMangaPlanned"]?.let { mangaPlanned.postValue(it) }
res["recommendations"]?.let { recommendation.postValue(it) }
res["hidden"]?.let { hidden.postValue(it) }
}
suspend fun loadMain(context: FragmentActivity) {
Anilist.getSavedToken(context)
MAL.getSavedToken(context)
Discord.getSavedToken(context)
if (loadData<Boolean>("check_update") != false) AppUpdater.check(context)
genres.postValue(Anilist.query.getGenresAndTags(context))
Anilist.getSavedToken()
MAL.getSavedToken()
Discord.getSavedToken()
if (!BuildConfig.FLAVOR.contains("fdroid")) {
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
}
}
val empty = MutableLiveData<Boolean>(null)
@@ -116,7 +128,7 @@ class AnilistHomeViewModel : ViewModel() {
class AnilistAnimeViewModel : ViewModel() {
var searched = false
var notSet = true
lateinit var searchResults: SearchResults
lateinit var aniMangaSearchResults: AniMangaSearchResults
private val type = "ANIME"
private val trending: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
@@ -125,46 +137,44 @@ class AnilistAnimeViewModel : ViewModel() {
suspend fun loadTrending(i: Int) {
val (season, year) = Anilist.currentSeasons[i]
trending.postValue(
Anilist.query.search(
Anilist.query.searchAniManga(
type,
perPage = 12,
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<AniMangaSearchResults?>(null)
private val animePopular = MutableLiveData<SearchResults?>(null)
fun getPopular(): LiveData<SearchResults?> = animePopular
fun getPopular(): LiveData<AniMangaSearchResults?> = 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,
) {
animePopular.postValue(
Anilist.query.search(
Anilist.query.searchAniManga(
type,
search = search_val,
search = searchVal,
onList = if (onList) null else false,
sort = sort,
genres = genres
genres = genres,
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
)
)
}
suspend fun loadNextPage(r: SearchResults) = animePopular.postValue(
Anilist.query.search(
suspend fun loadNextPage(r: AniMangaSearchResults) = animePopular.postValue(
Anilist.query.searchAniManga(
r.type,
r.page + 1,
r.perPage,
@@ -172,19 +182,49 @@ 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() {
var searched = false
var notSet = true
lateinit var searchResults: SearchResults
lateinit var aniMangaSearchResults: AniMangaSearchResults
private val type = "MANGA"
private val trending: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null)
@@ -192,51 +232,40 @@ class AnilistMangaViewModel : ViewModel() {
fun getTrending(): LiveData<MutableList<Media>> = trending
suspend fun loadTrending() =
trending.postValue(
Anilist.query.search(
Anilist.query.searchAniManga(
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
private val mangaPopular = MutableLiveData<AniMangaSearchResults?>(null)
fun getPopular(): LiveData<AniMangaSearchResults?> = 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,
) {
mangaPopular.postValue(
Anilist.query.search(
Anilist.query.searchAniManga(
type,
search = search_val,
search = searchVal,
onList = if (onList) null else false,
sort = sort,
genres = genres
genres = genres,
adultOnly = PrefManager.getVal(PrefName.AdultOnly)
)
)
}
suspend fun loadNextPage(r: SearchResults) = mangaPopular.postValue(
Anilist.query.search(
suspend fun loadNextPage(r: AniMangaSearchResults) = mangaPopular.postValue(
Anilist.query.searchAniManga(
r.type,
r.page + 1,
r.perPage,
@@ -244,28 +273,183 @@ 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() {
enum class SearchType {
ANIME, MANGA, CHARACTER, STAFF, STUDIO, USER;
companion object {
fun SearchType.toAnilistString(): String {
return when (this) {
ANIME -> "ANIME"
MANGA -> "MANGA"
CHARACTER -> "CHARACTER"
STAFF -> "STAFF"
STUDIO -> "STUDIO"
USER -> "USER"
}
}
fun fromString(string: String): SearchType {
return when (string.uppercase()) {
"ANIME" -> ANIME
"MANGA" -> MANGA
"CHARACTER" -> CHARACTER
"STAFF" -> STAFF
"STUDIO" -> STUDIO
"USER" -> USER
else -> throw IllegalArgumentException("Invalid search type")
}
}
}
}
var searched = false
var notSet = true
lateinit var searchResults: SearchResults
private val result: MutableLiveData<SearchResults?> = MutableLiveData<SearchResults?>(null)
lateinit var aniMangaSearchResults: AniMangaSearchResults
private val aniMangaResult: MutableLiveData<AniMangaSearchResults?> =
MutableLiveData<AniMangaSearchResults?>(null)
fun getSearch(): LiveData<SearchResults?> = result
suspend fun loadSearch(r: SearchResults) = result.postValue(
Anilist.query.search(
lateinit var characterSearchResults: CharacterSearchResults
private val characterResult: MutableLiveData<CharacterSearchResults?> =
MutableLiveData<CharacterSearchResults?>(null)
lateinit var studioSearchResults: StudioSearchResults
private val studioResult: MutableLiveData<StudioSearchResults?> =
MutableLiveData<StudioSearchResults?>(null)
lateinit var staffSearchResults: StaffSearchResults
private val staffResult: MutableLiveData<StaffSearchResults?> =
MutableLiveData<StaffSearchResults?>(null)
lateinit var userSearchResults: UserSearchResults
private val userResult: MutableLiveData<UserSearchResults?> =
MutableLiveData<UserSearchResults?>(null)
fun <T> getSearch(type: SearchType): MutableLiveData<T?> {
return when (type) {
SearchType.ANIME, SearchType.MANGA -> aniMangaResult as MutableLiveData<T?>
SearchType.CHARACTER -> characterResult as MutableLiveData<T?>
SearchType.STUDIO -> studioResult as MutableLiveData<T?>
SearchType.STAFF -> staffResult as MutableLiveData<T?>
SearchType.USER -> userResult as MutableLiveData<T?>
}
}
suspend fun loadSearch(type: SearchType) {
when (type) {
SearchType.ANIME, SearchType.MANGA -> loadAniMangaSearch(aniMangaSearchResults)
SearchType.CHARACTER -> loadCharacterSearch(characterSearchResults)
SearchType.STUDIO -> loadStudiosSearch(studioSearchResults)
SearchType.STAFF -> loadStaffSearch(staffSearchResults)
SearchType.USER -> loadUserSearch(userSearchResults)
}
}
suspend fun loadNextPage(type: SearchType) {
when (type) {
SearchType.ANIME, SearchType.MANGA -> loadNextAniMangaPage(aniMangaSearchResults)
SearchType.CHARACTER -> loadNextCharacterPage(characterSearchResults)
SearchType.STUDIO -> loadNextStudiosPage(studioSearchResults)
SearchType.STAFF -> loadNextStaffPage(staffSearchResults)
SearchType.USER -> loadNextUserPage(userSearchResults)
}
}
fun hasNextPage(type: SearchType): Boolean {
return when (type) {
SearchType.ANIME, SearchType.MANGA -> aniMangaSearchResults.hasNextPage
SearchType.CHARACTER -> characterSearchResults.hasNextPage
SearchType.STUDIO -> studioSearchResults.hasNextPage
SearchType.STAFF -> staffSearchResults.hasNextPage
SearchType.USER -> userSearchResults.hasNextPage
}
}
fun resultsIsNotEmpty(type: SearchType): Boolean {
return when (type) {
SearchType.ANIME, SearchType.MANGA -> aniMangaSearchResults.results.isNotEmpty()
SearchType.CHARACTER -> characterSearchResults.results.isNotEmpty()
SearchType.STUDIO -> studioSearchResults.results.isNotEmpty()
SearchType.STAFF -> staffSearchResults.results.isNotEmpty()
SearchType.USER -> userSearchResults.results.isNotEmpty()
}
}
fun size(type: SearchType): Int {
return when (type) {
SearchType.ANIME, SearchType.MANGA -> aniMangaSearchResults.results.size
SearchType.CHARACTER -> characterSearchResults.results.size
SearchType.STUDIO -> studioSearchResults.results.size
SearchType.STAFF -> staffSearchResults.results.size
SearchType.USER -> userSearchResults.results.size
}
}
fun clearResults(type: SearchType) {
when (type) {
SearchType.ANIME, SearchType.MANGA -> aniMangaSearchResults.results.clear()
SearchType.CHARACTER -> characterSearchResults.results.clear()
SearchType.STUDIO -> studioSearchResults.results.clear()
SearchType.STAFF -> staffSearchResults.results.clear()
SearchType.USER -> userSearchResults.results.clear()
}
}
private suspend fun loadAniMangaSearch(r: AniMangaSearchResults) = aniMangaResult.postValue(
Anilist.query.searchAniManga(
r.type,
r.page,
r.perPage,
@@ -273,18 +457,50 @@ 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,
)
)
suspend fun loadNextPage(r: SearchResults) = result.postValue(
Anilist.query.search(
private suspend fun loadCharacterSearch(r: CharacterSearchResults) = characterResult.postValue(
Anilist.query.searchCharacters(
r.page,
r.search,
)
)
private suspend fun loadStudiosSearch(r: StudioSearchResults) = studioResult.postValue(
Anilist.query.searchStudios(
r.page,
r.search,
)
)
private suspend fun loadStaffSearch(r: StaffSearchResults) = staffResult.postValue(
Anilist.query.searchStaff(
r.page,
r.search,
)
)
private suspend fun loadUserSearch(r: UserSearchResults) = userResult.postValue(
Anilist.query.searchUsers(
r.page,
r.search,
)
)
private suspend fun loadNextAniMangaPage(r: AniMangaSearchResults) = aniMangaResult.postValue(
Anilist.query.searchAniManga(
r.type,
r.page + 1,
r.perPage,
@@ -292,15 +508,48 @@ 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
)
)
private suspend fun loadNextCharacterPage(r: CharacterSearchResults) =
characterResult.postValue(
Anilist.query.searchCharacters(
r.page + 1,
r.search,
)
)
private suspend fun loadNextStudiosPage(r: StudioSearchResults) = studioResult.postValue(
Anilist.query.searchStudios(
r.page + 1,
r.search,
)
)
private suspend fun loadNextStaffPage(r: StaffSearchResults) = staffResult.postValue(
Anilist.query.searchStaff(
r.page + 1,
r.search,
)
)
private suspend fun loadNextUserPage(r: UserSearchResults) = userResult.postValue(
Anilist.query.searchUsers(
r.page + 1,
r.search,
)
)
}
class GenresViewModel : ViewModel() {
@@ -320,4 +569,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

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

View File

@@ -5,27 +5,31 @@ import android.net.Uri
import android.os.Bundle
import androidx.core.os.bundleOf
import ani.dantotsu.loadMedia
import ani.dantotsu.others.LangSet
import ani.dantotsu.startMainActivity
import ani.dantotsu.themes.ThemeManager
class UrlMedia : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
LangSet.setLocale(this)
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

@@ -0,0 +1,431 @@
package ani.dantotsu.connections.anilist
val standardPageInformation = """
pageInfo {
total
perPage
currentPage
lastPage
hasNextPage
}
""".prepare()
fun String.prepare() = this.trimIndent().replace("\n", " ").replace(""" """, "")
fun characterInformation(includeMediaInfo: Boolean) = """
id
name {
first
middle
last
full
native
userPreferred
}
image {
large
medium
}
age
gender
description
dateOfBirth {
year
month
day
}
${
if (includeMediaInfo) """
media(page: 0,sort:[POPULARITY_DESC,SCORE_DESC]) {
$standardPageInformation
edges {
id
voiceActors {
id,
name {
userPreferred
}
languageV2,
image {
medium,
large
}
}
characterRole
node {
id
idMal
isAdult
status
chapters
episodes
nextAiringEpisode { episode }
type
meanScore
isFavourite
format
bannerImage
countryOfOrigin
coverImage { large }
title {
english
romaji
userPreferred
}
mediaListEntry {
progress
private
score(format: POINT_100)
status
}
}
}
}""".prepare() else ""
}
""".prepare()
fun studioInformation(page: Int, perPage: Int) = """
id
name
isFavourite
favourites
media(page: $page, sort:START_DATE_DESC, perPage: $perPage) {
$standardPageInformation
edges {
id
node {
id
idMal
isAdult
status
chapters
episodes
nextAiringEpisode { episode }
type
meanScore
startDate{ year }
isFavourite
format
bannerImage
countryOfOrigin
coverImage { large }
title {
english
romaji
userPreferred
}
mediaListEntry {
progress
private
score(format: POINT_100)
status
}
}
}
}
""".prepare()
fun staffInformation(page: Int, perPage: Int) = """
id
name {
first
middle
last
full
native
userPreferred
}
image {
large
medium
}
dateOfBirth {
year
month
day
}
dateOfDeath {
year
month
day
}
age
yearsActive
homeTown
staffMedia(page: $page,sort:START_DATE_DESC, perPage: $perPage) {
$standardPageInformation
edges {
staffRole
id
node {
id
idMal
isAdult
status
chapters
episodes
nextAiringEpisode { episode }
type
meanScore
startDate{ year }
isFavourite
format
bannerImage
countryOfOrigin
coverImage { large }
title {
english
romaji
userPreferred
}
mediaListEntry {
progress
private
score(format: POINT_100)
status
}
}
}
}
""".prepare()
fun userInformation() = """
id
name
about(asHtml: true)
avatar {
large
medium
}
bannerImage
isFollowing
isFollower
isBlocked
siteUrl
""".prepare()
fun aniMangaSearch(perPage: Int?) = """
query (${"$"}page: Int = 1, ${"$"}id: Int, ${"$"}type: MediaType, ${"$"}isAdult: Boolean = false, ${"$"}search: String, ${"$"}format: [MediaFormat], ${"$"}status: MediaStatus, ${"$"}countryOfOrigin: CountryCode, ${"$"}source: MediaSource, ${"$"}season: MediaSeason, ${"$"}seasonYear: Int, ${"$"}year: String, ${"$"}onList: Boolean, ${"$"}yearLesser: FuzzyDateInt, ${"$"}yearGreater: FuzzyDateInt, ${"$"}episodeLesser: Int, ${"$"}episodeGreater: Int, ${"$"}durationLesser: Int, ${"$"}durationGreater: Int, ${"$"}chapterLesser: Int, ${"$"}chapterGreater: Int, ${"$"}volumeLesser: Int, ${"$"}volumeGreater: Int, ${"$"}licensedBy: [String], ${"$"}isLicensed: Boolean, ${"$"}genres: [String], ${"$"}excludedGenres: [String], ${"$"}tags: [String], ${"$"}excludedTags: [String], ${"$"}minimumTagRank: Int, ${"$"}sort: [MediaSort] = [POPULARITY_DESC, SCORE_DESC, START_DATE_DESC]) {
Page(page: ${"$"}page, perPage: ${perPage ?: 50}) {
$standardPageInformation
media(id: ${"$"}id, type: ${"$"}type, season: ${"$"}season, format_in: ${"$"}format, status: ${"$"}status, countryOfOrigin: ${"$"}countryOfOrigin, source: ${"$"}source, search: ${"$"}search, onList: ${"$"}onList, seasonYear: ${"$"}seasonYear, startDate_like: ${"$"}year, startDate_lesser: ${"$"}yearLesser, startDate_greater: ${"$"}yearGreater, episodes_lesser: ${"$"}episodeLesser, episodes_greater: ${"$"}episodeGreater, duration_lesser: ${"$"}durationLesser, duration_greater: ${"$"}durationGreater, chapters_lesser: ${"$"}chapterLesser, chapters_greater: ${"$"}chapterGreater, volumes_lesser: ${"$"}volumeLesser, volumes_greater: ${"$"}volumeGreater, licensedBy_in: ${"$"}licensedBy, isLicensed: ${"$"}isLicensed, genre_in: ${"$"}genres, genre_not_in: ${"$"}excludedGenres, tag_in: ${"$"}tags, tag_not_in: ${"$"}excludedTags, minimumTagRank: ${"$"}minimumTagRank, sort: ${"$"}sort, isAdult: ${"$"}isAdult) {
${standardMediaInformation()}
}
}
}
""".prepare()
fun standardMediaInformation() = """
id
idMal
siteUrl
isAdult
status(version: 2)
chapters
episodes
nextAiringEpisode {
episode
airingAt
}
type
genres
meanScore
popularity
favourites
isFavourite
format
bannerImage
countryOfOrigin
coverImage {
large
extraLarge
}
title {
english
romaji
userPreferred
}
mediaListEntry {
progress
private
score(format: POINT_100)
status
}
""".prepare()
fun fullMediaInformation(id: Int) = """
{
Media(id: $id) {
streamingEpisodes {
title
thumbnail
url
site
}
mediaListEntry {
id
status
score(format: POINT_100)
progress
private
notes
repeat
customLists
updatedAt
startedAt {
year
month
day
}
completedAt {
year
month
day
}
}
reviews(perPage: 3, sort: SCORE_DESC) {
nodes {
id
mediaId
mediaType
summary
body(asHtml: true)
rating
ratingAmount
userRating
score
private
siteUrl
createdAt
updatedAt
user {
id
name
bannerImage
avatar {
medium
large
}
}
}
}
${standardMediaInformation()}
source
duration
season
seasonYear
startDate {
year
month
day
}
endDate {
year
month
day
}
studios(isMain: true) {
nodes {
id
name
siteUrl
}
}
description
trailer {
site
id
}
synonyms
tags {
name
rank
isMediaSpoiler
}
characters(sort: [ROLE, FAVOURITES_DESC], perPage: 25, page: 1) {
edges {
role
voiceActors {
id
name {
first
middle
last
full
native
userPreferred
}
image {
large
medium
}
languageV2
}
node {
id
image {
medium
}
name {
userPreferred
}
isFavourite
}
}
}
relations {
edges {
relationType(version: 2)
node {
${standardMediaInformation()}
}
}
}
staffPreview: staff(perPage: 8, sort: [RELEVANCE, ID]) {
edges {
role
node {
id
image {
large
medium
}
name {
userPreferred
}
}
}
}
recommendations(sort: RATING_DESC) {
nodes {
mediaRecommendation {
${standardMediaInformation()}
}
}
}
externalLinks {
url
site
}
}
Page(page: 1) {
$standardPageInformation
mediaList(isFollowing: true, sort: [STATUS], mediaId: $id) {
id
status
score(format: POINT_100)
progress
progressVolumes
user {
id
name
avatar {
large
medium
}
}
}
}
}
""".prepare()

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?
)
}
@@ -105,41 +107,677 @@ class Query {
)
}
@Serializable
data class CombinedMediaListResponse(
@SerialName("data")
val data: Data?
) {
@Serializable
data class Data(
@SerialName("current") val current: ani.dantotsu.connections.anilist.api.MediaListCollection?,
@SerialName("planned") val planned: ani.dantotsu.connections.anilist.api.MediaListCollection?,
@SerialName("repeating") val repeating: ani.dantotsu.connections.anilist.api.MediaListCollection?,
)
}
@Serializable
data class HomePageMedia(
@SerialName("data")
val data: Data?
) {
@Serializable
data class Data(
@SerialName("currentAnime") val currentAnime: ani.dantotsu.connections.anilist.api.MediaListCollection?,
@SerialName("repeatingAnime") val repeatingAnime: ani.dantotsu.connections.anilist.api.MediaListCollection?,
@SerialName("favoriteAnime") val favoriteAnime: ani.dantotsu.connections.anilist.api.User?,
@SerialName("plannedAnime") val plannedAnime: ani.dantotsu.connections.anilist.api.MediaListCollection?,
@SerialName("currentManga") val currentManga: ani.dantotsu.connections.anilist.api.MediaListCollection?,
@SerialName("repeatingManga") val repeatingManga: ani.dantotsu.connections.anilist.api.MediaListCollection?,
@SerialName("favoriteManga") val favoriteManga: ani.dantotsu.connections.anilist.api.User?,
@SerialName("plannedManga") val plannedManga: ani.dantotsu.connections.anilist.api.MediaListCollection?,
@SerialName("recommendationQuery") val recommendationQuery: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("recommendationPlannedQueryAnime") val recommendationPlannedQueryAnime: ani.dantotsu.connections.anilist.api.MediaListCollection?,
@SerialName("recommendationPlannedQueryManga") val recommendationPlannedQueryManga: ani.dantotsu.connections.anilist.api.MediaListCollection?,
@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("trendingMovies") val trendingMovies: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("topRated") val topRated: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("mostFav") val mostFav: 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("trendingManhwa") val trendingManhwa: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingNovel") val trendingNovel: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("topRated") val topRated: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("mostFav") val mostFav: 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 ReviewsResponse(
@SerialName("data")
val data: Data
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("Page")
val page: ReviewPage?
) : java.io.Serializable
}
@Serializable
data class ReviewPage(
@SerialName("pageInfo")
val pageInfo: PageInfo,
@SerialName("reviews")
val reviews: List<Review>?
) : java.io.Serializable
@Serializable
data class RateReviewResponse(
@SerialName("data")
val data: Data
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("RateReview")
val rateReview: Review
) : java.io.Serializable
}
@Serializable
data class Review(
@SerialName("id")
val id: Int,
@SerialName("mediaId")
val mediaId: Int,
@SerialName("mediaType")
val mediaType: String,
@SerialName("summary")
val summary: String,
@SerialName("body")
val body: String,
@SerialName("rating")
var rating: Int,
@SerialName("ratingAmount")
var ratingAmount: Int,
@SerialName("userRating")
var userRating: String,
@SerialName("score")
val score: Int,
@SerialName("private")
val private: Boolean,
@SerialName("siteUrl")
val siteUrl: String,
@SerialName("createdAt")
val createdAt: Int,
@SerialName("updatedAt")
val updatedAt: Int?,
@SerialName("user")
val user: 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(
@@ -169,7 +807,7 @@ class Query {
// // Activity reply query
// val ActivityReply: ActivityReply?,
// // Comment query
// // CommentNotificationWorker query
// val ThreadComment: List<ThreadComment>?,
// // Notification query

View File

@@ -0,0 +1,132 @@
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 ReplyResponse(
@SerialName("data")
val data: Data
) : java.io.Serializable {
@Serializable
data class Data(
@SerialName("Page")
val page: ReplyPage
) : java.io.Serializable
}
@Serializable
data class ReplyPage(
@SerialName("activityReplies")
val activityReplies: List<ActivityReply>
) : 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

@@ -143,7 +143,7 @@ data class Media(
@SerialName("externalLinks") var externalLinks: List<MediaExternalLink>?,
// Data and links to legal streaming episodes on external sites
// @SerialName("streamingEpisodes") var streamingEpisodes: List<MediaStreamingEpisode>?,
@SerialName("streamingEpisodes") var streamingEpisodes: List<MediaStreamingEpisode>?,
// The ranking of the media in a particular time span and format compared to other media
// @SerialName("rankings") var rankings: List<MediaRank>?,
@@ -152,7 +152,7 @@ data class Media(
@SerialName("mediaListEntry") var mediaListEntry: MediaList?,
// User reviews of the media
// @SerialName("reviews") var reviews: ReviewConnection?,
@SerialName("reviews") var reviews: ReviewConnection?,
// User recommendations for similar media
@SerialName("recommendations") var recommendations: RecommendationConnection?,
@@ -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("_", " ")
}
}
@@ -238,6 +240,21 @@ data class AiringSchedule(
@SerialName("media") var media: Media?,
)
@Serializable
data class MediaStreamingEpisode(
// The title of the episode
@SerialName("title") var title: String?,
// The thumbnail image of the episode
@SerialName("thumbnail") var thumbnail: String?,
// The url of the episode
@SerialName("url") var url: String?,
// The site location of the streaming episode
@SerialName("site") var site: String?,
) : java.io.Serializable
@Serializable
data class MediaCoverImage(
// The cover image url of the media at its largest size. If this size isn't available, large will be provided instead.
@@ -251,7 +268,7 @@ data class MediaCoverImage(
// Average #hex color of cover image
@SerialName("color") var color: String?,
)
) : java.io.Serializable
@Serializable
data class MediaList(
@@ -431,7 +448,7 @@ data class MediaEdge(
@SerialName("staffRole") var staffRole: 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>?,
@@ -445,17 +462,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 +510,7 @@ data class MediaExternalLink(
// isDisabled: Boolean
@SerialName("notes") var notes: String?,
)
) : java.io.Serializable
@Serializable
enum class ExternalLinkType {
@@ -512,7 +532,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 +552,9 @@ data class MediaListGroup(
@SerialName("isSplitCompletedList") var isSplitCompletedList: Boolean?,
@SerialName("status") var status: MediaListStatus?,
) : java.io.Serializable
@Serializable
data class ReviewConnection(
@SerialName("nodes") var nodes: List<Query.Review>?,
)

View File

@@ -0,0 +1,140 @@
package ani.dantotsu.connections.anilist.api
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.util.Locale
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"),
COMMENT_WARNING("COMMENT_WARNING"),
DANTOTSU_UPDATE("DANTOTSU_UPDATE"),
SUBSCRIPTION("SUBSCRIPTION");
fun toFormattedString(): String {
return this.value.replace("_", " ").lowercase(Locale.ROOT)
.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.ROOT) else it.toString() }
}
companion object {
fun String.fromFormattedString(): String {
return this.replace(" ", "_").uppercase(Locale.ROOT)
}
}
}
@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,
val image: String? = null,
val banner: String? = 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,12 +69,12 @@ data class User(
// The user's previously used names.
// @SerialName("previousNames") var previousNames: List<UserPreviousName>?,
)
) : java.io.Serializable
@Serializable
data class UserOptions(
// The language the user wants to see media titles in
// @SerialName("titleLanguage") var titleLanguage: UserTitleLanguage?,
@SerialName("titleLanguage") var titleLanguage: UserTitleLanguage?,
// Whether the user has enabled viewing of 18+ content
@SerialName("displayAdultContent") var displayAdultContent: Boolean?,
@@ -88,17 +88,17 @@ data class UserOptions(
// // Notification options
// // @SerialName("notificationOptions") var notificationOptions: List<NotificationOption>?,
//
// // The user's timezone offset (Auth user only)
// @SerialName("timezone") var timezone: String?,
// The user's timezone offset (Auth user only)
@SerialName("timezone") var timezone: String?,
//
// // Minutes between activity for them to be merged together. 0 is Never, Above 2 weeks (20160 mins) is Always.
// @SerialName("activityMergeTime") var activityMergeTime: Int?,
// Minutes between activity for them to be merged together. 0 is Never, Above 2 weeks (20160 mins) is Always.
@SerialName("activityMergeTime") var activityMergeTime: Int?,
//
// // The language the user wants to see staff and character names in
// // @SerialName("staffNameLanguage") var staffNameLanguage: UserStaffNameLanguage?,
// The language the user wants to see staff and character names in
@SerialName("staffNameLanguage") var staffNameLanguage: UserStaffNameLanguage?,
//
// // Whether the user only allow messages from users they follow
// @SerialName("restrictMessagesToFollowing") var restrictMessagesToFollowing: Boolean?,
// Whether the user only allow messages from users they follow
@SerialName("restrictMessagesToFollowing") var restrictMessagesToFollowing: Boolean?,
// The list activity types the user has disabled from being created from list updates
// @SerialName("disabledListActivity") var disabledListActivity: List<ListActivityOption>?,
@@ -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(
@@ -119,6 +119,48 @@ data class UserStatisticTypes(
@SerialName("manga") var manga: UserStatistics?
)
@Serializable
enum class UserTitleLanguage {
@SerialName("ENGLISH")
ENGLISH,
@SerialName("ROMAJI")
ROMAJI,
@SerialName("NATIVE")
NATIVE
}
@Serializable
enum class UserStaffNameLanguage {
@SerialName("ROMAJI_WESTERN")
ROMAJI_WESTERN,
@SerialName("ROMAJI")
ROMAJI,
@SerialName("NATIVE")
NATIVE
}
@Serializable
enum class ScoreFormat {
@SerialName("POINT_100")
POINT_100,
@SerialName("POINT_10_DECIMAL")
POINT_10_DECIMAL,
@SerialName("POINT_10")
POINT_10,
@SerialName("POINT_5")
POINT_5,
@SerialName("POINT_3")
POINT_3,
}
@Serializable
data class UserStatistics(
//
@@ -164,7 +206,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: ScoreFormat?,
// The default order list rows should be displayed in
@SerialName("rowOrder") var rowOrder: String?,
@@ -181,8 +223,8 @@ data class MediaListTypeOptions(
// The order each list should be displayed in
@SerialName("sectionOrder") var sectionOrder: List<String>?,
// If the completed sections of the list should be separated by format
@SerialName("splitCompletedSectionByFormat") var splitCompletedSectionByFormat: Boolean?,
// // If the completed sections of the list should be separated by format
// @SerialName("splitCompletedSectionByFormat") var splitCompletedSectionByFormat: Boolean?,
// The names of the user's custom lists
@SerialName("customLists") var customLists: List<String>?,

View File

@@ -0,0 +1,595 @@
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 API_ADDRESS: String = "https://api.dantotsu.app"
private const val LOCAL_HOST: String = "https://127.0.0.1"
private var isOnline: Boolean = true
private var commentsEnabled = PrefManager.getVal<Int>(PrefName.CommentsEnabled) == 1
private val ADDRESS: String get() = if (commentsEnabled) API_ADDRESS else LOCAL_HOST
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) {
if (commentsEnabled) Logger.log(reason)
if (isOnline && commentsEnabled) 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
}
fun requestBuilder(client: OkHttpClient = Injekt.get<NetworkHelper>().client): Requests {
return Requests(
client,
headerBuilder()
)
}
private fun errorReason(code: Int, reason: String? = null) {
val error = when (code) {
429 -> "Rate limited. :("
else -> "Failed to connect"
}
val parsed = try {
Json.decodeFromString<ErrorResponse>(reason!!)
} catch (e: Exception) {
null
}
val message = parsed?.message ?: reason ?: error
val fullMessage = if (code == 500) message else "$code: $message"
toast(fullMessage)
}
}
@Serializable
data class ErrorResponse(
@SerialName("message")
val message: String
)
@Serializable
data class NotificationResponse(
@SerialName("notifications")
val notifications: List<Notification>
)
@Serializable
data class Notification(
@SerialName("username")
val username: String,
@SerialName("media_id")
val mediaId: Int,
@SerialName("comment_id")
val commentId: Int,
@SerialName("type")
val type: Int? = null,
@SerialName("content")
val content: String? = null,
@SerialName("notification_id")
val notificationId: Int
)
@Serializable
data class AuthResponse(
@SerialName("authToken")
val authToken: String,
@SerialName("user")
val user: User
) : java.io.Serializable {
companion object {
private const val serialVersionUID: Long = 1
}
}
@Serializable
data class UserResponse(
@SerialName("user")
val user: User
)
@Serializable
data class User(
@SerialName("user_id")
val id: String,
@SerialName("username")
val username: String,
@SerialName("profile_picture_url")
val profilePictureUrl: String? = null,
@SerialName("is_banned")
@Serializable(with = NumericBooleanSerializer::class)
val isBanned: Boolean? = null,
@SerialName("is_mod")
@Serializable(with = NumericBooleanSerializer::class)
val isAdmin: Boolean? = null,
@SerialName("is_admin")
@Serializable(with = NumericBooleanSerializer::class)
val isMod: Boolean? = null,
@SerialName("total_votes")
val totalVotes: Int,
@SerialName("warnings")
val warnings: Int
) : java.io.Serializable {
companion object {
private const val serialVersionUID: Long = 1
}
}
@Serializable
data class CommentResponse(
@SerialName("comments")
val comments: List<Comment>,
@SerialName("totalPages")
val totalPages: Int
)
@Serializable
data class Comment(
@SerialName("comment_id")
val commentId: Int,
@SerialName("user_id")
val userId: String,
@SerialName("media_id")
val mediaId: Int,
@SerialName("parent_comment_id")
val parentCommentId: Int?,
@SerialName("content")
var content: String,
@SerialName("timestamp")
var timestamp: String,
@SerialName("deleted")
@Serializable(with = NumericBooleanSerializer::class)
val deleted: Boolean?,
@SerialName("tag")
val tag: Int?,
@SerialName("upvotes")
var upvotes: Int,
@SerialName("downvotes")
var downvotes: Int,
@SerialName("user_vote_type")
var userVoteType: Int?,
@SerialName("username")
val username: String,
@SerialName("profile_picture_url")
val profilePictureUrl: String?,
@SerialName("is_mod")
@Serializable(with = NumericBooleanSerializer::class)
val isMod: Boolean? = null,
@SerialName("is_admin")
@Serializable(with = NumericBooleanSerializer::class)
val isAdmin: Boolean? = null,
@SerialName("reply_count")
val replyCount: Int? = null,
@SerialName("total_votes")
val totalVotes: Int
)
@Serializable
data class ReturnedComment(
@SerialName("id")
var id: Int,
@SerialName("comment_id")
var commentId: Int?,
@SerialName("user_id")
val userId: String,
@SerialName("media_id")
val mediaId: Int,
@SerialName("parent_comment_id")
val parentCommentId: Int? = null,
@SerialName("content")
val content: String,
@SerialName("timestamp")
val timestamp: String,
@SerialName("deleted")
@Serializable(with = NumericBooleanSerializer::class)
val deleted: Boolean?,
@SerialName("tag")
val tag: Int? = null,
)
object NumericBooleanSerializer : KSerializer<Boolean> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("NumericBoolean", PrimitiveKind.INT)
override fun serialize(encoder: Encoder, value: Boolean) {
encoder.encodeInt(if (value) 1 else 0)
}
override fun deserialize(decoder: Decoder): Boolean {
return decoder.decodeInt() != 0
}
}

View File

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

View File

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

View File

@@ -3,9 +3,10 @@ package ani.dantotsu.connections.discord
import android.content.Context
import android.content.Intent
import android.widget.TextView
import androidx.core.content.edit
import ani.dantotsu.R
import ani.dantotsu.others.CustomBottomDialog
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.toast
import ani.dantotsu.tryWith
import io.noties.markwon.Markwon
@@ -18,37 +19,20 @@ object Discord {
var userid: String? = null
var avatar: String? = null
const val TOKEN = "discord_token"
fun getSavedToken(context: Context): Boolean {
val sharedPref = context.getSharedPreferences(
context.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
fun getSavedToken(): Boolean {
token = PrefManager.getVal(
PrefName.DiscordToken, null as String?
)
token = sharedPref.getString(TOKEN, null)
return token != null
}
fun saveToken(context: Context, token: String) {
val sharedPref = context.getSharedPreferences(
context.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
sharedPref.edit {
putString(TOKEN, token)
commit()
}
fun saveToken(token: String) {
PrefManager.setVal(PrefName.DiscordToken, token)
}
fun removeSavedToken(context: Context) {
val sharedPref = context.getSharedPreferences(
context.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
sharedPref.edit {
remove(TOKEN)
commit()
}
PrefManager.removeVal(PrefName.DiscordToken)
tryWith(true) {
val dir = File(context.filesDir?.parentFile, "app_webview")
@@ -86,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/9NqpMxXs4ZNQtMG42L7hqINW92GqqDxgxS9Oh0Sp880/%3Fsize%3D48%26quality%3Dlossless%26name%3DDantotsu/https/cdn.discordapp.com/emojis/1167344924874784828.gif"
const val small_Image_AniList: String =
"https://anilist.co/img/icons/android-chrome-512x512.png"
}

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
@@ -24,6 +19,9 @@ import ani.dantotsu.R
import ani.dantotsu.connections.discord.serializers.Presence
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
@@ -35,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
@@ -47,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
@@ -65,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(
@@ -149,27 +147,19 @@ class DiscordService : Service() {
}
fun saveProfile(response: String) {
val sharedPref = baseContext.getSharedPreferences(
baseContext.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
val user = json.decodeFromString<User.Response>(response).d.user
log("User data: $user")
with(sharedPref.edit()) {
putString("discord_username", user.username)
putString("discord_id", user.id)
putString("discord_avatar", user.avatar)
apply()
}
PrefManager.setVal(PrefName.DiscordUserName, user.username)
PrefManager.setVal(PrefName.DiscordId, user.id)
PrefManager.setVal(PrefName.DiscordAvatar, user.avatar)
}
override fun onBind(p0: Intent?): IBinder? = null
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
@@ -238,7 +228,7 @@ class DiscordService : Service() {
resume()
resume = false
} else {
identify(webSocket, baseContext)
identify(webSocket)
log("WebSocket: Identified")
}
}
@@ -251,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()
@@ -276,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(
@@ -295,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()
}
@@ -303,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()
@@ -317,18 +307,14 @@ class DiscordService : Service() {
}
}
fun getToken(context: Context): String {
val sharedPref = context.getSharedPreferences(
context.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
val token = sharedPref.getString(Discord.TOKEN, null)
if (token == null) {
fun getToken(): String {
val token = PrefManager.getVal(PrefName.DiscordToken, null as String?)
return if (token == null) {
log("WebSocket: Token not found")
errorNotification("Could not set the presence", "token not found")
return ""
""
} else {
return token
token
}
}
@@ -359,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
@@ -385,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()
@@ -459,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

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

@@ -1,22 +1,19 @@
package ani.dantotsu.connections.discord
import ani.dantotsu.connections.discord.Discord.token
import ani.dantotsu.connections.discord.serializers.Activity
import ani.dantotsu.connections.discord.serializers.Presence
import kotlinx.serialization.Serializable
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit.SECONDS
import kotlin.coroutines.CoroutineContext
import ani.dantotsu.client as app
@Suppress("MemberVisibilityCanBePrivate")
open class RPC(val token: String, val coroutineContext: CoroutineContext) {
private val json = Json {
encodeDefaults = true
allowStructuredMapKeys = true
ignoreUnknownKeys = true
}
enum class Type {
PLAYING, STREAMING, LISTENING, WATCHING, COMPETING
}
@@ -25,7 +22,7 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
companion object {
data class RPCData(
val applicationId: String? = null,
val applicationId: String,
val type: Type? = null,
val activityName: String? = null,
val details: String? = null,
@@ -38,24 +35,24 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
val buttons: MutableList<Link> = mutableListOf()
)
@Serializable
data class KizzyApi(val id: String)
val api = "https://kizzy-api.vercel.app/image?url="
private suspend fun String.discordUrl(): String? {
if (startsWith("mp:")) return this
val json = app.get("$api$this").parsedSafe<KizzyApi>()
return json?.id
}
suspend fun createPresence(data: RPCData): String {
val json = Json {
encodeDefaults = true
allowStructuredMapKeys = true
ignoreUnknownKeys = true
}
return json.encodeToString(Presence.Response(
3,
val client = OkHttpClient.Builder()
.connectTimeout(10, SECONDS)
.readTimeout(10, SECONDS)
.writeTimeout(10, SECONDS)
.build()
val assetApi = RPCExternalAsset(data.applicationId, token!!, client, json)
suspend fun String.discordUrl() = assetApi.getDiscordUri(this)
return json.encodeToString(
Presence.Response(
3,
Presence(
activities = listOf(
Activity(
@@ -69,8 +66,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 +78,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,61 @@
// this code was kanged from the greatest mind of this era, aka shivam brahmkshatriya
// please subscribe to my only fans here: https://github.com/brahmkshatriya
package ani.dantotsu.connections.discord
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.Call
import okhttp3.Callback
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okio.IOException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
class RPCExternalAsset(
applicationId: String,
private val token: String,
private val client: OkHttpClient,
private val json: Json
) {
@Serializable
data class ExternalAsset(
val url: String? = null,
@SerialName("external_asset_path")
val externalAssetPath: String? = null
)
private val api = "https://discord.com/api/v9/applications/$applicationId/external-assets"
suspend fun getDiscordUri(imageUrl: String): String? {
if (imageUrl.startsWith("mp:")) return imageUrl
val request = Request.Builder().url(api).header("Authorization", token)
.post("{\"urls\":[\"$imageUrl\"]}".toRequestBody("application/json".toMediaType()))
.build()
return runCatching {
val res = client.newCall(request).await()
json.decodeFromString<List<ExternalAsset>>(res.body.string())
.firstOrNull()?.externalAssetPath?.let { "mp:$it" }
}.getOrNull()
}
private suspend inline fun Call.await(): Response {
return suspendCoroutine {
enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
it.resumeWithException(e)
}
override fun onResponse(call: Call, response: Response) {
it.resume(response)
}
})
}
}
}

View File

@@ -40,6 +40,7 @@ data class Activity(
@Serializable
data class Timestamps(
val start: Long? = null,
@SerialName("end")
val stop: Long? = null
)
}

View File

@@ -0,0 +1,121 @@
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"
"itsmechinmoy" -> "Discord and Telegram Admin/Helper, Comment Moderator & Translator"
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(
"Ziadsenior",
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6049773-8cjYeUOFUguv.jpg",
"Comment Moderator and Arabic Translator",
"https://anilist.co/user/6049773"
),
Developer(
"Dawnusedyeet",
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6237399-RHFvRHriXjwS.png",
"Contributor",
"https://anilist.co/user/Dawnusedyeet/"
),
Developer(
"hastsu",
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6183359-9os7zUhYdF64.jpg",
"Comment Moderator and Arabic Translator",
"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

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

View File

@@ -5,17 +5,15 @@ 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
import ani.dantotsu.loadData
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.saveData
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.tryWithSuspend
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.io.File
import java.security.SecureRandom
object MAL {
@@ -34,7 +32,7 @@ object MAL {
.replace("/", "_")
.replace("\n", "")
saveData("malCodeChallenge", codeChallenge, context)
PrefManager.setVal(PrefName.MALCodeChallenge, codeChallenge)
val request =
"https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id=$clientId&code_challenge=$codeChallenge"
try {
@@ -47,11 +45,9 @@ object MAL {
}
}
private const val MAL_TOKEN = "malToken"
private suspend fun refreshToken(): ResponseToken? {
return tryWithSuspend {
val token = loadData<ResponseToken>(MAL_TOKEN)
val token = PrefManager.getNullableVal<ResponseToken>(PrefName.MALToken, null)
?: throw Exception(currContext()?.getString(R.string.refresh_token_load_failed))
val res = client.post(
"https://myanimelist.net/v1/oauth2/token",
@@ -67,10 +63,11 @@ object MAL {
}
suspend fun getSavedToken(context: FragmentActivity): Boolean {
suspend fun getSavedToken(): Boolean {
return tryWithSuspend(false) {
var res: ResponseToken = loadData(MAL_TOKEN, context)
?: return@tryWithSuspend false
var res: ResponseToken =
PrefManager.getNullableVal<ResponseToken>(PrefName.MALToken, null)
?: return@tryWithSuspend false
if (System.currentTimeMillis() > res.expiresIn)
res = refreshToken()
?: throw Exception(currContext()?.getString(R.string.refreshing_token_failed))
@@ -79,19 +76,17 @@ object MAL {
} ?: false
}
fun removeSavedToken(context: Context) {
fun removeSavedToken() {
token = null
username = null
userid = null
avatar = null
if (MAL_TOKEN in context.fileList()) {
File(context.filesDir, MAL_TOKEN).delete()
}
PrefManager.removeVal(PrefName.MALToken)
}
fun saveResponse(res: ResponseToken) {
res.expiresIn += System.currentTimeMillis()
saveData(MAL_TOKEN, res)
PrefManager.setVal(PrefName.MALToken, res)
}
@Serializable
@@ -100,6 +95,10 @@ object MAL {
@SerialName("expires_in") var expiresIn: Long,
@SerialName("access_token") val accessToken: String,
@SerialName("refresh_token") val refreshToken: String,
) : java.io.Serializable
) : java.io.Serializable {
companion object {
private const val serialVersionUID = 1L
}
}
}

View File

@@ -0,0 +1,384 @@
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(
downloadedType.titleName,
"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(
downloadedType.titleName,
"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,
"Unknown",
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, toast: Boolean) {
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 (toast) {
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,34 +1,46 @@
package ani.dantotsu.download
import android.content.Context
import android.content.SharedPreferences
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.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) {
private val prefs: SharedPreferences =
context.getSharedPreferences("downloads_pref", Context.MODE_PRIVATE)
private val gson = Gson()
private val downloadsList = loadDownloads().toMutableList()
val mangaDownloadedTypes: List<DownloadedType>
get() = downloadsList.filter { it.type == DownloadedType.Type.MANGA }
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)
prefs.edit().putString("downloads_key", jsonString).apply()
PrefManager.setVal(PrefName.DownloadsKeys, jsonString)
}
private fun loadDownloads(): List<DownloadedType> {
val jsonString = prefs.getString("downloads_key", null)
val jsonString = PrefManager.getVal(PrefName.DownloadsKeys, null as String?)
return if (jsonString != null) {
val type = object : TypeToken<List<DownloadedType>>() {}.type
gson.fromJson(jsonString, type)
@@ -42,82 +54,72 @@ class DownloadsManager(private val context: Context) {
saveDownloads()
}
fun removeDownload(downloadedType: DownloadedType) {
downloadsList.remove(downloadedType)
removeDirectory(downloadedType)
fun removeDownload(
downloadedType: DownloadedType,
toast: Boolean = true,
onFinished: () -> Unit
) {
removeDownloadCompat(context, downloadedType, toast)
downloadsList.removeAll { it.titleName == downloadedType.titleName && it.chapterName == downloadedType.chapterName }
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)
}
}
}
@@ -125,122 +127,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.removeAll { it.titleName == downloadedType.titleName && it.chapterName == downloadedType.chapterName }
// 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 }
@@ -248,57 +266,148 @@ 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
*/
@Synchronized
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
*/
@Synchronized
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")
}
}
@Synchronized
private fun getBaseDirectory(context: Context): DocumentFile? {
val baseDirectory = Uri.parse(PrefManager.getVal<String>(PrefName.DownloadsDir))
if (baseDirectory == Uri.EMPTY) return null
val base = DocumentFile.fromTreeUri(context, baseDirectory) ?: return null
return base.findOrCreateFolder(BASE_LOCATION, false)
}
private val lock = Any()
private fun DocumentFile.findOrCreateFolder(
name: String, overwrite: Boolean
): DocumentFile? {
val validName = name.findValidName()
synchronized(lock) {
return if (overwrite) {
findFolder(validName)?.delete()
createDirectory(validName)
} else {
val folder = findFolder(validName)
folder ?: createDirectory(validName)
}
}
}
private fun DocumentFile.findFolder(name: String): DocumentFile? =
listFiles().find { it.name == name && it.isDirectory }
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?.replace("/", "_")?.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,
val scanlator: String = "Unknown"
) : Serializable {
val titleName: String
get() = title ?: pTitle.findValidName()
val chapterName: String
get() = chapter ?: pChapter.findValidName()
val uniqueName: String
get() = "$chapterName-${scanlator}"
}

View File

@@ -9,31 +9,34 @@ 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.currActivity
import ani.dantotsu.addons.download.DownloadAddonManager
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
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.download.findValidName
import ani.dantotsu.media.Media
import ani.dantotsu.media.SubtitleDownloader
import ani.dantotsu.media.MediaType
import ani.dantotsu.media.anime.AnimeWatchFragment
import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.Video
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.snackString
import com.google.firebase.crashlytics.FirebaseCrashlytics
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
@@ -44,9 +47,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
@@ -55,13 +56,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
@@ -72,6 +72,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.
@@ -80,13 +81,19 @@ 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 {
setContentTitle("Anime Download Progress")
setSmallIcon(R.drawable.ic_round_download_24)
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(
@@ -155,27 +162,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()
@@ -187,7 +181,6 @@ class AnimeDownloaderService : Service() {
}
private fun updateNotification() {
// Update the notification to reflect the current state of the queue
val pendingDownloads = AnimeServiceDataSingleton.downloadQueue.size
val text = if (pendingDownloads > 0) {
"Pending downloads: $pendingDownloads"
@@ -207,9 +200,8 @@ class AnimeDownloaderService : Service() {
@androidx.annotation.OptIn(UnstableApi::class)
suspend fun download(task: AnimeDownloadTask) {
try {
val downloadManager = Helper.downloadManager(this@AnimeDownloaderService)
withContext(Dispatchers.Main) {
withContext(Dispatchers.IO) {
try {
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(
this@AnimeDownloaderService,
@@ -219,158 +211,232 @@ 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())
withContext(Dispatchers.Main) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
}
broadcastDownloadStarted(task.episode)
val baseOutputDir = getSubDirectory(
this@AnimeDownloaderService,
MediaType.ANIME,
false,
task.title
) ?: throw Exception("Failed to create output directory")
val outputDir = getSubDirectory(
this@AnimeDownloaderService,
MediaType.ANIME,
true,
task.title,
task.episode
) ?: throw Exception("Failed to create output directory")
currActivity()?.let {
Helper.downloadVideo(
it,
task.video,
task.subtitle
val extension = ffExtension!!.getFileExtension()
outputDir.findFile("${task.getTaskName().findValidName()}.${extension.first}")
?.delete()
val outputFile =
outputDir.createFile(
extension.second,
"${task.getTaskName()}.${extension.first}"
)
?: throw Exception("Failed to create output file")
var percent = 0
var totalLength = 0.0
val path = ffExtension.setDownloadPath(
this@AnimeDownloaderService,
outputFile.uri
)
if (!task.video.file.headers.containsKey("User-Agent")
&& !task.video.file.headers.containsKey("user-agent")
) {
val newHeaders = task.video.file.headers.toMutableMap()
newHeaders["User-Agent"] = defaultHeaders["User-Agent"]!!
task.video.file.headers = newHeaders
}
saveMediaInfo(task)
task.subtitle?.let {
SubtitleDownloader.downloadSubtitle(
this@AnimeDownloaderService,
it.file.url,
DownloadedType(
task.title,
task.episode,
DownloadedType.Type.ANIME,
)
)
}
val downloadStarted =
hasDownloadStarted(downloadManager, task, 30000) // 30 seconds timeout
if (!downloadStarted) {
logger("Download failed to start")
builder.setContentText("${task.title} - ${task.episode} Download failed to start")
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${task.title} - ${task.episode} Download failed to start")
broadcastDownloadFailed(task.episode)
return@withContext
ffExtension.executeFFProbe(
task.video.file.url,
task.video.file.headers
) {
if (it.toDoubleOrNull() != null) {
totalLength = it.toDouble()
}
}
val ffTask =
ffExtension.executeFFMpeg(
task.video.file.url,
path,
task.video.file.headers,
task.subtitle,
task.audio,
) {
// 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, baseOutputDir)
// 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
)
)
FirebaseCrashlytics.getInstance().recordException(
Exception(
"Anime Download failed:" +
" ${download.failureReason}" +
" url: ${task.video.file.url}" +
" title: ${task.title}" +
" episode: ${task.episode}"
)
)
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
broadcastDownloadFailed(task.episode)
break
}
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_COMPLETED) {
logger("Download completed")
builder.setContentText("${task.title} - ${task.episode} Download completed")
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${task.title} - ${task.episode} Download completed")
getSharedPreferences(
getString(R.string.anime_downloads),
Context.MODE_PRIVATE
).edit().putString(
task.getTaskName(),
task.video.file.url
).apply()
downloadsManager.addDownload(
DownloadedType(
task.title,
task.episode,
DownloadedType.Type.ANIME,
)
)
currentTasks.removeAll { it.getTaskName() == task.getTaskName() }
broadcastDownloadFinished(task.episode)
break
}
if (download.state == androidx.media3.exoplayer.offline.Download.STATE_STOPPED) {
logger("Download stopped")
builder.setContentText("${task.title} - ${task.episode} Download stopped")
notificationManager.notify(NOTIFICATION_ID, builder.build())
snackString("${task.title} - ${task.episode} Download stopped")
break
}
broadcastDownloadProgress(
task.episode,
download.percentDownloaded.toInt()
} Download failed"
)
if (notifi) {
withContext(Dispatchers.Main) {
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) {
withContext(Dispatchers.Main) {
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"
)
if (notifi) {
withContext(Dispatchers.Main) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
}
snackString("${getTaskName(task.title, task.episode)} Download failed")
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)
return@withContext
}
Logger.log("Download completed")
builder.setContentText(
"${
getTaskName(
task.title,
task.episode
)
} Download completed"
)
if (notifi) {
withContext(Dispatchers.Main) {
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.log("Exception while downloading file: ${e.message}")
snackString("Exception while downloading file: ${e.message}")
e.printStackTrace()
Injekt.get<CrashlyticsInterface>().logException(e)
}
broadcastDownloadFailed(task.episode)
}
} catch (e: Exception) {
if (e.message?.contains("Coroutine was cancelled") == false) { //wut
logger("Exception while downloading file: ${e.message}")
snackString("Exception while downloading file: ${e.message}")
e.printStackTrace()
FirebaseCrashlytics.getInstance().recordException(e)
}
broadcastDownloadFailed(task.episode)
}
}
@androidx.annotation.OptIn(UnstableApi::class)
suspend fun hasDownloadStarted(
downloadManager: DownloadManager,
task: AnimeDownloadTask,
timeout: Long
): Boolean {
val startTime = System.currentTimeMillis()
while (System.currentTimeMillis() - startTime < timeout) {
val download = downloadManager.downloadIndex.getDownload(task.video.file.url)
if (download != null) {
return true
}
// Delay between each poll
kotlinx.coroutines.delay(500)
}
return false
}
private fun saveMediaInfo(task: AnimeDownloadTask, directory: DocumentFile) {
CoroutineScope(Dispatchers.IO).launch {
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")
@OptIn(DelicateCoroutinesApi::class)
private fun saveMediaInfo(task: AnimeDownloadTask) {
GlobalScope.launch(Dispatchers.IO) {
val directory = File(
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"${DownloadsManager.animeLocation}/${task.title}"
)
val episodeDirectory = File(directory, task.episode)
if (!directory.exists()) directory.mkdirs()
if (!episodeDirectory.exists()) episodeDirectory.mkdirs()
val file = File(directory, "media.json")
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
@@ -404,14 +470,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")
@@ -422,13 +499,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) {
@@ -490,19 +570,21 @@ class AnimeDownloaderService : Service() {
val title: String,
val episode: String,
val video: Video,
val subtitle: Subtitle? = null,
val subtitle: List<Pair<String, String>> = emptyList(),
val audio: List<Pair<String, String>> = emptyList(),
val sourceMedia: Media? = null,
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("/", "")}"
}
}
}
@@ -516,7 +598,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
@@ -12,6 +11,8 @@ import android.widget.LinearLayout
import android.widget.TextView
import androidx.cardview.widget.CardView
import ani.dantotsu.R
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
class OfflineAnimeAdapter(
@@ -22,8 +23,7 @@ class OfflineAnimeAdapter(
private val inflater: LayoutInflater =
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
private var originalItems: List<OfflineAnimeModel> = items
private var style =
context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("offline_view", 0)
private var style: Int = PrefManager.getVal(PrefName.OfflineView)
override fun getCount(): Int {
return items.size
@@ -37,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) {
@@ -50,28 +49,28 @@ 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
val text = " ${context.getString(R.string.episodes)}"
episodes.text = text
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)
@@ -105,8 +104,7 @@ class OfflineAnimeAdapter(
}
fun notifyNewGrid() {
style =
context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("offline_view", 0)
style = PrefManager.getVal(PrefName.OfflineView)
notifyDataSetChanged()
}
}

View File

@@ -1,22 +1,16 @@
package ani.dantotsu.download.anime
import android.animation.ObjectAnimator
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.text.Editable
import android.text.TextWatcher
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.AlphaAnimation
import android.view.animation.LayoutAnimationController
import android.view.animation.OvershootInterpolator
import android.widget.AbsListView
import android.widget.AutoCompleteTextView
import android.widget.GridView
@@ -25,34 +19,40 @@ import android.widget.TextView
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
import androidx.core.util.Pair
import androidx.core.view.ViewCompat
import androidx.core.view.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.loadData
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.UserInterfaceSettings
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import ani.dantotsu.util.Logger
import ani.dantotsu.util.customAlertDialog
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
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.animesource.model.SAnime
@@ -61,11 +61,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
import kotlin.math.max
import kotlin.math.min
class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
@@ -73,9 +75,8 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
private var downloads: List<OfflineAnimeModel> = listOf()
private lateinit var gridView: GridView
private lateinit var adapter: OfflineAnimeAdapter
private lateinit var total : TextView
private var uiSettings: UserInterfaceSettings =
loadData("ui_settings") ?: UserInterfaceSettings()
private lateinit var total: TextView
private var downloadsJob: Job = Job()
override fun onCreateView(
inflater: LayoutInflater,
@@ -91,9 +92,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 {
@@ -101,15 +100,12 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.OfflineANIME)
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
}
if (!uiSettings.immersiveMode) {
if (!(PrefManager.getVal(PrefName.ImmersiveMode) as Boolean)) {
view.rootView.fitsSystemWindows = true
}
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("colorOverflow", false) ?: false
if (!colorOverflow) {
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
}
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
val searchView = view.findViewById<AutoCompleteTextView>(R.id.animeSearchBarText)
searchView.addTextChangedListener(object : TextWatcher {
@@ -123,13 +119,12 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
onSearchQuery(s.toString())
}
})
var style = context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getInt("offline_view", 0)
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
@@ -143,26 +138,25 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
layoutList.setOnClickListener {
selected(it as ImageView)
style = 0
context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
?.putInt("offline_view", style!!)?.apply()
PrefManager.setVal(PrefName.OfflineView, style)
gridView.visibility = View.GONE
gridView = view.findViewById(R.id.gridView)
adapter.notifyNewGrid()
grid()
}
layoutcompact.setOnClickListener {
layoutCompact.setOnClickListener {
selected(it as ImageView)
style = 1
context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
?.putInt("offline_view", style!!)?.apply()
PrefManager.setVal(PrefName.OfflineView, style)
gridView.visibility = View.GONE
gridView = view.findViewById(R.id.gridView1)
adapter.notifyNewGrid()
grid()
}
gridView = if (style == 0) view.findViewById(R.id.gridView) else view.findViewById(R.id.gridView1)
gridView =
if (style == 0) view.findViewById(R.id.gridView) else view.findViewById(R.id.gridView1)
total = view.findViewById(R.id.total)
grid()
return view
@@ -171,11 +165,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"
@@ -183,26 +177,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
}
MediaDetailsActivity.mediaSingleton = mediaModel
ContextCompat.startActivity(
requireActivity(),
Intent(requireContext(), MediaDetailsActivity::class.java)
.putExtra("download", true),
ActivityOptionsCompat.makeSceneTransitionAnimation(
lifecycleScope.launch {
val mediaModel = getMedia(it)
if (mediaModel == null) {
snackString("Error loading media.json")
return@launch
}
MediaDetailsActivity.mediaSingleton = mediaModel
ContextCompat.startActivity(
requireActivity(),
Pair.create(
requireActivity().findViewById<ImageView>(R.id.itemCompactImage),
ViewCompat.getTransitionName(requireActivity().findViewById(R.id.itemCompactImage))
),
).toBundle()
)
Intent(requireContext(), MediaDetailsActivity::class.java)
.putExtra("download", true),
null
)
}
} ?: run {
snackString("no media found")
}
@@ -210,37 +200,27 @@ 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 =
androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup)
builder.setTitle("Delete ${item.title}?")
builder.setMessage("Are you sure you want to delete ${item.title}?")
builder.setPositiveButton("Yes") { _, _ ->
downloadManager.removeMedia(item.title, type)
val mediaIds = requireContext().getSharedPreferences(
getString(R.string.anime_downloads),
Context.MODE_PRIVATE
)
?.all?.filter { it.key.contains(item.title) }?.values ?: emptySet()
if (mediaIds.isEmpty()) {
snackString("No media found") // if this happens, terrible things have happened
requireContext().customAlertDialog().apply {
setTitle("Delete ${item.title}?")
setMessage("Are you sure you want to delete ${item.title}?")
setPosButton(R.string.yes) {
downloadManager.removeMedia(item.title, type)
val mediaIds =
PrefManager.getAnimeDownloadPreferences().all?.filter { it.key.contains(item.title) }?.values
?: emptySet()
if (mediaIds.isEmpty()) {
snackString("No media found") // if this happens, terrible things have happened
}
getDownloads()
}
for (mediaId in mediaIds) {
ani.dantotsu.download.video.Helper.downloadManager(requireContext())
.removeDownload(mediaId.toString())
setNegButton(R.string.no) {
// Do nothing
}
getDownloads()
adapter.setItems(downloads)
total.text = if (gridView.count > 0) "Anime (${gridView.count})" else "Empty List"
show()
}
builder.setNegativeButton("No") { _, _ ->
// Do nothing
}
val dialog = builder.show()
dialog.window?.setDimAmount(0.8f)
true
}
}
@@ -252,41 +232,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
var height = statusBarHeight
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val displayCutout = activity?.window?.decorView?.rootWindowInsets?.displayCutout
if (displayCutout != null) {
if (displayCutout.boundingRects.size > 0) {
height = max(
statusBarHeight,
min(
displayCutout.boundingRects[0].width(),
displayCutout.boundingRects[0].height()
)
)
}
}
}
val scrollTop = view.findViewById<CardView>(R.id.mangaPageScrollTop)
scrollTop.translationY =
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
val visible = false
fun animate() {
val start = if (visible) 0f else 1f
val end = if (!visible) 0f else 1f
ObjectAnimator.ofFloat(scrollTop, "scaleX", start, end).apply {
duration = 300
interpolator = OvershootInterpolator(2f)
start()
}
ObjectAnimator.ofFloat(scrollTop, "scaleY", start, end).apply {
duration = 300
interpolator = OvershootInterpolator(2f)
start()
}
}
scrollTop.setOnClickListener {
gridView.smoothScrollToPositionFromTop(0, 0)
}
@@ -296,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(
@@ -306,8 +251,10 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
totalItemCount: Int
) {
val first = view.getChildAt(0)
val visibility = first != null && first.top < -height
scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE
val visibility = first != null && first.top < 0
scrollTop.translationY =
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
scrollTop.isVisible = visibility
}
})
initActivity(requireActivity())
@@ -317,7 +264,6 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
override fun onResume() {
super.onResume()
getDownloads()
adapter.notifyDataSetChanged()
}
override fun onPause() {
@@ -337,78 +283,94 @@ 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 _downloads = downloadManager.animeDownloadedTypes.filter { it.title == title }
val download = _downloads.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)
if (offlineAnimeModel.title == "unknown") offlineAnimeModel.title = title
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 = if (downloadedType.type == DownloadedType.Type.ANIME) {
"Anime"
} else if (downloadedType.type == DownloadedType.Type.MANGA) {
"Manga"
} else {
"Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
/**
* 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
SChapterImpl()
})
.registerTypeAdapter(SAnime::class.java, InstanceCreator<SAnime> {
SAnimeImpl() // Provide an instance of SAnimeImpl
SAnimeImpl()
})
.registerTypeAdapter(SEpisode::class.java, InstanceCreator<SEpisode> {
SEpisodeImpl() // Provide an instance of SEpisodeImpl
SEpisodeImpl()
})
.create()
val media = File(directory, "media.json")
val mediaJson = media.readText()
val media = directory?.findFile("media.json")
if (media == null) {
Logger.log("No media.json found at ${directory?.uri?.path}")
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())
FirebaseCrashlytics.getInstance().recordException(e)
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 = if (downloadedType.type == DownloadedType.Type.MANGA) {
"Manga"
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
"Anime"
} else {
"Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
/**
* 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 media = File(directory, "media.json")
val mediaJson = media.readText()
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()
@@ -437,22 +399,27 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
bannerUri
)
} catch (e: Exception) {
logger("Error loading media.json: ${e.message}")
logger(e.printStackTrace())
FirebaseCrashlytics.getInstance().recordException(e)
return OfflineAnimeModel(
"unknown",
"0",
"??",
"??",
"??",
"movie",
"hmm",
false,
false,
null,
null
)
Logger.log(e)
return try {
loadOfflineAnimeModelCompat(downloadedType)
} catch (e: Exception) {
Logger.log("Error loading media.json: ${e.message}")
Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e)
OfflineAnimeModel(
downloadedType.titleName,
"0",
"??",
"??",
"??",
"movie",
"hmm",
isOngoing = false,
isUserScored = false,
null,
null
)
}
}
}
}

View File

@@ -3,7 +3,7 @@ package ani.dantotsu.download.anime
import android.net.Uri
data class OfflineAnimeModel(
val title: String,
var title: String,
val score: String,
val totalEpisode: String,
val totalEpisodeList: String,

View File

@@ -10,18 +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
@@ -29,7 +31,11 @@ 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 com.google.firebase.crashlytics.FirebaseCrashlytics
import ani.dantotsu.util.Logger
import ani.dantotsu.util.NumberConverter.Companion.ofLength
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 +43,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 +53,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
@@ -76,7 +81,7 @@ class MangaDownloaderService : Service() {
notificationManager = NotificationManagerCompat.from(this)
builder = NotificationCompat.Builder(this, CHANNEL_DOWNLOADER_PROGRESS).apply {
setContentTitle("Manga Download Progress")
setSmallIcon(R.drawable.ic_round_download_24)
setSmallIcon(R.drawable.ic_download_24)
priority = NotificationCompat.PRIORITY_DEFAULT
setOnlyAlertOnce(true)
setProgress(0, 0, false)
@@ -130,15 +135,15 @@ class MangaDownloaderService : Service() {
mutex.withLock {
downloadJobs[task.chapter] = job
}
job.join() // Wait for the job to complete before continuing to the next task
job.join()
mutex.withLock {
downloadJobs.remove(task.chapter)
}
updateNotification() // Update the notification after each task is completed
updateNotification()
}
if (MangaServiceDataSingleton.downloadQueue.isEmpty()) {
withContext(Dispatchers.Main) {
stopSelf() // Stop the service when the queue is empty
stopSelf()
}
}
}
@@ -177,7 +182,7 @@ class MangaDownloaderService : Service() {
suspend fun download(task: DownloadTask) {
try {
withContext(Dispatchers.Main) {
withContext(Dispatchers.IO) {
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(
this@MangaDownloaderService,
@@ -187,14 +192,30 @@ 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())
withContext(Dispatchers.Main) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
}
// Loop through each ImageData object from the task
val baseOutputDir = getSubDirectory(
this@MangaDownloaderService,
MediaType.MANGA,
false,
task.title
) ?: throw Exception("Base output directory not found")
val outputDir = getSubDirectory(
this@MangaDownloaderService,
MediaType.MANGA,
false,
task.title,
task.chapter
) ?: throw Exception("Output directory not found")
outputDir.deleteRecursively(this@MangaDownloaderService, true)
var farthest = 0
for ((index, image) in task.imageData.withIndex()) {
if (deferredMap.size >= task.simultaneousDownloads) {
@@ -209,93 +230,95 @@ class MangaDownloaderService : Service() {
while (bitmap == null && retryCount < task.retries) {
bitmap = image.fetchAndProcessImage(
image.page,
image.source,
this@MangaDownloaderService
image.source
)
if (bitmap == null) {
snackString("${task.chapter} - Retrying to download page ${index.ofLength(3)}, attempt ${retryCount + 1}.")
}
retryCount++
}
if (bitmap != null) {
saveToDisk("$index.jpg", bitmap, task.title, task.chapter)
if (bitmap == null) {
outputDir.deleteRecursively(this@MangaDownloaderService, false)
throw Exception("${task.chapter} - Unable to download all pages after $retryCount attempts. Try again.")
}
saveToDisk("${index.ofLength(3)}.jpg", outputDir, bitmap)
farthest++
builder.setProgress(task.imageData.size, farthest, false)
broadcastDownloadProgress(
task.chapter,
task.uniqueName,
farthest * 100 / task.imageData.size
)
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
withContext(Dispatchers.Main) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
}
bitmap
}
}
// Wait for any remaining deferred to complete
deferredMap.values.awaitAll()
builder.setContentText("${task.title} - ${task.chapter} Download complete")
.setProgress(0, 0, false)
notificationManager.notify(NOTIFICATION_ID, builder.build())
withContext(Dispatchers.Main) {
builder.setContentText("${task.title} - ${task.chapter} Download complete")
.setProgress(0, 0, false)
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
}
saveMediaInfo(task)
saveMediaInfo(task, baseOutputDir)
downloadsManager.addDownload(
DownloadedType(
task.title,
task.chapter,
DownloadedType.Type.MANGA
MediaType.MANGA,
scanlator = task.scanlator,
)
)
broadcastDownloadFinished(task.chapter)
broadcastDownloadFinished(task.uniqueName)
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}")
FirebaseCrashlytics.getInstance().recordException(e)
broadcastDownloadFailed(task.chapter)
Injekt.get<CrashlyticsInterface>().logException(e)
broadcastDownloadFailed(task.uniqueName)
}
}
private fun saveToDisk(fileName: String, bitmap: Bitmap, title: String, chapter: String) {
private fun saveToDisk(
fileName: String,
directory: DocumentFile,
bitmap: Bitmap
) {
try {
// Define the directory within the private external storage space
val directory = File(
this.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/$title/$chapter"
)
directory.findFile(fileName)?.forceDelete(this)
val file =
directory.createFile("image/jpeg", fileName) ?: throw Exception("File not created")
if (!directory.exists()) {
directory.mkdirs()
}
// Create a file reference within that directory for your image
val file = File(directory, fileName)
// Use a FileOutputStream to write the bitmap to the file
FileOutputStream(file).use { outputStream ->
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}")
FirebaseCrashlytics.getInstance().recordException(e)
Injekt.get<CrashlyticsInterface>().logException(e)
}
}
private fun saveMediaInfo(task: DownloadTask) {
GlobalScope.launch(Dispatchers.IO) {
val directory = File(
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Manga/${task.title}"
)
if (!directory.exists()) directory.mkdirs()
val file = File(directory, "media.json")
@OptIn(DelicateCoroutinesApi::class)
private fun saveMediaInfo(task: DownloadTask, directory: DocumentFile) {
launchIO {
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 +333,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 +351,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 +361,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) {
@@ -402,11 +430,15 @@ class MangaDownloaderService : Service() {
data class DownloadTask(
val title: String,
val chapter: String,
val scanlator: String,
val imageData: List<ImageData>,
val sourceMedia: Media? = null,
val retries: Int = 2,
val simultaneousDownloads: Int = 2,
)
) {
val uniqueName: String
get() = "$chapter-$scanlator"
}
companion object {
private const val NOTIFICATION_ID = 1103

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
@@ -11,6 +10,8 @@ import android.widget.LinearLayout
import android.widget.TextView
import androidx.cardview.widget.CardView
import ani.dantotsu.R
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
class OfflineMangaAdapter(
@@ -21,8 +22,7 @@ class OfflineMangaAdapter(
private val inflater: LayoutInflater =
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
private var originalItems: List<OfflineMangaModel> = items
private var style =
context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("offline_view", 0)
private var style: Int = PrefManager.getVal(PrefName.OfflineView)
override fun getCount(): Int {
return items.size
@@ -36,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) {
@@ -49,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)
@@ -59,14 +57,15 @@ 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)
val text = " ${context.getString(R.string.chapters)}"
chapters.text = text
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
@@ -104,8 +103,7 @@ class OfflineMangaAdapter(
}
fun notifyNewGrid() {
style =
context.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).getInt("offline_view", 0)
style = PrefManager.getVal(PrefName.OfflineView)
notifyDataSetChanged()
}
}

View File

@@ -1,21 +1,15 @@
package ani.dantotsu.download.manga
import android.animation.ObjectAnimator
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.text.Editable
import android.text.TextWatcher
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.AlphaAnimation
import android.view.animation.LayoutAnimationController
import android.view.animation.OvershootInterpolator
import android.widget.AbsListView
import android.widget.AutoCompleteTextView
import android.widget.GridView
@@ -23,42 +17,51 @@ import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
import androidx.core.util.Pair
import androidx.core.view.ViewCompat
import androidx.core.view.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.loadData
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.UserInterfaceSettings
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import ani.dantotsu.util.Logger
import ani.dantotsu.util.customAlertDialog
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
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.google.gson.GsonBuilder
import com.google.gson.InstanceCreator
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SChapterImpl
import 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
import kotlin.math.max
import kotlin.math.min
class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
@@ -67,8 +70,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private lateinit var gridView: GridView
private lateinit var adapter: OfflineMangaAdapter
private lateinit var total: TextView
private var uiSettings: UserInterfaceSettings =
loadData("ui_settings") ?: UserInterfaceSettings()
private var downloadsJob: Job = Job()
override fun onCreateView(
inflater: LayoutInflater,
@@ -84,9 +86,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 {
@@ -94,15 +94,12 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
SettingsDialogFragment.newInstance(SettingsDialogFragment.Companion.PageType.OfflineMANGA)
dialogFragment.show((it.context as AppCompatActivity).supportFragmentManager, "dialog")
}
if (!uiSettings.immersiveMode) {
if (!(PrefManager.getVal(PrefName.ImmersiveMode) as Boolean)) {
view.rootView.fitsSystemWindows = true
}
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("colorOverflow", false) ?: false
if (!colorOverflow) {
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
}
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000)
val searchView = view.findViewById<AutoCompleteTextView>(R.id.animeSearchBarText)
searchView.addTextChangedListener(object : TextWatcher {
@@ -116,8 +113,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
onSearchQuery(s.toString())
}
})
var style = context?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getInt("offline_view", 0)
var style: Int = PrefManager.getVal(PrefName.OfflineView)
val layoutList = view.findViewById<ImageView>(R.id.downloadedList)
val layoutcompact = view.findViewById<ImageView>(R.id.downloadedGrid)
var selected = when (style) {
@@ -136,8 +132,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
layoutList.setOnClickListener {
selected(it as ImageView)
style = 0
requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit()
.putInt("offline_view", style!!).apply()
PrefManager.setVal(PrefName.OfflineView, style)
gridView.visibility = View.GONE
gridView = view.findViewById(R.id.gridView)
adapter.notifyNewGrid()
@@ -148,8 +143,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
layoutcompact.setOnClickListener {
selected(it as ImageView)
style = 1
requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE).edit()
.putInt("offline_view", style!!).apply()
PrefManager.setVal(PrefName.OfflineView, style)
gridView.visibility = View.GONE
gridView = view.findViewById(R.id.gridView1)
adapter.notifyNewGrid()
@@ -164,11 +158,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 =
@@ -177,23 +171,22 @@ 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 }
media?.let {
ContextCompat.startActivity(
requireActivity(),
Intent(requireContext(), MediaDetailsActivity::class.java)
.putExtra("media", getMedia(it))
.putExtra("download", true),
ActivityOptionsCompat.makeSceneTransitionAnimation(
requireActivity(),
Pair.create(
gridView.getChildAt(position)
.findViewById<ImageView>(R.id.itemCompactImage),
ViewCompat.getTransitionName(requireActivity().findViewById(R.id.itemCompactImage))
downloadManager.mangaDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) }
?: downloadManager.novelDownloadedTypes.firstOrNull {
it.titleName.compareName(
item.title
)
).toBundle()
)
}
media?.let {
lifecycleScope.launch {
ContextCompat.startActivity(
requireActivity(),
Intent(requireContext(), MediaDetailsActivity::class.java)
.putExtra("media", getMedia(it))
.putExtra("download", true),
null
)
}
} ?: run {
snackString("no media found")
}
@@ -202,29 +195,22 @@ 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 =
androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup)
builder.setTitle("Delete ${item.title}?")
builder.setMessage("Are you sure you want to delete ${item.title}?")
builder.setPositiveButton("Yes") { _, _ ->
downloadManager.removeMedia(item.title, type)
getDownloads()
adapter.setItems(downloads)
total.text =
if (gridView.count > 0) "Manga and Novels (${gridView.count})" else "Empty List"
}
builder.setNegativeButton("No") { _, _ ->
// Do nothing
}
val dialog = builder.show()
dialog.window?.setDimAmount(0.8f)
requireContext().customAlertDialog().apply {
setTitle("Delete ${item.title}?")
setMessage("Are you sure you want to delete ${item.title}?")
setPosButton(R.string.yes) {
downloadManager.removeMedia(item.title, type)
getDownloads()
}
setNegButton(R.string.no)
}.show()
true
}
}
@@ -236,41 +222,8 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initActivity(requireActivity())
var height = statusBarHeight
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val displayCutout = activity?.window?.decorView?.rootWindowInsets?.displayCutout
if (displayCutout != null) {
if (displayCutout.boundingRects.size > 0) {
height = max(
statusBarHeight,
min(
displayCutout.boundingRects[0].width(),
displayCutout.boundingRects[0].height()
)
)
}
}
}
val scrollTop = view.findViewById<CardView>(R.id.mangaPageScrollTop)
scrollTop.translationY =
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
val visible = false
fun animate() {
val start = if (visible) 0f else 1f
val end = if (!visible) 0f else 1f
ObjectAnimator.ofFloat(scrollTop, "scaleX", start, end).apply {
duration = 300
interpolator = OvershootInterpolator(2f)
start()
}
ObjectAnimator.ofFloat(scrollTop, "scaleY", start, end).apply {
duration = 300
interpolator = OvershootInterpolator(2f)
start()
}
}
scrollTop.setOnClickListener {
gridView.smoothScrollToPositionFromTop(0, 0)
}
@@ -280,7 +233,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(
@@ -290,8 +242,10 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
totalItemCount: Int
) {
val first = view.getChildAt(0)
val visibility = first != null && first.top < -height
scrollTop.visibility = if (visibility) View.VISIBLE else View.GONE
val visibility = first != null && first.top < 0
scrollTop.isVisible = visibility
scrollTop.translationY =
-(navBarHeight + bottomBar.height + bottomBar.marginBottom).toFloat()
}
})
@@ -301,7 +255,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
override fun onResume() {
super.onResume()
getDownloads()
adapter.notifyDataSetChanged()
}
override fun onPause() {
@@ -321,96 +274,108 @@ 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 _downloads = downloadManager.mangaDownloadedTypes.filter { it.title == title }
val download = _downloads.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 _downloads = downloadManager.novelDownloadedTypes.filter { it.title == title }
val download = _downloads.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 = if (downloadedType.type == DownloadedType.Type.MANGA) {
"Manga"
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
"Anime"
} else {
"Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
/**
* 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
SChapterImpl()
})
.create()
val media = File(directory, "media.json")
val mediaJson = media.readText()
val media = directory?.findFile("media.json")
if (media == null) {
Logger.log("No media.json found at ${directory?.uri?.path}")
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())
FirebaseCrashlytics.getInstance().recordException(e)
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 = if (downloadedType.type == DownloadedType.Type.MANGA) {
"Manga"
} else if (downloadedType.type == DownloadedType.Type.ANIME) {
"Anime"
} else {
"Novel"
}
val directory = File(
currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/$type/${downloadedType.title}"
)
//load media.json and convert to media class with gson
private suspend fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
val type = downloadedType.type.asText()
try {
val media = File(directory, "media.json")
val mediaJson = media.readText()
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,
@@ -419,21 +384,26 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
bannerUri
)
} catch (e: Exception) {
logger("Error loading media.json: ${e.message}")
logger(e.printStackTrace())
FirebaseCrashlytics.getInstance().recordException(e)
return OfflineMangaModel(
"unknown",
"0",
"??",
"??",
"movie",
"hmm",
false,
false,
null,
null
)
Logger.log(e)
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(
downloadedType.titleName,
"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 com.google.firebase.crashlytics.FirebaseCrashlytics
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.
@@ -75,7 +78,7 @@ class NovelDownloaderService : Service() {
builder =
NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER_PROGRESS).apply {
setContentTitle("Novel Download Progress")
setSmallIcon(R.drawable.ic_round_download_24)
setSmallIcon(R.drawable.ic_download_24)
priority = NotificationCompat.PRIORITY_DEFAULT
setOnlyAlertOnce(true)
setProgress(0, 0, false)
@@ -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,17 +228,24 @@ 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
}
val baseDirectory = getSubDirectory(
this@NovelDownloaderService,
MediaType.NOVEL,
false,
task.title
) ?: throw Exception("Directory not found")
// Start the download
withContext(Dispatchers.IO) {
try {
@@ -245,27 +255,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 +314,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 +329,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
}
}
@@ -328,34 +341,31 @@ class NovelDownloaderService : Service() {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
saveMediaInfo(task)
saveMediaInfo(task, baseDirectory)
downloadsManager.addDownload(
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}")
FirebaseCrashlytics.getInstance().recordException(e)
Injekt.get<CrashlyticsInterface>().logException(e)
broadcastDownloadFailed(task.originalLink)
}
}
private fun saveMediaInfo(task: DownloadTask) {
GlobalScope.launch(Dispatchers.IO) {
val directory = File(
getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),
"Dantotsu/Novel/${task.title}"
)
if (!directory.exists()) directory.mkdirs()
val file = File(directory, "media.json")
@OptIn(DelicateCoroutinesApi::class)
private fun saveMediaInfo(task: DownloadTask, directory: DocumentFile) {
launchIO {
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

@@ -3,21 +3,13 @@ package ani.dantotsu.download.video
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
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.core.content.ContextCompat.getString
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.MimeTypes
import androidx.media3.common.TrackSelectionParameters
import androidx.media3.common.util.UnstableApi
import androidx.media3.database.StandaloneDatabaseProvider
import androidx.media3.datasource.DataSource
@@ -25,104 +17,113 @@ 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 androidx.media3.ui.TrackSelectionDialogBuilder
import ani.dantotsu.R
import ani.dantotsu.defaultHeaders
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.anime.AnimeDownloaderService
import ani.dantotsu.download.anime.AnimeServiceDataSingleton
import ani.dantotsu.logError
import ani.dantotsu.media.Media
import ani.dantotsu.okHttpClient
import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.SubtitleType
import ani.dantotsu.media.MediaType
import ani.dantotsu.parsers.Video
import ani.dantotsu.parsers.VideoType
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.util.Logger
import ani.dantotsu.util.customAlertDialog
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: List<Pair<String, String>> = emptyList(),
audio: List<Pair<String, String>> = emptyList(),
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,
audio,
sourceMedia,
episodeImage
)
val downloadsManager = Injekt.get<DownloadsManager>()
val downloadCheck = downloadsManager
.queryDownload(title, episode, MediaType.ANIME)
if (downloadCheck) {
context.customAlertDialog().apply {
setTitle("Download Exists")
setMessage("A download for this episode already exists. Do you want to overwrite it?")
setPosButton(R.string.yes) {
PrefManager.getAnimeDownloadPreferences().edit()
.remove(animeDownloadTask.getTaskName())
.apply()
downloadsManager.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
}
}
}
setNegButton(R.string.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>()
@@ -159,15 +160,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")
}
}
}
@@ -177,101 +178,7 @@ object Helper {
}
}
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,
context.getSharedPreferences(
getString(context, R.string.anime_downloads),
Context.MODE_PRIVATE
).getString(
animeDownloadTask.getTaskName(),
""
) ?: "",
false
)
context.getSharedPreferences(
getString(context, R.string.anime_downloads),
Context.MODE_PRIVATE
).edit()
.remove(animeDownloadTask.getTaskName())
.apply()
downloadsManger.removeDownload(
DownloadedType(
title,
episode,
DownloadedType.Type.ANIME
)
)
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
if (!AnimeServiceDataSingleton.isServiceRunning) {
val intent = Intent(context, AnimeDownloaderService::class.java)
ContextCompat.startForegroundService(context, intent)
AnimeServiceDataSingleton.isServiceRunning = true
}
}
.setNegativeButton("No") { _, _ -> }
.show()
} else {
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
if (!AnimeServiceDataSingleton.isServiceRunning) {
val intent = Intent(context, AnimeDownloaderService::class.java)
ContextCompat.startForegroundService(context, intent)
AnimeServiceDataSingleton.isServiceRunning = true
}
}
}
@Deprecated("exoplayer download manager is no longer used")
@OptIn(UnstableApi::class)
fun getSimpleCache(context: Context): SimpleCache {
return if (simpleCache == null) {
@@ -284,13 +191,27 @@ object Helper {
}
}
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

@@ -2,7 +2,6 @@ package ani.dantotsu.home
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
@@ -23,21 +22,23 @@ import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.bottomBar
import ani.dantotsu.connections.anilist.AniMangaSearchResults
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistAnimeViewModel
import ani.dantotsu.connections.anilist.SearchResults
import ani.dantotsu.connections.anilist.getUserId
import ani.dantotsu.databinding.FragmentAnimeBinding
import ani.dantotsu.loadData
import ani.dantotsu.media.MediaAdaptor
import ani.dantotsu.media.ProgressAdapter
import ani.dantotsu.media.SearchActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.px
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -50,9 +51,6 @@ class AnimeFragment : Fragment() {
private val binding get() = _binding!!
private lateinit var animePageAdapter: AnimePageAdapter
private var uiSettings: UserInterfaceSettings =
loadData("ui_settings") ?: UserInterfaceSettings()
val model: AnilistAnimeViewModel by activityViewModels()
override fun onCreateView(
@@ -102,7 +100,7 @@ class AnimeFragment : Fragment() {
var loading = true
if (model.notSet) {
model.notSet = false
model.searchResults = SearchResults(
model.aniMangaSearchResults = AniMangaSearchResults(
"ANIME",
isAdult = false,
onList = false,
@@ -111,7 +109,7 @@ class AnimeFragment : Fragment() {
sort = Anilist.sortBy[1]
)
}
val popularAdaptor = MediaAdaptor(1, model.searchResults.results, requireActivity())
val popularAdaptor = MediaAdaptor(1, model.aniMangaSearchResults.results, requireActivity())
val progressAdaptor = ProgressAdapter(searched = model.searched)
val adapter = ConcatAdapter(animePageAdapter, popularAdaptor, progressAdaptor)
binding.animePageRecyclerView.adapter = adapter
@@ -144,7 +142,7 @@ class AnimeFragment : Fragment() {
animePageAdapter.onIncludeListClick = { checked ->
oldIncludeList = !checked
loading = true
model.searchResults.results.clear()
model.aniMangaSearchResults.results.clear()
popularAdaptor.notifyDataSetChanged()
scope.launch(Dispatchers.IO) {
model.loadPopular("ANIME", sort = Anilist.sortBy[1], onList = checked)
@@ -154,17 +152,17 @@ class AnimeFragment : Fragment() {
model.getPopular().observe(viewLifecycleOwner) {
if (it != null) {
if (oldIncludeList == (it.onList != false)) {
val prev = model.searchResults.results.size
model.searchResults.results.addAll(it.results)
val prev = model.aniMangaSearchResults.results.size
model.aniMangaSearchResults.results.addAll(it.results)
popularAdaptor.notifyItemRangeInserted(prev, it.results.size)
} else {
model.searchResults.results.addAll(it.results)
model.aniMangaSearchResults.results.addAll(it.results)
popularAdaptor.notifyDataSetChanged()
oldIncludeList = it.onList ?: true
}
model.searchResults.onList = it.onList
model.searchResults.hasNextPage = it.hasNextPage
model.searchResults.page = it.page
model.aniMangaSearchResults.onList = it.onList
model.aniMangaSearchResults.hasNextPage = it.hasNextPage
model.aniMangaSearchResults.page = it.page
if (it.hasNextPage)
progressAdaptor.bar?.visibility = View.VISIBLE
else {
@@ -179,10 +177,10 @@ class AnimeFragment : Fragment() {
RecyclerView.OnScrollListener() {
override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) {
if (!v.canScrollVertically(1)) {
if (model.searchResults.hasNextPage && model.searchResults.results.isNotEmpty() && !loading) {
if (model.aniMangaSearchResults.hasNextPage && model.aniMangaSearchResults.results.isNotEmpty() && !loading) {
scope.launch(Dispatchers.IO) {
loading = true
model.loadNextPage(model.searchResults)
model.loadNextPage(model.aniMangaSearchResults)
}
}
}
@@ -208,7 +206,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) {
@@ -217,7 +230,7 @@ class AnimeFragment : Fragment() {
if (it != null) {
animePageAdapter.updateTrending(
MediaAdaptor(
if (uiSettings.smallView) 3 else 2,
if (PrefManager.getVal(PrefName.SmallView)) 3 else 2,
it,
requireActivity(),
viewPager = animePageAdapter.trendingViewPager
@@ -257,22 +270,44 @@ 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.loadPopular("ANIME", sort = Anilist.sortBy[1], onList = requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
.getBoolean("popular_list", false))
}
model.loaded = true
val loadTrending = async(Dispatchers.IO) { model.loadTrending(1) }
val loadAll = async(Dispatchers.IO) { model.loadAll() }
val loadPopular = async(Dispatchers.IO) {
model.loadPopular(
"ANIME",
sort = Anilist.sortBy[1],
onList = PrefManager.getVal(PrefName.PopularAnimeList)
)
}
loadTrending.await()
loadAll.await()
loadPopular.await()
live.postValue(false)
_binding?.animeRefresh?.isRefreshing = false
running = false
}
}
}
@@ -284,7 +319,9 @@ class AnimeFragment : Fragment() {
binding.root.requestApplyInsets()
binding.root.requestLayout()
}
if (this::animePageAdapter.isInitialized && _binding != null) {
animePageAdapter.updateNotificationCount()
}
super.onResume()
}
}

View File

@@ -1,16 +1,16 @@
package ani.dantotsu.home
import android.content.Context
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
@@ -20,20 +20,25 @@ 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.ItemAnimePageBinding
import ani.dantotsu.loadData
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
import ani.dantotsu.setSlideUp
import ani.dantotsu.settings.SettingsDialogFragment
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.statusBarHeight
import com.google.android.material.card.MaterialCardView
import com.google.android.material.textfield.TextInputLayout
@@ -41,11 +46,10 @@ 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
private var uiSettings: UserInterfaceSettings =
loadData("ui_settings") ?: UserInterfaceSettings()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnimePageViewHolder {
val binding =
@@ -55,53 +59,67 @@ 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)
trendingBinding.titleContainer.updatePadding(top = statusBarHeight)
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("colorOverflow", false) ?: false
if (!colorOverflow) {
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
}
binding.animeTitleContainer.updatePadding(top = statusBarHeight)
if (uiSettings.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 {
ContextCompat.startActivity(
it.context,
Intent(it.context, SearchActivity::class.java).putExtra("type", "ANIME"),
null
)
trendingBinding.searchBar.hint = binding.root.context.getString(R.string.search)
trendingBinding.searchBarText.setOnClickListener {
val context = binding.root.context
if (PrefManager.getVal(PrefName.AniMangaSearchDirect) && Anilist.token != null) {
ContextCompat.startActivity(
context,
Intent(context, SearchActivity::class.java).putExtra("type", "ANIME"),
null
)
} else {
SearchBottomSheet.newInstance().show(
(context as AppCompatActivity).supportFragmentManager,
"search"
)
}
}
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.isVisible = Anilist.unreadNotificationCount > 0
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
listOf(
binding.animePreviousSeason,
@@ -130,17 +148,14 @@ 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 = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("popular_list", true) ?: true
binding.animeIncludeList.isChecked = PrefManager.getVal(PrefName.PopularAnimeList)
binding.animeIncludeList.setOnCheckedChangeListener { _, isChecked ->
onIncludeListClick.invoke(isChecked)
currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
?.putBoolean("popular_list", isChecked)?.apply()
PrefManager.setVal(PrefName.PopularAnimeList, isChecked)
}
if (ready.value == false)
ready.postValue(true)
@@ -157,59 +172,146 @@ 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 =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
binding.animeTitleContainer.startAnimation(setSlideUp(uiSettings))
trendingBinding.trendingViewPager.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
trendingBinding.titleContainer.startAnimation(setSlideUp())
binding.animeListContainer.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
LayoutAnimationController(setSlideIn(), 0.25f)
binding.animeSeasonsCont.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
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(uiSettings))
binding.animeUpdatedRecyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
binding.animePopular.visibility = View.VISIBLE
binding.animePopular.startAnimation(setSlideUp(uiSettings))
more.setOnClickListener {
MediaListViewActivity.passedMedia = media.toCollection(ArrayList())
ContextCompat.startActivity(
it.context, Intent(it.context, MediaListViewActivity::class.java)
.putExtra("title", string),
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)
}
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.isVisible = Anilist.unreadNotificationCount > 0
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
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.loadData
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.UserInterfaceSettings
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.Dispatchers
import kotlinx.coroutines.async
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,16 +77,23 @@ class HomeFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val scope = lifecycleScope
var uiSettings = loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
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 (!uiSettings.bannerAnimations) 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
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
binding.homeAnimeList.setOnClickListener {
ContextCompat.startActivity(
@@ -98,14 +112,14 @@ class HomeFragment : Fragment() {
)
}
binding.homeUserAvatarContainer.startAnimation(setSlideUp(uiSettings))
binding.homeUserAvatarContainer.startAnimation(setSlideUp())
binding.homeUserDataContainer.visibility = View.VISIBLE
binding.homeUserDataContainer.layoutAnimation =
LayoutAnimationController(setSlideUp(uiSettings), 0.25f)
LayoutAnimationController(setSlideUp(), 0.25f)
binding.homeAnimeList.visibility = View.VISIBLE
binding.homeMangaList.visibility = View.VISIBLE
binding.homeListContainer.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
LayoutAnimationController(setSlideIn(), 0.25f)
}
else {
snackString(currContext()?.getString(R.string.please_reload))
@@ -119,26 +133,44 @@ class HomeFragment : Fragment() {
"dialog"
)
}
binding.searchImageContainer.setSafeOnClickListener {
SearchBottomSheet.newInstance().show(
(it.context as androidx.appcompat.app.AppCompatActivity).supportFragmentManager,
"search"
)
}
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 = (uiSettings.animationSpeed * 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)
val duration = ((PrefManager.getVal(PrefName.AnimationSpeed) as Float) * 200).toLong()
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()
}
}
}
}
@@ -185,13 +217,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
@@ -204,15 +239,25 @@ class HomeFragment : Fragment() {
LinearLayoutManager.HORIZONTAL,
false
)
more.setOnClickListener { i ->
MediaListViewActivity.passedMedia = it
ContextCompat.startActivity(
i.context, Intent(i.context, MediaListViewActivity::class.java)
.putExtra("title", string),
null
)
}
recyclerView.visibility = View.VISIBLE
recyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
LayoutAnimationController(setSlideIn(), 0.25f)
} else {
empty.visibility = View.VISIBLE
}
more.visibility = View.VISIBLE
title.visibility = View.VISIBLE
title.startAnimation(setSlideUp(uiSettings))
more.startAnimation(setSlideUp())
title.startAnimation(setSlideUp())
progress.visibility = View.GONE
}
}
@@ -226,7 +271,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)
@@ -238,7 +285,9 @@ class HomeFragment : Fragment() {
binding.homeFavAnimeRecyclerView,
binding.homeFavAnimeProgressBar,
binding.homeFavAnimeEmpty,
binding.homeFavAnime
binding.homeFavAnime,
binding.homeFavAnimeMore,
getString(R.string.fav_anime)
)
initRecyclerView(
@@ -247,7 +296,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)
@@ -259,7 +310,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)
@@ -271,7 +324,9 @@ class HomeFragment : Fragment() {
binding.homeFavMangaRecyclerView,
binding.homeFavMangaProgressBar,
binding.homeFavMangaEmpty,
binding.homeFavManga
binding.homeFavManga,
binding.homeFavMangaMore,
getString(R.string.fav_manga)
)
initRecyclerView(
@@ -280,7 +335,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)
@@ -292,28 +349,105 @@ 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)
binding.homeUserAvatarContainer.startAnimation(setSlideUp(uiSettings))
} else {
binding.homeUserStatusContainer.visibility = View.GONE
}
binding.homeUserStatusProgressBar.visibility = View.GONE
}
model.empty.observe(viewLifecycleOwner) {
}
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 { _ ->
MediaListViewActivity.passedMedia = it
ContextCompat.startActivity(
requireActivity(),
Intent(requireActivity(), MediaListViewActivity::class.java)
.putExtra("title", getString(R.string.hidden)),
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)
{
binding.homeDantotsuContainer.visibility = if (it == true) View.VISIBLE else View.GONE
(binding.homeDantotsuIcon.drawable as Animatable).start()
binding.homeDantotsuContainer.startAnimation(setSlideUp(uiSettings))
binding.homeDantotsuContainer.startAnimation(setSlideUp())
binding.homeDantotsuIcon.setSafeOnClickListener {
(binding.homeDantotsuIcon.drawable as Animatable).start()
}
}
val array = arrayOf(
Runnable { runBlocking { model.setAnimeContinue() } },
Runnable { runBlocking { model.setAnimeFav() } },
Runnable { runBlocking { model.setAnimePlanned() } },
Runnable { runBlocking { model.setMangaContinue() } },
Runnable { runBlocking { model.setMangaFav() } },
Runnable { runBlocking { model.setMangaPlanned() } },
Runnable { runBlocking { model.setRecommendation() } }
"AnimeContinue",
"AnimeFav",
"AnimePlanned",
"MangaContinue",
"MangaFav",
"MangaPlanned",
"Recommendation",
"UserStatus",
)
val containers = arrayOf(
@@ -323,35 +457,62 @@ 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) { shouldRefresh ->
if (!running && shouldRefresh) {
running = true
scope.launch {
uiSettings =
loadData<UserInterfaceSettings>("ui_settings") ?: UserInterfaceSettings()
withContext(Dispatchers.IO) {
//Get userData First
getUserId(requireContext()) {
load()
// Get user data first
Anilist.userid =
PrefManager.getNullableVal<String>(PrefName.AnilistUserId, null)
?.toIntOrNull()
if (Anilist.userid == null) {
withContext(Dispatchers.Main) {
getUserId(requireContext()) {
load()
}
}
} else {
getUserId(requireContext()) {
load()
}
}
model.loaded = true
model.setListImages()
var empty = true
(array.indices).forEach { i ->
if (uiSettings.homeLayoutShow[i]) {
array[i].run()
}
var empty = true
val homeLayoutShow: List<Boolean> = PrefManager.getVal(PrefName.HomeLayout)
withContext(Dispatchers.Main) {
homeLayoutShow.indices.forEach { i ->
if (homeLayoutShow.elementAt(i)) {
empty = false
} else withContext(Dispatchers.Main) {
} else {
containers[i].visibility = View.GONE
}
}
model.empty.postValue(empty)
}
val initHomePage = async(Dispatchers.IO) { model.initHomePage() }
val initUserStatus = async(Dispatchers.IO) { model.initUserStatus() }
initHomePage.await()
initUserStatus.await()
withContext(Dispatchers.Main) {
model.empty.postValue(empty)
binding.homeHiddenItemsContainer.visibility = View.GONE
}
live.postValue(false)
_binding?.homeRefresh?.isRefreshing = false
running = false
}
}
}
@@ -359,6 +520,11 @@ class HomeFragment : Fragment() {
override fun onResume() {
if (!model.loaded) Refresh.activity[1]!!.postValue(true)
if (_binding != null) {
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
}
super.onResume()
}
}

View File

@@ -1,14 +1,23 @@
package ani.dantotsu.home
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.DialogUserAgentBinding
import ani.dantotsu.databinding.FragmentLoginBinding
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 ani.dantotsu.util.customAlertDialog
class LoginFragment : Fragment() {
@@ -29,5 +38,92 @@ class LoginFragment : Fragment() {
binding.loginDiscord.setOnClickListener { openLinkInBrowser(getString(R.string.discord)) }
binding.loginGithub.setOnClickListener { openLinkInBrowser(getString(R.string.github)) }
binding.loginTelegram.setOnClickListener { openLinkInBrowser(getString(R.string.telegram)) }
val openDocumentLauncher =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
if (uri != null) {
try {
val jsonString =
requireActivity().contentResolver.openInputStream(uri)?.readBytes()
?: throw Exception("Error reading file")
val name =
DocumentFile.fromSingleUri(requireActivity(), uri)?.name ?: "settings"
//.sani is encrypted, .ani is not
if (name.endsWith(".sani")) {
passwordAlertDialog { password ->
if (password != null) {
val salt = jsonString.copyOfRange(0, 16)
val encrypted = jsonString.copyOfRange(16, jsonString.size)
val decryptedJson = try {
PreferenceKeystore.decryptWithPassword(
password,
encrypted,
salt
)
} catch (e: Exception) {
toast("Incorrect password")
return@passwordAlertDialog
}
if (PreferencePackager.unpack(decryptedJson))
restartApp()
} else {
toast("Password cannot be empty")
}
}
} else if (name.endsWith(".ani")) {
val decryptedJson = jsonString.toString(Charsets.UTF_8)
if (PreferencePackager.unpack(decryptedJson))
restartApp()
} else {
toast("Invalid file type")
}
} catch (e: Exception) {
Logger.log(e)
toast("Error importing settings")
}
}
}
binding.importSettingsButton.setOnClickListener {
openDocumentLauncher.launch(arrayOf("*/*"))
}
}
private fun passwordAlertDialog(callback: (CharArray?) -> Unit) {
val password = CharArray(16).apply { fill('0') }
// Inflate the dialog layout
val dialogView = DialogUserAgentBinding.inflate(layoutInflater).apply {
userAgentTextBox.hint = "Password"
subtitle.visibility = View.VISIBLE
subtitle.text = getString(R.string.enter_password_to_decrypt_file)
}
requireActivity().customAlertDialog().apply {
setTitle("Enter Password")
setCustomView(dialogView.root)
setPosButton(R.string.ok) {
val editText = dialogView.userAgentTextBox
if (editText.text?.isNotBlank() == true) {
editText.text?.toString()?.trim()?.toCharArray(password)
callback(password)
} else {
toast("Password cannot be empty")
}
}
setNegButton(R.string.cancel) {
password.fill('0')
callback(null)
}
}.show()
}
private fun restartApp() {
val intent = Intent(requireActivity(), requireActivity().javaClass)
requireActivity().finish()
startActivity(intent)
}
}

View File

@@ -2,7 +2,6 @@ package ani.dantotsu.home
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
@@ -21,20 +20,22 @@ import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.Refresh
import ani.dantotsu.bottomBar
import ani.dantotsu.connections.anilist.AniMangaSearchResults
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistMangaViewModel
import ani.dantotsu.connections.anilist.SearchResults
import ani.dantotsu.connections.anilist.getUserId
import ani.dantotsu.databinding.FragmentMangaBinding
import ani.dantotsu.loadData
import ani.dantotsu.media.MediaAdaptor
import ani.dantotsu.media.ProgressAdapter
import ani.dantotsu.navBarHeight
import ani.dantotsu.px
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -46,9 +47,6 @@ class MangaFragment : Fragment() {
private val binding get() = _binding!!
private lateinit var mangaPageAdapter: MangaPageAdapter
private var uiSettings: UserInterfaceSettings =
loadData("ui_settings") ?: UserInterfaceSettings()
val model: AnilistMangaViewModel by activityViewModels()
override fun onCreateView(
@@ -96,7 +94,7 @@ class MangaFragment : Fragment() {
var loading = true
if (model.notSet) {
model.notSet = false
model.searchResults = SearchResults(
model.aniMangaSearchResults = AniMangaSearchResults(
"MANGA",
isAdult = false,
onList = false,
@@ -105,7 +103,7 @@ class MangaFragment : Fragment() {
sort = Anilist.sortBy[1]
)
}
val popularAdaptor = MediaAdaptor(1, model.searchResults.results, requireActivity())
val popularAdaptor = MediaAdaptor(1, model.aniMangaSearchResults.results, requireActivity())
val progressAdaptor = ProgressAdapter(searched = model.searched)
binding.mangaPageRecyclerView.adapter =
ConcatAdapter(mangaPageAdapter, popularAdaptor, progressAdaptor)
@@ -137,10 +135,10 @@ class MangaFragment : Fragment() {
RecyclerView.OnScrollListener() {
override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) {
if (!v.canScrollVertically(1)) {
if (model.searchResults.hasNextPage && model.searchResults.results.isNotEmpty() && !loading) {
if (model.aniMangaSearchResults.hasNextPage && model.aniMangaSearchResults.results.isNotEmpty() && !loading) {
scope.launch(Dispatchers.IO) {
loading = true
model.loadNextPage(model.searchResults)
model.loadNextPage(model.aniMangaSearchResults)
}
}
}
@@ -164,9 +162,38 @@ 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) {
@@ -175,7 +202,7 @@ class MangaFragment : Fragment() {
if (it != null) {
mangaPageAdapter.updateTrending(
MediaAdaptor(
if (uiSettings.smallView) 3 else 2,
if (PrefManager.getVal(PrefName.SmallView)) 3 else 2,
it,
requireActivity(),
viewPager = mangaPageAdapter.trendingViewPager
@@ -196,7 +223,7 @@ class MangaFragment : Fragment() {
mangaPageAdapter.onIncludeListClick = { checked ->
oldIncludeList = !checked
loading = true
model.searchResults.results.clear()
model.aniMangaSearchResults.results.clear()
popularAdaptor.notifyDataSetChanged()
scope.launch(Dispatchers.IO) {
model.loadPopular("MANGA", sort = Anilist.sortBy[1], onList = checked)
@@ -206,17 +233,17 @@ class MangaFragment : Fragment() {
model.getPopular().observe(viewLifecycleOwner) {
if (it != null) {
if (oldIncludeList == (it.onList != false)) {
val prev = model.searchResults.results.size
model.searchResults.results.addAll(it.results)
val prev = model.aniMangaSearchResults.results.size
model.aniMangaSearchResults.results.addAll(it.results)
popularAdaptor.notifyItemRangeInserted(prev, it.results.size)
} else {
model.searchResults.results.addAll(it.results)
model.aniMangaSearchResults.results.addAll(it.results)
popularAdaptor.notifyDataSetChanged()
oldIncludeList = it.onList ?: true
}
model.searchResults.onList = it.onList
model.searchResults.hasNextPage = it.hasNextPage
model.searchResults.page = it.page
model.aniMangaSearchResults.onList = it.onList
model.aniMangaSearchResults.hasNextPage = it.hasNextPage
model.aniMangaSearchResults.page = it.page
if (it.hasNextPage)
progressAdaptor.bar?.visibility = View.VISIBLE
else {
@@ -231,22 +258,46 @@ 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.loadPopular("MANGA", sort = Anilist.sortBy[1], onList = requireContext().getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
.getBoolean("popular_list", false) )
}
model.loaded = true
val loadTrending = async(Dispatchers.IO) { model.loadTrending() }
val loadAll = async(Dispatchers.IO) { model.loadAll() }
val loadPopular = async(Dispatchers.IO) {
model.loadPopular(
"MANGA",
sort = Anilist.sortBy[1],
onList = PrefManager.getVal(PrefName.PopularAnimeList)
)
}
loadTrending.await()
loadAll.await()
loadPopular.await()
live.postValue(false)
_binding?.mangaRefresh?.isRefreshing = false
running = false
}
}
}
@@ -259,6 +310,9 @@ class MangaFragment : Fragment() {
binding.root.requestApplyInsets()
binding.root.requestLayout()
}
if (this::mangaPageAdapter.isInitialized && _binding != null) {
mangaPageAdapter.updateNotificationCount()
}
super.onResume()
}

View File

@@ -1,16 +1,16 @@
package ani.dantotsu.home
import android.content.Context
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
@@ -20,19 +20,24 @@ 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.loadData
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
import ani.dantotsu.setSlideUp
import ani.dantotsu.settings.SettingsDialogFragment
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.statusBarHeight
import com.google.android.material.card.MaterialCardView
import com.google.android.material.textfield.TextInputLayout
@@ -40,11 +45,10 @@ 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
private var uiSettings: UserInterfaceSettings =
loadData("ui_settings") ?: UserInterfaceSettings()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MangaPageViewHolder {
val binding =
@@ -54,52 +58,64 @@ 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)
trendingBinding.titleContainer.updatePadding(top = statusBarHeight)
val colorOverflow = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("colorOverflow", false) ?: false
if (!colorOverflow) {
textInputLayout.boxBackgroundColor = (color and 0x00FFFFFF) or 0x28000000.toInt()
materialCardView.setCardBackgroundColor((color and 0x00FFFFFF) or 0x28000000.toInt())
}
binding.mangaTitleContainer.updatePadding(top = statusBarHeight)
if (uiSettings.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 {
ContextCompat.startActivity(
it.context,
Intent(it.context, SearchActivity::class.java).putExtra("type", "MANGA"),
null
)
trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
trendingBinding.searchBar.hint = binding.root.context.getString(R.string.search)
trendingBinding.searchBarText.setOnClickListener {
val context = binding.root.context
if (PrefManager.getVal(PrefName.AniMangaSearchDirect) && Anilist.token != null) {
ContextCompat.startActivity(
context,
Intent(context, SearchActivity::class.java).putExtra("type", "MANGA"),
null
)
} else {
SearchBottomSheet.newInstance().show(
(context as AppCompatActivity).supportFragmentManager,
"search"
)
}
}
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")
@@ -123,17 +139,14 @@ 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 = currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
?.getBoolean("popular_list", true) ?: true
binding.mangaIncludeList.isChecked = PrefManager.getVal(PrefName.PopularMangaList)
binding.mangaIncludeList.setOnCheckedChangeListener { _, isChecked ->
onIncludeListClick.invoke(isChecked)
currContext()?.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)?.edit()
?.putBoolean("popular_list", isChecked)?.apply()
PrefManager.setVal(PrefName.PopularMangaList, isChecked)
}
if (ready.value == false)
ready.postValue(true)
@@ -148,56 +161,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 =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
binding.mangaTitleContainer.startAnimation(setSlideUp(uiSettings))
trendingBinding.trendingViewPager.layoutAnimation =
LayoutAnimationController(setSlideIn(), 0.25f)
trendingBinding.titleContainer.startAnimation(setSlideUp())
binding.mangaListContainer.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
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(uiSettings))
binding.mangaNovelRecyclerView.layoutAnimation =
LayoutAnimationController(setSlideIn(uiSettings), 0.25f)
binding.mangaPopular.visibility = View.VISIBLE
binding.mangaPopular.startAnimation(setSlideUp(uiSettings))
more.setOnClickListener {
MediaListViewActivity.passedMedia = media.toCollection(ArrayList())
ContextCompat.startActivity(
it.context, Intent(it.context, MediaListViewActivity::class.java)
.putExtra("title", string),
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)
}
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.isVisible = Anilist.unreadNotificationCount > 0
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
}
}

View File

@@ -1,6 +1,5 @@
package ani.dantotsu.home
import android.content.Context
import android.graphics.drawable.GradientDrawable
import android.os.Build
import android.os.Bundle
@@ -23,43 +22,36 @@ import ani.dantotsu.databinding.ActivityNoInternetBinding
import ani.dantotsu.download.anime.OfflineAnimeFragment
import ani.dantotsu.download.manga.OfflineMangaFragment
import ani.dantotsu.initActivity
import ani.dantotsu.loadData
import ani.dantotsu.navBarHeight
import ani.dantotsu.offline.OfflineFragment
import ani.dantotsu.others.LangSet
import ani.dantotsu.selectedOption
import ani.dantotsu.settings.UserInterfaceSettings
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.themes.ThemeManager
import nl.joery.animatedbottombar.AnimatedBottomBar
class NoInternet : AppCompatActivity() {
private lateinit var binding: ActivityNoInternetBinding
lateinit var bottomBar: AnimatedBottomBar
private var uiSettings = UserInterfaceSettings()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
LangSet.setLocale(this)
ThemeManager(this).applyTheme()
binding = ActivityNoInternetBinding.inflate(layoutInflater)
setContentView(binding.root)
val _bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
val bottomBar = findViewById<AnimatedBottomBar>(R.id.navbar)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val backgroundDrawable = _bottomBar.background as GradientDrawable
val backgroundDrawable = bottomBar.background as GradientDrawable
val currentColor = backgroundDrawable.color?.defaultColor ?: 0
val semiTransparentColor = (currentColor and 0x00FFFFFF) or 0xE8000000.toInt()
backgroundDrawable.setColor(semiTransparentColor)
_bottomBar.background = backgroundDrawable
bottomBar.background = backgroundDrawable
}
val colorOverflow = this.getSharedPreferences("Dantotsu", Context.MODE_PRIVATE)
.getBoolean("colorOverflow", false)
if (!colorOverflow) {
_bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
bottomBar.background = ContextCompat.getDrawable(this, R.drawable.bottom_nav_gray)
}
var doubleBackToExitPressedOnce = false
onBackPressedDispatcher.addCallback(this) {
@@ -76,8 +68,7 @@ class NoInternet : AppCompatActivity() {
binding.root.doOnAttach {
initActivity(this)
uiSettings = loadData("ui_settings") ?: uiSettings
selectedOption = uiSettings.defaultStartUpTab
selectedOption = PrefManager.getVal(PrefName.DefaultStartUpTab)
binding.includedNavbar.navbarContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight
@@ -89,7 +80,7 @@ class NoInternet : AppCompatActivity() {
val mainViewPager = binding.viewpager
mainViewPager.isUserInputEnabled = false
mainViewPager.adapter = ViewPagerAdapter(supportFragmentManager, lifecycle)
mainViewPager.setPageTransformer(ZoomOutPageTransformer(uiSettings))
mainViewPager.setPageTransformer(ZoomOutPageTransformer())
navbar.setOnTabSelectListener(object :
AnimatedBottomBar.OnTabSelectListener {
override fun onTabSelected(

View File

@@ -0,0 +1,74 @@
package ani.dantotsu.home
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import ani.dantotsu.BottomSheetDialogFragment
import ani.dantotsu.connections.anilist.AnilistSearch.SearchType
import ani.dantotsu.connections.anilist.AnilistSearch.SearchType.Companion.toAnilistString
import ani.dantotsu.databinding.BottomSheetSearchBinding
import ani.dantotsu.media.SearchActivity
class SearchBottomSheet : BottomSheetDialogFragment() {
private var _binding: BottomSheetSearchBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = BottomSheetSearchBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.animeSearch.setOnClickListener {
startActivity(requireContext(), SearchType.ANIME)
dismiss()
}
binding.mangaSearch.setOnClickListener {
startActivity(requireContext(), SearchType.MANGA)
dismiss()
}
binding.characterSearch.setOnClickListener {
startActivity(requireContext(), SearchType.CHARACTER)
dismiss()
}
binding.staffSearch.setOnClickListener {
startActivity(requireContext(), SearchType.STAFF)
dismiss()
}
binding.studioSearch.setOnClickListener {
startActivity(requireContext(), SearchType.STUDIO)
dismiss()
}
binding.userSearch.setOnClickListener {
startActivity(requireContext(), SearchType.USER)
dismiss()
}
}
private fun startActivity(context: Context, type: SearchType) {
ContextCompat.startActivity(
context,
Intent(context, SearchActivity::class.java).putExtra("type", type.toAnilistString()),
null
)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
companion object {
fun newInstance() = SearchBottomSheet()
}
}

View File

@@ -0,0 +1,83 @@
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 = 12f
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
strokeCap = Paint.Cap.ROUND
}
@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,124 @@
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.home.status.listener.StoriesCallback
import ani.dantotsu.initActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.profile.User
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.Logger
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())
if (activity.getOrNull(position) != null) {
val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity)
val startIndex = if (startFrom > 0) startFrom else 0
binding.stories.setStoriesList(
activityList = activity[position].activity,
startIndex = startIndex + 1
)
} else {
Logger.log("index out of bounds for position $position of size ${activity.size}")
finish()
}
}
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()
if (hasWindowFocus())
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, startIndex + 1)
binding.stories.startAnimation(slideInRight)
} else {
finish()
}
}
override fun onStoriesStart() {
position -= 1
if (position >= 0 && activity[position].activity.isNotEmpty()) {
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, startIndex + 1)
binding.stories.startAnimation(slideInLeft)
} else {
finish()
}
}
companion object {
var user: ArrayList<User> = arrayListOf()
}
}

View File

@@ -0,0 +1,539 @@
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.core.view.isVisible
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.profile.activity.RepliesBottomDialog
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
import kotlin.math.abs
class Stories @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr), View.OnTouchListener {
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.touchPanel.setOnTouchListener(this)
}
fun setStoriesList(
activityList: List<Activity>, startIndex: Int = 1
) {
this.activityList = activityList
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) {
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 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()
}
@SuppressLint("ClickableViewAccessibility")
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
)
}
binding.textActivity.setOnTouchListener { v, event ->
onTouchView(v, event, true)
v.onTouchEvent(event)
}
binding.textActivityContainer.setOnTouchListener { v, event ->
onTouchView(v, event, true)
v.onTouchEvent(event)
}
fun visible(isList: Boolean) {
binding.textActivity.isVisible = !isList
binding.textActivityContainer.isVisible = !isList
binding.infoText.isVisible = isList
binding.coverImage.isVisible = isList
binding.infoText.visibility = if (isList) View.VISIBLE else View.INVISIBLE
binding.infoText.text = ""
binding.contentImageViewKen.isVisible = isList
binding.contentImageView.isVisible = isList
}
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") &&
!story.status.contains("paused") &&
!story.status.contains("dropped")
) {
"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(
(it.context as FragmentActivity),
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.replyCount.text = story.replyCount.toString()
binding.activityReplies.setColorFilter(ContextCompat.getColor(context, R.color.bg_opp))
binding.activityRepliesContainer.setOnClickListener {
RepliesBottomDialog.newInstance(story.id)
.show((it.context as FragmentActivity).supportFragmentManager, "replies")
}
binding.activityLike.setColorFilter(if (story.isLiked == true) likeColor else notLikeColor)
binding.activityLikeCount.text = story.likeCount.toString()
binding.activityLikeContainer.setOnClickListener {
like()
}
binding.activityLikeContainer.setOnLongClickListener {
UsersDialogFragment().apply {
userList(userList)
show((it.context as FragmentActivity).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.mutation.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")
}
}
}
}
private var startClickTime = 0L
private var startX = 0f
private var startY = 0f
private var isLongPress = false
private val swipeThreshold = 100
override fun onTouch(view: View, event: MotionEvent): Boolean {
onTouchView(view, event)
return true
}
private fun onTouchView(view: View, event: MotionEvent, isText: Boolean = false) {
val maxClickDuration = 200
val screenWidth = view.width
val leftHalf = screenWidth / 2
val leftQuarter = screenWidth * 0.15
val rightQuarter = screenWidth * 0.85
when (event.action) {
MotionEvent.ACTION_DOWN -> {
startX = event.x
startY = event.y
startClickTime = Calendar.getInstance().timeInMillis
pause()
isLongPress = false
}
MotionEvent.ACTION_MOVE -> {
val deltaX = event.x - startX
val deltaY = event.y - startY
if (!isLongPress && (abs(deltaX) > swipeThreshold || abs(deltaY) > swipeThreshold)) {
isLongPress = true
}
}
MotionEvent.ACTION_UP -> {
val clickDuration = Calendar.getInstance().timeInMillis - startClickTime
if (isText) {
if (clickDuration < maxClickDuration && !isLongPress) {
if (event.x < leftQuarter) {
leftPanelTouch()
} else if (event.x > rightQuarter) {
rightPanelTouch()
} else {
resume()
}
} else {
resume()
}
} else {
if (clickDuration < maxClickDuration && !isLongPress) {
if (event.x < leftHalf) {
leftPanelTouch()
} else {
rightPanelTouch()
}
} else {
resume()
}
}
val deltaX = event.x - startX
val deltaY = event.y - startY
if (abs(deltaX) > swipeThreshold && !(abs(deltaY) > 10)) {
if (deltaX > 0) onStoriesPrevious()
else onStoriesCompleted()
}
}
}
}
}

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,92 @@
package ani.dantotsu.home.status
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
import ani.dantotsu.snackString
import ani.dantotsu.util.ActivityMarkdownCreator
class UserStatusAdapter(private val user: ArrayList<User>) :
RecyclerView.Adapter<UserStatusAdapter.UsersViewHolder>() {
inner class UsersViewHolder(val binding: ItemUserStatusBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
itemView.setOnClickListener {
if (user[bindingAdapterPosition].activity.isEmpty()) {
snackString("No activity")
return@setOnClickListener
}
StatusActivity.user = user
ContextCompat.startActivity(
itemView.context,
Intent(
itemView.context,
StatusActivity::class.java
).putExtra("position", bindingAdapterPosition),
null
)
}
itemView.setOnLongClickListener {
if (user[bindingAdapterPosition].id == Anilist.userid) {
ContextCompat.startActivity(
itemView.context,
Intent(itemView.context, ActivityMarkdownCreator::class.java)
.putExtra("type", "activity"),
null
)
} else {
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
}

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