Compare commits

...

173 Commits

Author SHA1 Message Date
Ankit Grai
c3f6d0ecee fix : dialog key names (#605) 2025-05-22 12:51:18 +05:30
aayush262
5124d6a2d8 fix: discord login 2025-05-22 12:50:06 +05:30
Ankit Grai
e83a0fe7da fix : long press progress dialog reset not working (#603) 2025-05-19 17:11:24 +05:30
rebel onion
61a8350043 fix: avoid waiting on network for local exts 2025-05-15 01:47:06 -05:00
rebel onion
baffbc845c fix: help bounds check /w custom speeds 2025-05-15 01:16:23 -05:00
rebel onion
afd9f6b884 fix: subtitles not showing 2025-05-15 01:13:47 -05:00
rebel onion
7d0894cd92 chore: bump extension interface 2025-05-14 22:35:50 -05:00
Rishvaish
dec2ed7959 hope for the best
* Update README.md

* To install multiple mangas

users can enter the value required to install as there is an EditText field instead of the Text View

* Issues

1)Installation of many mangas at same time now made to one to increase the installation efficiency
2)Installation order from the latest progresses chapter to the limit index
3)Tried to resolve the app crash bug

* Issues

1)Installation of many mangas at same time now made to one to increase the installation efficiency
2)Installation order from the latest progresses chapter to the limit index
3)Tried to resolve the app crash bug

---------

Co-authored-by: rebel onion <87634197+rebelonion@users.noreply.github.com>
2025-04-23 14:58:42 +05:30
aayush262
e4630df3e0 move stuff to dev (#587)
* Update README.md

* Fixed missing manga pages when downloading (#586)

* To install multiple mangas (#582)

users can enter the value required to install as there is an EditText field instead of the Text View

---------

Co-authored-by: rebel onion <87634197+rebelonion@users.noreply.github.com>
Co-authored-by: Daniele Santoru <30676094+danyev3@users.noreply.github.com>
Co-authored-by: Rishvaish <68911202+rishabpuranika@users.noreply.github.com>
2025-04-02 10:52:58 +05:30
Sadwhy
6fd3515d2c stuff (#567)
* Add blur to dialog for devices that support it

* More adjustable seek time

* Bump exo player
2025-01-17 13:03:01 +05:30
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
280 changed files with 14484 additions and 6634 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,17 +1,25 @@
name: Build APK and Notify Discord name: Build APK and Notify Discord
on: on:
push: push:
branches: branches-ignore:
- dev - main
- l10n_dev_crowdin
- custom-download-location
paths-ignore: paths-ignore:
- '**/README.md' - '**/README.md'
tags:
- "v*.*.*"
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: write
env: env:
CI: true CI: true
SKIP_BUILD: false
steps: steps:
- name: Checkout repo - name: Checkout repo
@@ -19,14 +27,12 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Download last SHA artifact - name: Download last SHA artifact
uses: dawidd6/action-download-artifact@v3 uses: dawidd6/action-download-artifact@v6
with: with:
workflow: beta.yml workflow: beta.yml
name: last-sha name: last-sha
path: . path: .
continue-on-error: true continue-on-error: true
- name: Get Commits Since Last Run - name: Get Commits Since Last Run
@@ -39,7 +45,9 @@ jobs:
fi fi
echo "Commits since $LAST_SHA:" echo "Commits since $LAST_SHA:"
# Accumulate commit logs in a shell variable # Accumulate commit logs in a shell variable
COMMIT_LOGS=$(git log $LAST_SHA..HEAD --pretty=format:"● %s ~%an") 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 # URL-encode the newline characters for GitHub Actions
COMMIT_LOGS="${COMMIT_LOGS//'%'/'%25'}" COMMIT_LOGS="${COMMIT_LOGS//'%'/'%25'}"
COMMIT_LOGS="${COMMIT_LOGS//$'\n'/'%0A'}" COMMIT_LOGS="${COMMIT_LOGS//$'\n'/'%0A'}"
@@ -49,6 +57,10 @@ jobs:
# Debugging: Print the variable to check its content # Debugging: Print the variable to check its content
echo "$COMMIT_LOGS" echo "$COMMIT_LOGS"
echo "$COMMIT_LOGS" > commit_log.txt 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} shell: /usr/bin/bash -e {0}
env: env:
CI: true CI: true
@@ -65,7 +77,11 @@ jobs:
echo "Version $VERSION" echo "Version $VERSION"
echo "VERSION=$VERSION" >> $GITHUB_ENV echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: List files in the directory
run: ls -l
- name: Setup JDK 17 - name: Setup JDK 17
if: ${{ env.SKIP_BUILD != 'true' }}
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
distribution: 'temurin' distribution: 'temurin'
@@ -73,18 +89,28 @@ jobs:
cache: gradle cache: gradle
- name: Decode Keystore File - name: Decode Keystore File
if: ${{ github.repository == 'rebelonion/Dantotsu' }}
run: echo "${{ secrets.KEYSTORE_FILE }}" | base64 -d > $GITHUB_WORKSPACE/key.keystore run: echo "${{ secrets.KEYSTORE_FILE }}" | base64 -d > $GITHUB_WORKSPACE/key.keystore
- name: List files in the directory
run: ls -l
- name: Make gradlew executable - name: Make gradlew executable
if: ${{ env.SKIP_BUILD != 'true' }}
run: chmod +x ./gradlew run: chmod +x ./gradlew
- name: Build with Gradle - name: Build with Gradle
run: ./gradlew assembleGoogleAlpha -Pandroid.injected.signing.store.file=$GITHUB_WORKSPACE/key.keystore -Pandroid.injected.signing.store.password=${{ secrets.KEYSTORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.KEY_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.KEY_PASSWORD }} 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 - name: Upload a Build Artifact
if: ${{ env.SKIP_BUILD != 'true' }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: Dantotsu name: Dantotsu
@@ -96,21 +122,232 @@ jobs:
if: ${{ github.repository == 'rebelonion/Dantotsu' }} if: ${{ github.repository == 'rebelonion/Dantotsu' }}
shell: bash shell: bash
run: | run: |
#Discord # 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"
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/') commit_messages=$(echo "$COMMIT_LOG" | sed 's/%0A/\n/g; s/^/\n/')
# Truncate commit messages if they are too long if [ ${#developers} -gt $max_length ]; then
max_length=1900 # Adjust this value as needed developers="${developers:0:$max_length}... (truncated)"
fi
if [ ${#commit_messages} -gt $max_length ]; then if [ ${#commit_messages} -gt $max_length ]; then
commit_messages="${commit_messages:0:$max_length}... (truncated)" commit_messages="${commit_messages:0:$max_length}... (truncated)"
fi fi
contentbody=$( jq -nc --arg msg "Alpha-Build: <@&1225347048321191996> **$VERSION**:" --arg commits "$commit_messages" '{"content": ($msg + "\n" + $commits)}' )
curl -F "payload_json=${contentbody}" -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }}
#Telegram # Construct Discord payload
curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \ discord_data=$(jq -nc \
-F "document=@app/build/outputs/apk/google/alpha/app-google-universal-alpha.apk" \ --arg field_value "$commit_messages" \
-F "caption=Alpha-Build: ${VERSION}: ${commit_messages}" \ --arg author_value "$developers" \
https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument --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: env:
COMMIT_LOG: ${{ env.COMMIT_LOG }} COMMIT_LOG: ${{ env.COMMIT_LOG }}

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
});

6
.gitignore vendored
View File

@@ -2,6 +2,9 @@
.gradle/ .gradle/
build/ build/
#kotlin
.kotlin/
# Local configuration file (sdk path, etc) # Local configuration file (sdk path, etc)
local.properties local.properties
@@ -34,3 +37,6 @@ scripts/
#crowdin #crowdin
crowdin.yml crowdin.yml
#vscode
.vscode

View File

@@ -14,7 +14,24 @@ 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! > **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> ## 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! ### 🚀 STAR THIS REPOSITORY TO SUPPORT THE DEVELOPER AND ENCOURAGE THE DEVELOPMENT OF THE APPLICATION!
@@ -38,4 +55,4 @@ You can come hang out with our awesome community, request new features, and repo
## LICENSE 📜 ## 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)

View File

@@ -11,15 +11,15 @@ def gitCommitHash = providers.exec {
}.standardOutput.asText.get().trim() }.standardOutput.asText.get().trim()
android { android {
compileSdk 34 compileSdk 35
defaultConfig { defaultConfig {
applicationId "ani.dantotsu" applicationId "ani.dantotsu"
minSdk 21 minSdk 21
targetSdk 34 targetSdk 35
versionCode((System.currentTimeMillis() / 60000).toInteger()) versionCode((System.currentTimeMillis() / 60000).toInteger())
versionName "3.1.0" versionName "3.2.1"
versionCode 300100000 versionCode versionName.split("\\.").collect { it.toInteger() * 100 }.join("") as Integer
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
} }
@@ -48,6 +48,10 @@ android {
manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_alpha_round" manifestPlaceholders.icon_placeholder_round = "@mipmap/ic_launcher_alpha_round"
debuggable System.getenv("CI") == null debuggable System.getenv("CI") == null
isDefault true isDefault true
debuggable true
jniDebuggable true
minifyEnabled false
shrinkResources false
} }
debug { debug {
applicationIdSuffix ".beta" applicationIdSuffix ".beta"
@@ -81,26 +85,29 @@ android {
dependencies { dependencies {
// FireBase // FireBase
googleImplementation platform('com.google.firebase:firebase-bom:33.0.0') googleImplementation platform('com.google.firebase:firebase-bom:33.13.0')
googleImplementation 'com.google.firebase:firebase-analytics-ktx:22.0.0' googleImplementation 'com.google.firebase:firebase-analytics-ktx:22.4.0'
googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:19.0.0' googleImplementation 'com.google.firebase:firebase-crashlytics-ktx:19.4.3'
// Core // Core
implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.browser:browser:1.8.0' implementation 'androidx.browser:browser:1.8.0'
implementation 'androidx.core:core-ktx:1.13.1' implementation 'androidx.core:core-ktx:1.16.0'
implementation 'androidx.fragment:fragment-ktx:1.6.2' implementation 'androidx.fragment:fragment-ktx:1.8.6'
implementation 'androidx.activity:activity-ktx:1.10.1'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.multidex:multidex:2.0.1'
implementation "androidx.work:work-runtime-ktx:2.9.0" implementation "androidx.work:work-runtime-ktx:2.10.1"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.github.Blatzar:NiceHttp:0.4.4' implementation 'com.github.Blatzar:NiceHttp:0.4.4'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3'
implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.webkit:webkit:1.11.0' implementation 'androidx.webkit:webkit:1.13.0'
implementation "com.anggrayudi:storage:1.5.5" implementation "com.anggrayudi:storage:1.5.5"
implementation "androidx.biometric:biometric:1.1.0"
// Glide // Glide
ext.glide_version = '4.16.0' ext.glide_version = '4.16.0'
@@ -111,7 +118,7 @@ dependencies {
implementation 'jp.wasabeef:glide-transformations:4.3.0' implementation 'jp.wasabeef:glide-transformations:4.3.0'
// Exoplayer // Exoplayer
ext.exo_version = '1.3.1' ext.exo_version = '1.6.1'
implementation "androidx.media3:media3-exoplayer:$exo_version" implementation "androidx.media3:media3-exoplayer:$exo_version"
implementation "androidx.media3:media3-ui:$exo_version" implementation "androidx.media3:media3-ui:$exo_version"
implementation "androidx.media3:media3-exoplayer-hls:$exo_version" implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
@@ -121,6 +128,8 @@ dependencies {
// Media3 Casting // Media3 Casting
implementation "androidx.media3:media3-cast:$exo_version" implementation "androidx.media3:media3-cast:$exo_version"
implementation "androidx.mediarouter:mediarouter:1.7.0" implementation "androidx.mediarouter:mediarouter:1.7.0"
// Media3 extension
implementation "com.github.anilbeesetti.nextlib:nextlib-media3ext:0.8.4"
// UI // UI
implementation 'com.google.android.material:material:1.12.0' implementation 'com.google.android.material:material:1.12.0'
@@ -129,9 +138,9 @@ dependencies {
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
implementation 'com.alexvasilkov:gesture-views:2.8.3' implementation 'com.alexvasilkov:gesture-views:2.8.3'
implementation 'com.github.VipulOG:ebook-reader:0.1.6' implementation 'com.github.VipulOG:ebook-reader:0.1.6'
implementation 'androidx.paging:paging-runtime-ktx:3.2.1' implementation 'androidx.paging:paging-runtime-ktx:3.3.6'
implementation 'com.github.eltos:simpledialogfragments:v3.7' implementation 'com.github.eltos:simpledialogfragments:v3.7'
implementation 'com.github.AAChartModel:AAChartCore-Kotlin:7.2.1' implementation 'com.github.AAChartModel:AAChartCore-Kotlin:7.2.3'
// Markwon // Markwon
ext.markwon_version = '4.6.2' ext.markwon_version = '4.6.2'
@@ -157,14 +166,14 @@ dependencies {
implementation 'ru.beryukhov:flowreactivenetwork:1.0.4' implementation 'ru.beryukhov:flowreactivenetwork:1.0.4'
implementation 'ca.gosyer:voyager-navigator:1.0.0-rc07' implementation 'ca.gosyer:voyager-navigator:1.0.0-rc07'
implementation 'com.squareup.logcat:logcat:0.1' implementation 'com.squareup.logcat:logcat:0.1'
implementation 'com.github.inorichi.injekt:injekt-core:65b0440' implementation 'uy.kohesive.injekt:injekt-core:1.16.+'
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.12' implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.14'
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12' implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.14'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps' implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'
implementation 'com.squareup.okio:okio:3.8.0' implementation 'com.squareup.okio:okio:3.9.1'
implementation 'com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.12' implementation 'com.squareup.okhttp3:okhttp-brotli:5.0.0-alpha.14'
implementation 'org.jsoup:jsoup:1.16.1' implementation 'org.jsoup:jsoup:1.18.1'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.6.3' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-okio:1.7.3'
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0' implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
implementation 'com.github.tachiyomiorg:unifile:17bec43' implementation 'com.github.tachiyomiorg:unifile:17bec43'
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1' implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'

View File

@@ -1,9 +1,40 @@
package ani.dantotsu.others package ani.dantotsu.others
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.text.SimpleDateFormat
import java.util.Locale
object AppUpdater { object AppUpdater {
suspend fun check(activity: FragmentActivity, post: Boolean = false) { suspend fun check(activity: FragmentActivity, post: Boolean = false) {
//no-op // 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

@@ -18,7 +18,9 @@ import ani.dantotsu.Mapper
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.buildMarkwon import ani.dantotsu.buildMarkwon
import ani.dantotsu.client import ani.dantotsu.client
import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.decodeBase64ToString
import ani.dantotsu.logError import ani.dantotsu.logError
import ani.dantotsu.openLinkInBrowser import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
@@ -37,26 +39,88 @@ import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
object AppUpdater { 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) { suspend fun check(activity: FragmentActivity, post: Boolean = false) {
if (post) snackString(currContext()?.getString(R.string.checking_for_update)) if (post) snackString(currContext()?.getString(R.string.checking_for_update))
val repo = activity.getString(R.string.repo) val repo = activity.getString(R.string.repo)
tryWithSuspend { tryWithSuspend {
val (md, version) = if (BuildConfig.DEBUG) { val (md, version) = fetchUpdateInfo(repo, BuildConfig.DEBUG) ?: return@tryWithSuspend
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")
}
Logger.log("Git Version : $version") Logger.log("Git Version : $version")
val dontShow = PrefManager.getCustomVal("dont_ask_for_update_$version", false) val dontShow = PrefManager.getCustomVal("dont_ask_for_update_$version", false)
@@ -69,7 +133,11 @@ object AppUpdater {
) )
addView( addView(
TextView(activity).apply { TextView(activity).apply {
val markWon = buildMarkwon(activity, false) val markWon = try {
buildMarkwon(activity, false)
} catch (e: IllegalArgumentException) {
return@runOnUiThread
}
markWon.setMarkdown(this, md) markWon.setMarkdown(this, md)
} }
) )
@@ -85,17 +153,11 @@ object AppUpdater {
setPositiveButton(currContext()!!.getString(R.string.lets_go)) { setPositiveButton(currContext()!!.getString(R.string.lets_go)) {
MainScope().launch(Dispatchers.IO) { MainScope().launch(Dispatchers.IO) {
try { try {
val apks = val apkUrl = fetchApkUrl(repo, version, BuildConfig.DEBUG)
client.get("https://api.github.com/repos/$repo/releases/tags/v$version") if (apkUrl != null) {
.parsed<GithubResponse>().assets?.filter { activity.downloadUpdate(version, apkUrl)
it.browserDownloadURL.endsWith( } else {
".apk" openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
)
}
val apkToDownload = apks?.first()
apkToDownload?.browserDownloadURL.apply {
if (this != null) activity.downloadUpdate(version, this)
else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
} }
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
@@ -108,8 +170,7 @@ object AppUpdater {
} }
show(activity.supportFragmentManager, "dialog") show(activity.supportFragmentManager, "dialog")
} }
} } else {
else {
if (post) snackString(currContext()?.getString(R.string.no_update_found)) if (post) snackString(currContext()?.getString(R.string.no_update_found))
} }
} }
@@ -140,8 +201,7 @@ object AppUpdater {
//Blatantly kanged from https://github.com/LagradOst/CloudStream-3/blob/master/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt //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)) toast(getString(R.string.downloading_update, version))
val downloadManager = this.getSystemService<DownloadManager>()!! val downloadManager = this.getSystemService<DownloadManager>()!!
@@ -163,7 +223,7 @@ object AppUpdater {
logError(e) logError(e)
-1 -1
} }
if (id == -1L) return true if (id == -1L) return
ContextCompat.registerReceiver( ContextCompat.registerReceiver(
this, this,
object : BroadcastReceiver() { object : BroadcastReceiver() {
@@ -184,7 +244,6 @@ object AppUpdater {
}, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), }, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
ContextCompat.RECEIVER_EXPORTED ContextCompat.RECEIVER_EXPORTED
) )
return true
} }
private fun openApk(context: Context, uri: Uri) { private fun openApk(context: Context, uri: Uri) {

View File

@@ -19,6 +19,7 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" /> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission <uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" /> android:maxSdkVersion="29" />
@@ -112,10 +113,9 @@
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/epub+zip" /> <data android:mimeType="application/epub+zip"/>
<data android:mimeType="application/x-mobipocket-ebook" /> <data android:mimeType="application/x-mobipocket-ebook" />
<data android:mimeType="application/vnd.amazon.ebook" /> <data android:mimeType="application/vnd.amazon.ebook" />
<data android:mimeType="application/fb2+zip" /> <data android:mimeType="application/fb2+zip" />
@@ -131,10 +131,11 @@
</activity> </activity>
<activity android:name=".others.calc.CalcActivity" <activity android:name=".others.calc.CalcActivity"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
<activity android:name=".settings.FAQActivity" /> <activity android:name=".settings.AnilistSettingsActivity"/>
<activity android:name=".settings.ReaderSettingsActivity" />
<activity android:name=".settings.UserInterfaceSettingsActivity" /> <activity android:name=".settings.UserInterfaceSettingsActivity" />
<activity android:name=".settings.PlayerSettingsActivity" /> <activity android:name=".settings.PlayerSettingsActivity" />
<activity android:name=".settings.ReaderSettingsActivity" />
<activity android:name=".settings.FAQActivity" />
<activity <activity
android:name=".settings.SettingsActivity" android:name=".settings.SettingsActivity"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
@@ -155,7 +156,8 @@
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
<activity <activity
android:name=".settings.SettingsExtensionsActivity" android:name=".settings.SettingsExtensionsActivity"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity"
android:windowSoftInputMode="adjustPan"/>
<activity <activity
android:name=".settings.SettingsAddonActivity" android:name=".settings.SettingsAddonActivity"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
@@ -194,14 +196,15 @@
android:label="Inbox Activity" android:label="Inbox Activity"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
<activity <activity
android:name=".profile.activity.NotificationActivity" android:name=".profile.notification.NotificationActivity"
android:label="Inbox Activity" android:label="Inbox Activity"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
<activity <activity
android:name=".others.imagesearch.ImageSearchActivity" android:name=".others.imagesearch.ImageSearchActivity"
android:parentActivityName=".MainActivity" /> android:parentActivityName=".MainActivity" />
<activity <activity
android:name=".util.MarkdownCreatorActivity"/> android:name=".util.ActivityMarkdownCreator"
android:windowSoftInputMode="adjustResize|stateVisible" />
<activity android:name=".parsers.ParserTestActivity" /> <activity android:name=".parsers.ParserTestActivity" />
<activity <activity
android:name=".media.ReviewActivity" android:name=".media.ReviewActivity"
@@ -370,24 +373,29 @@
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.Main" /> <action android:name="android.intent.action.Main" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" /> <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:scheme="content" /> <data android:scheme="content" />
<data android:scheme="file" />
<data android:mimeType="*/*" /> <data android:mimeType="*/*" />
<data android:pathPattern=".*\\.ani" /> <data android:pathPattern=".*\\.ani" />
<data android:pathPattern=".*\\.sani" /> <data android:pathPattern=".*\\.sani" />
<data android:host="*" /> </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> </intent-filter>
</activity> </activity>
<activity <activity

View File

@@ -105,21 +105,36 @@ class App : MultiDexApplication() {
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE)) LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
} }
CoroutineScope(Dispatchers.IO).launch { if (PrefManager.getVal<Int>(PrefName.CommentsEnabled) == 0) {
if (BuildConfig.FLAVOR.contains("fdroid")) {
PrefManager.setVal(PrefName.CommentsEnabled, 2)
} else {
PrefManager.setVal(PrefName.CommentsEnabled, 1)
}
}
val scope = CoroutineScope(Dispatchers.IO)
scope.launch {
animeExtensionManager = Injekt.get() animeExtensionManager = Injekt.get()
animeExtensionManager.findAvailableExtensions() launch {
animeExtensionManager.findAvailableExtensions()
}
Logger.log("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}") Logger.log("Anime Extensions: ${animeExtensionManager.installedExtensionsFlow.first()}")
AnimeSources.init(animeExtensionManager.installedExtensionsFlow) AnimeSources.init(animeExtensionManager.installedExtensionsFlow)
} }
CoroutineScope(Dispatchers.IO).launch { scope.launch {
mangaExtensionManager = Injekt.get() mangaExtensionManager = Injekt.get()
mangaExtensionManager.findAvailableExtensions() launch {
mangaExtensionManager.findAvailableExtensions()
}
Logger.log("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}") Logger.log("Manga Extensions: ${mangaExtensionManager.installedExtensionsFlow.first()}")
MangaSources.init(mangaExtensionManager.installedExtensionsFlow) MangaSources.init(mangaExtensionManager.installedExtensionsFlow)
} }
CoroutineScope(Dispatchers.IO).launch { scope.launch {
novelExtensionManager = Injekt.get() novelExtensionManager = Injekt.get()
novelExtensionManager.findAvailableExtensions() launch {
novelExtensionManager.findAvailableExtensions()
}
Logger.log("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}") Logger.log("Novel Extensions: ${novelExtensionManager.installedExtensionsFlow.first()}")
NovelSources.init(novelExtensionManager.installedExtensionsFlow) NovelSources.init(novelExtensionManager.installedExtensionsFlow)
} }
@@ -128,7 +143,9 @@ class App : MultiDexApplication() {
downloadAddonManager = Injekt.get() downloadAddonManager = Injekt.get()
torrentAddonManager.init() torrentAddonManager.init()
downloadAddonManager.init() downloadAddonManager.init()
CommentsAPI.fetchAuthToken(this@App) if (PrefManager.getVal<Int>(PrefName.CommentsEnabled) == 1) {
CommentsAPI.fetchAuthToken(this@App)
}
val useAlarmManager = PrefManager.getVal<Boolean>(PrefName.UseAlarmManager) val useAlarmManager = PrefManager.getVal<Boolean>(PrefName.UseAlarmManager)
val scheduler = TaskScheduler.create(this@App, useAlarmManager) val scheduler = TaskScheduler.create(this@App, useAlarmManager)

View File

@@ -68,7 +68,6 @@ import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
@@ -92,12 +91,12 @@ import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.BuildConfig.APPLICATION_ID import ani.dantotsu.BuildConfig.APPLICATION_ID
import ani.dantotsu.connections.anilist.Genre import ani.dantotsu.connections.anilist.Genre
import ani.dantotsu.connections.anilist.api.FuzzyDate import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.bakaupdates.MangaUpdates
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.databinding.ItemCountDownBinding import ani.dantotsu.databinding.ItemCountDownBinding
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.notifications.IncognitoNotificationClickReceiver import ani.dantotsu.notifications.IncognitoNotificationClickReceiver
import ani.dantotsu.others.AlignTagHandler
import ani.dantotsu.others.ImageViewDialog import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.others.SpoilerPlugin import ani.dantotsu.others.SpoilerPlugin
import ani.dantotsu.parsers.ShowResponse import ani.dantotsu.parsers.ShowResponse
@@ -106,7 +105,6 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.settings.saving.internal.PreferenceKeystore import ani.dantotsu.settings.saving.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt
import ani.dantotsu.util.CountUpTimer
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.RequestBuilder import com.bumptech.glide.RequestBuilder
@@ -120,7 +118,6 @@ import com.bumptech.glide.load.resource.gif.GifDrawable
import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.Target import com.bumptech.glide.request.target.Target
import com.bumptech.glide.request.target.ViewTarget
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
@@ -140,12 +137,9 @@ import io.noties.markwon.html.TagHandlerNoOp
import io.noties.markwon.image.AsyncDrawable import io.noties.markwon.image.AsyncDrawable
import io.noties.markwon.image.glide.GlideImagesPlugin import io.noties.markwon.image.glide.GlideImagesPlugin
import jp.wasabeef.glide.transformations.BlurTransformation import jp.wasabeef.glide.transformations.BlurTransformation
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import nl.joery.animatedbottombar.AnimatedBottomBar import nl.joery.animatedbottombar.AnimatedBottomBar
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@@ -154,10 +148,13 @@ import java.io.FileOutputStream
import java.io.OutputStream import java.io.OutputStream
import java.lang.reflect.Field import java.lang.reflect.Field
import java.util.Calendar import java.util.Calendar
import java.util.Locale
import java.util.TimeZone import java.util.TimeZone
import java.util.Timer import java.util.Timer
import java.util.TimerTask import java.util.TimerTask
import kotlin.collections.set import kotlin.collections.set
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.math.log2 import kotlin.math.log2
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@@ -314,6 +311,7 @@ fun Activity.reloadActivity() {
Refresh.all() Refresh.all()
finish() finish()
startActivity(Intent(this, this::class.java)) startActivity(Intent(this, this::class.java))
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
initActivity(this) initActivity(this)
} }
@@ -855,6 +853,7 @@ fun savePrefsToDownloads(
) )
} }
@SuppressLint("StringFormatMatches")
fun savePrefs(serialized: String, path: String, title: String, context: Context): File? { fun savePrefs(serialized: String, path: String, title: String, context: Context): File? {
var file = File(path, "$title.ani") var file = File(path, "$title.ani")
var counter = 1 var counter = 1
@@ -874,6 +873,7 @@ fun savePrefs(serialized: String, path: String, title: String, context: Context)
} }
} }
@SuppressLint("StringFormatMatches")
fun savePrefs( fun savePrefs(
serialized: String, serialized: String,
path: String, path: String,
@@ -921,6 +921,7 @@ fun shareImage(title: String, bitmap: Bitmap, context: Context) {
context.startActivity(Intent.createChooser(intent, "Share $title")) context.startActivity(Intent.createChooser(intent, "Share $title"))
} }
@SuppressLint("StringFormatMatches")
fun saveImage(image: Bitmap, path: String, imageFileName: String): File? { fun saveImage(image: Bitmap, path: String, imageFileName: String): File? {
val imageFile = File(path, "$imageFileName.png") val imageFile = File(path, "$imageFileName.png")
return try { return try {
@@ -1010,47 +1011,10 @@ fun countDown(media: Media, view: ViewGroup) {
} }
} }
fun sinceWhen(media: Media, view: ViewGroup) {
if (media.status != "RELEASING" && media.status != "HIATUS") return
CoroutineScope(Dispatchers.IO).launch {
MangaUpdates().search(media.mangaName(), media.startDate)?.let {
val latestChapter = MangaUpdates.getLatestChapter(view.context, it)
val timeSince = (System.currentTimeMillis() -
(it.metadata.series.lastUpdated!!.timestamp * 1000)) / 1000
withContext(Dispatchers.Main) {
val v =
ItemCountDownBinding.inflate(LayoutInflater.from(view.context), view, false)
view.addView(v.root, 0)
v.mediaCountdownText.text =
currActivity()?.getString(R.string.chapter_release_timeout, latestChapter)
object : CountUpTimer(86400000) {
override fun onTick(second: Int) {
val a = second + timeSince
v.mediaCountdown.text = currActivity()?.getString(
R.string.time_format,
a / 86400,
a % 86400 / 3600,
a % 86400 % 3600 / 60,
a % 86400 % 3600 % 60
)
}
override fun onFinish() {
// The legend will never die.
}
}.start()
}
}
}
}
fun displayTimer(media: Media, view: ViewGroup) { fun displayTimer(media: Media, view: ViewGroup) {
when { when {
media.anime != null -> countDown(media, view) media.anime != null -> countDown(media, view)
media.format == "MANGA" || media.format == "ONE_SHOT" -> sinceWhen(media, view) else -> {}
else -> {} // No timer yet
} }
} }
@@ -1447,6 +1411,8 @@ fun openOrCopyAnilistLink(link: String) {
} else { } else {
copyToClipboard(link, true) copyToClipboard(link, true)
} }
} else if (getYoutubeId(link).isNotEmpty()) {
openLinkInYouTube(link)
} else { } else {
copyToClipboard(link, true) copyToClipboard(link, true)
} }
@@ -1483,6 +1449,7 @@ fun buildMarkwon(
TagHandlerNoOp.create("h1", "h2", "h3", "h4", "h5", "h6", "hr", "pre", "a") TagHandlerNoOp.create("h1", "h2", "h3", "h4", "h5", "h6", "hr", "pre", "a")
) )
} }
plugin.addHandler(AlignTagHandler())
}) })
.usePlugin(GlideImagesPlugin.create(object : GlideImagesPlugin.GlideStore { .usePlugin(GlideImagesPlugin.create(object : GlideImagesPlugin.GlideStore {
@@ -1527,3 +1494,44 @@ fun buildMarkwon(
.build() .build()
return markwon return markwon
} }
fun getYoutubeId(url: String): String {
val regex =
"""(?:youtube\.com/(?:[^/]+/.+/|(?:v|e(?:mbed)?)/|.*[?&]v=)|(?:youtu\.be|youtube\.com)/)([^"&?/\s]{11})|youtube\.com/""".toRegex()
val matchResult = regex.find(url)
return matchResult?.groupValues?.getOrNull(1) ?: ""
}
fun getLanguageCode(language: String): CharSequence {
val locales = Locale.getAvailableLocales()
for (locale in locales) {
if (locale.displayLanguage.equals(language, ignoreCase = true)) {
val lang: CharSequence = locale.language
return lang
}
}
val out: CharSequence = "null"
return out
}
fun getLanguageName(language: String): String? {
val locales = Locale.getAvailableLocales()
for (locale in locales) {
if (locale.language.equals(language, ignoreCase = true)) {
return locale.displayLanguage
}
}
return null
}
@OptIn(ExperimentalEncodingApi::class)
fun String.decodeBase64ToString(): String {
return try {
String(Base64.decode(this), Charsets.UTF_8)
} catch (e: Exception) {
Logger.log(e)
""
}
}

View File

@@ -2,7 +2,6 @@ package ani.dantotsu
import android.animation.ObjectAnimator import android.animation.ObjectAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Intent import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.drawable.Animatable import android.graphics.drawable.Animatable
@@ -51,7 +50,8 @@ import ani.dantotsu.others.CustomBottomDialog
import ani.dantotsu.others.calc.CalcActivity import ani.dantotsu.others.calc.CalcActivity
import ani.dantotsu.profile.ProfileActivity import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.activity.FeedActivity import ani.dantotsu.profile.activity.FeedActivity
import ani.dantotsu.profile.activity.NotificationActivity import ani.dantotsu.profile.notification.NotificationActivity
import ani.dantotsu.settings.AddRepositoryBottomSheet
import ani.dantotsu.settings.ExtensionsActivity import ani.dantotsu.settings.ExtensionsActivity
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefManager.asLiveBool import ani.dantotsu.settings.saving.PrefManager.asLiveBool
@@ -60,10 +60,11 @@ import ani.dantotsu.settings.saving.SharedPreferenceBooleanLiveData
import ani.dantotsu.settings.saving.internal.PreferenceKeystore import ani.dantotsu.settings.saving.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferencePackager import ani.dantotsu.settings.saving.internal.PreferencePackager
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.AudioHelper
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import ani.dantotsu.util.customAlertDialog
import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputEditText
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import io.noties.markwon.Markwon import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin import io.noties.markwon.SoftBreakAddsNewLinePlugin
@@ -116,58 +117,8 @@ class MainActivity : AppCompatActivity() {
} }
} }
val action = intent.action if (Intent.ACTION_VIEW == intent.action) {
val type = intent.type handleViewIntent(intent)
if (Intent.ACTION_VIEW == action && type != null) {
val uri: Uri? = intent.data
try {
if (uri == null) {
throw Exception("Uri is null")
}
val jsonString =
contentResolver.openInputStream(uri)?.readBytes()
?: throw Exception("Error reading file")
val name =
DocumentFile.fromSingleUri(this, uri)?.name ?: "settings"
//.sani is encrypted, .ani is not
if (name.endsWith(".sani")) {
passwordAlertDialog { password ->
if (password != null) {
val salt = jsonString.copyOfRange(0, 16)
val encrypted = jsonString.copyOfRange(16, jsonString.size)
val decryptedJson = try {
PreferenceKeystore.decryptWithPassword(
password,
encrypted,
salt
)
} catch (e: Exception) {
toast("Incorrect password")
return@passwordAlertDialog
}
if (PreferencePackager.unpack(decryptedJson)) {
val intent = Intent(this, this.javaClass)
this.finish()
startActivity(intent)
}
} else {
toast("Password cannot be empty")
}
}
} else if (name.endsWith(".ani")) {
val decryptedJson = jsonString.toString(Charsets.UTF_8)
if (PreferencePackager.unpack(decryptedJson)) {
val intent = Intent(this, this.javaClass)
this.finish()
startActivity(intent)
}
} else {
toast("Invalid file type")
}
} catch (e: Exception) {
e.printStackTrace()
toast("Error importing settings")
}
} }
val bottomNavBar = findViewById<AnimatedBottomBar>(R.id.navbar) val bottomNavBar = findViewById<AnimatedBottomBar>(R.id.navbar)
@@ -287,7 +238,7 @@ class MainActivity : AppCompatActivity() {
.get() > 0 || preferences.mangaExtensionUpdatesCount().get() > 0 .get() > 0 || preferences.mangaExtensionUpdatesCount().get() > 0
) { ) {
snackString(R.string.extension_updates_available) snackString(R.string.extension_updates_available)
?.setDuration(Snackbar.LENGTH_LONG) ?.setDuration(Snackbar.LENGTH_SHORT)
?.setAction(R.string.review) { ?.setAction(R.string.review) {
startActivity(Intent(this, ExtensionsActivity::class.java)) startActivity(Intent(this, ExtensionsActivity::class.java))
} }
@@ -365,7 +316,6 @@ class MainActivity : AppCompatActivity() {
} else if (fragmentToLoad == "NOTIFICATIONS" && activityId != -1) { } else if (fragmentToLoad == "NOTIFICATIONS" && activityId != -1) {
Logger.log("MainActivity, onCreate: $activityId") Logger.log("MainActivity, onCreate: $activityId")
val notificationIntent = Intent(this, NotificationActivity::class.java).apply { val notificationIntent = Intent(this, NotificationActivity::class.java).apply {
putExtra("FRAGMENT_TO_LOAD", "NOTIFICATIONS")
putExtra("activityId", activityId) putExtra("activityId", activityId)
} }
launched = true launched = true
@@ -455,7 +405,10 @@ class MainActivity : AppCompatActivity() {
} }
} }
} }
if (PrefManager.getVal(PrefName.OC)) {
AudioHelper.run(this, R.raw.audio)
PrefManager.setVal(PrefName.OC, false)
}
val torrentManager = Injekt.get<TorrentAddonManager>() val torrentManager = Injekt.get<TorrentAddonManager>()
fun startTorrent() { fun startTorrent() {
if (torrentManager.isAvailable() && PrefManager.getVal(PrefName.TorrentEnabled)) { if (torrentManager.isAvailable() && PrefManager.getVal(PrefName.TorrentEnabled)) {
@@ -490,39 +443,102 @@ class MainActivity : AppCompatActivity() {
params.updateMargins(bottom = margin.toPx) 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) { private fun passwordAlertDialog(callback: (CharArray?) -> Unit) {
val password = CharArray(16).apply { fill('0') } val password = CharArray(16).apply { fill('0') }
// Inflate the dialog layout // Inflate the dialog layout
val dialogView = DialogUserAgentBinding.inflate(layoutInflater) val dialogView = DialogUserAgentBinding.inflate(layoutInflater).apply {
dialogView.userAgentTextBox.hint = "Password" userAgentTextBox.hint = "Password"
dialogView.subtitle.visibility = View.VISIBLE subtitle.visibility = View.VISIBLE
dialogView.subtitle.text = getString(R.string.enter_password_to_decrypt_file) subtitle.text = getString(R.string.enter_password_to_decrypt_file)
}
val dialog = AlertDialog.Builder(this, R.style.MyPopup) customAlertDialog().apply {
.setTitle("Enter Password") setTitle("Enter Password")
.setView(dialogView.root) setCustomView(dialogView.root)
.setPositiveButton("OK", null) setPosButton(R.string.yes) {
.setNegativeButton("Cancel") { dialog, _ -> 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') password.fill('0')
dialog.dismiss()
callback(null) callback(null)
} }
.create() show()
dialog.window?.setDimAmount(0.8f)
dialog.show()
// Override the positive button here
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val editText = dialog.findViewById<TextInputEditText>(R.id.userAgentTextBox)
if (editText?.text?.isNotBlank() == true) {
editText.text?.toString()?.trim()?.toCharArray(password)
dialog.dismiss()
callback(password)
} else {
toast("Password cannot be empty")
}
} }
} }

View File

@@ -26,9 +26,17 @@ interface DownloadAddonApiV2 {
statCallback: (Double) -> Unit statCallback: (Double) -> Unit
): Long ): Long
suspend fun customFFMpeg(command: String, videoUrls: List<String>, logCallback: (String) -> 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) suspend fun customFFProbe(
command: String,
videoUrls: List<String>,
logCallback: (String) -> Unit
)
fun getState(sessionId: Long): String fun getState(sessionId: Long): String

View File

@@ -6,11 +6,11 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.addons.AddonDownloader.Companion.hasUpdate import ani.dantotsu.addons.AddonDownloader.Companion.hasUpdate
import ani.dantotsu.addons.AddonInstallReceiver
import ani.dantotsu.addons.AddonListener import ani.dantotsu.addons.AddonListener
import ani.dantotsu.addons.AddonLoader import ani.dantotsu.addons.AddonLoader
import ani.dantotsu.addons.AddonManager import ani.dantotsu.addons.AddonManager
import ani.dantotsu.addons.LoadResult import ani.dantotsu.addons.LoadResult
import ani.dantotsu.addons.AddonInstallReceiver
import ani.dantotsu.media.AddonType import ani.dantotsu.media.AddonType
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName

View File

@@ -41,7 +41,8 @@ class TorrentServerService : Service() {
flags: Int, flags: Int,
startId: Int, startId: Int,
): Int { ): Int {
extension = Injekt.get<TorrentAddonManager>().extension?.extension ?: return START_NOT_STICKY extension =
Injekt.get<TorrentAddonManager>().extension?.extension ?: return START_NOT_STICKY
intent?.let { intent?.let {
if (it.action != null) { if (it.action != null) {
when (it.action) { when (it.action) {

View File

@@ -8,7 +8,6 @@ import androidx.media3.common.util.UnstableApi
import androidx.media3.database.StandaloneDatabaseProvider import androidx.media3.database.StandaloneDatabaseProvider
import ani.dantotsu.addons.download.DownloadAddonManager import ani.dantotsu.addons.download.DownloadAddonManager
import ani.dantotsu.addons.torrent.TorrentAddonManager import ani.dantotsu.addons.torrent.TorrentAddonManager
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.media.manga.MangaCache import ani.dantotsu.media.manga.MangaCache
import ani.dantotsu.parsers.novel.NovelExtensionManager import ani.dantotsu.parsers.novel.NovelExtensionManager

View File

@@ -2,15 +2,25 @@ package ani.dantotsu.connections.anilist
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.currContext import ani.dantotsu.currContext
import ani.dantotsu.media.Author
import ani.dantotsu.media.Character
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.Studio
import ani.dantotsu.profile.User
import java.io.Serializable 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, val type: String,
var isAdult: Boolean, var isAdult: Boolean,
var onList: Boolean? = null, var onList: Boolean? = null,
var perPage: Int? = null, var perPage: Int? = null,
var search: String? = null,
var countryOfOrigin: String? = null, var countryOfOrigin: String? = null,
var sort: String? = null, var sort: String? = null,
var genres: MutableList<String>? = null, var genres: MutableList<String>? = null,
@@ -23,10 +33,11 @@ data class SearchResults(
var seasonYear: Int? = null, var seasonYear: Int? = null,
var startYear: Int? = null, var startYear: Int? = null,
var season: String? = null, var season: String? = null,
var page: Int = 1, override var search: String? = null,
var results: MutableList<Media>, override var page: Int = 1,
var hasNextPage: Boolean, override var results: MutableList<Media>,
) : Serializable { override var hasNextPage: Boolean,
) : SearchResults<Media>, Serializable {
fun toChipList(): List<SearchChip> { fun toChipList(): List<SearchChip> {
val list = mutableListOf<SearchChip>() val list = mutableListOf<SearchChip>()
sort?.let { sort?.let {
@@ -109,3 +120,32 @@ data class SearchResults(
val text: 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

@@ -15,6 +15,8 @@ import ani.dantotsu.snackString
import ani.dantotsu.toast import ani.dantotsu.toast
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import java.util.Calendar import java.util.Calendar
import java.util.Locale
import kotlin.math.abs
object Anilist { object Anilist {
val query: AnilistQueries = AnilistQueries() val query: AnilistQueries = AnilistQueries()
@@ -22,7 +24,7 @@ object Anilist {
var token: String? = null var token: String? = null
var username: String? = null var username: String? = null
var adult: Boolean = false
var userid: Int? = null var userid: Int? = null
var avatar: String? = null var avatar: String? = null
var bg: String? = null var bg: String? = null
@@ -36,6 +38,17 @@ object Anilist {
var rateLimitReset: Long = 0 var rateLimitReset: Long = 0
var initialized = false 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( val sortBy = listOf(
"SCORE_DESC", "SCORE_DESC",
@@ -96,6 +109,86 @@ object Anilist {
"Original Creator", "Story & Art", "Story" "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 cal: Calendar = Calendar.getInstance()
private val currentYear = cal.get(Calendar.YEAR) private val currentYear = cal.get(Calendar.YEAR)
private val currentSeason: Int = when (cal.get(Calendar.MONTH)) { private val currentSeason: Int = when (cal.get(Calendar.MONTH)) {
@@ -106,6 +199,33 @@ object Anilist {
else -> 0 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> { private fun getSeason(next: Boolean): Pair<String, Int> {
var newSeason = if (next) currentSeason + 1 else currentSeason - 1 var newSeason = if (next) currentSeason + 1 else currentSeason - 1
var newYear = currentYear var newYear = currentYear

View File

@@ -3,16 +3,99 @@ package ani.dantotsu.connections.anilist
import ani.dantotsu.connections.anilist.Anilist.executeQuery import ani.dantotsu.connections.anilist.Anilist.executeQuery
import ani.dantotsu.connections.anilist.api.FuzzyDate import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.anilist.api.Query import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.connections.anilist.api.ToggleLike
import ani.dantotsu.currContext import ani.dantotsu.currContext
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
class AnilistMutations { 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) { suspend fun toggleFav(anime: Boolean = true, id: Int) {
val query = val query = """
"""mutation (${"$"}animeId: Int,${"$"}mangaId:Int) { ToggleFavourite(animeId:${"$"}animeId,mangaId:${"$"}mangaId){ anime { edges { id } } manga { edges { id } } } }""" 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"}""" val variables = if (anime) """{"animeId":"$id"}""" else """{"mangaId":"$id"}"""
executeQuery<JsonObject>(query, variables) executeQuery<JsonObject>(query, variables)
} }
@@ -25,7 +108,17 @@ class AnilistMutations {
FavType.STAFF -> "staffId" FavType.STAFF -> "staffId"
FavType.STUDIO -> "studioId" FavType.STUDIO -> "studioId"
} }
val query = """mutation{ToggleFavourite($filter:$id){anime{pageInfo{total}}}}""" val query = """
mutation {
ToggleFavourite($filter: $id) {
anime {
pageInfo {
total
}
}
}
}
""".trimIndent()
val result = executeQuery<JsonObject>(query) val result = executeQuery<JsonObject>(query)
return result?.get("errors") == null && result != null return result?.get("errors") == null && result != null
} }
@@ -34,6 +127,54 @@ class AnilistMutations {
ANIME, MANGA, CHARACTER, STAFF, STUDIO 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( suspend fun editList(
mediaID: Int, mediaID: Int,
progress: Int? = null, progress: Int? = null,
@@ -46,14 +187,45 @@ class AnilistMutations {
completedAt: FuzzyDate? = null, completedAt: FuzzyDate? = null,
customList: List<String>? = null customList: List<String>? = null
) { ) {
val query = """ 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 ""} ) { mutation (
SaveMediaListEntry( mediaId: ${"$"}mediaID, progress: ${"$"}progress, repeat: ${"$"}repeat, notes: ${"$"}notes, private: ${"$"}private, scoreRaw: ${"$"}scoreRaw, status:${"$"}status, startedAt: ${"$"}start, completedAt: ${"$"}completed , customLists: ${"$"}customLists ) { ${"$"}mediaID: Int,
score(format:POINT_10_DECIMAL) startedAt{year month day} completedAt{year month day} ${"$"}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 val variables = """{"mediaID":$mediaID
${if (private != null) ""","private":$private""" else ""} ${if (private != null) ""","private":$private""" else ""}
@@ -69,43 +241,179 @@ class AnilistMutations {
} }
suspend fun deleteList(listId: Int) { 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"}""" val variables = """{"id":"$listId"}"""
executeQuery<JsonObject>(query, variables) executeQuery<JsonObject>(query, variables)
} }
suspend fun rateReview(reviewId: Int, rating: String): Query.RateReviewResponse? { 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}}}}" 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) return executeQuery<Query.RateReviewResponse>(query)
} }
suspend fun postActivity(text:String): String { 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 encodedText = text.stringSanitizer()
val query = "mutation{SaveTextActivity(text:$encodedText){siteUrl}}" val query = """
mutation {
SaveTextActivity(${if (edit != null) "id: $edit," else ""} text: $encodedText) {
siteUrl
}
}
""".trimIndent()
val result = executeQuery<JsonObject>(query) val result = executeQuery<JsonObject>(query)
val errors = result?.get("errors") val errors = result?.get("errors")
return errors?.toString() return errors?.toString() ?: (currContext()?.getString(ani.dantotsu.R.string.success)
?: (currContext()?.getString(ani.dantotsu.R.string.success) ?: "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 { suspend fun postReview(summary: String, body: String, mediaId: Int, score: Int): String {
val encodedSummary = summary.stringSanitizer() val encodedSummary = summary.stringSanitizer()
val encodedBody = body.stringSanitizer() val encodedBody = body.stringSanitizer()
val query = "mutation{SaveReview(mediaId:$mediaId,summary:$encodedSummary,body:$encodedBody,score:$score){siteUrl}}" val query = """
mutation {
SaveReview(
mediaId: $mediaId,
summary: $encodedSummary,
body: $encodedBody,
score: $score
) {
siteUrl
}
}
""".trimIndent()
val result = executeQuery<JsonObject>(query) val result = executeQuery<JsonObject>(query)
val errors = result?.get("errors") val errors = result?.get("errors")
return errors?.toString() return errors?.toString() ?: (currContext()?.getString(ani.dantotsu.R.string.success)
?: (currContext()?.getString(ani.dantotsu.R.string.success) ?: "Success") ?: "Success")
} }
suspend fun postReply(activityId: Int, text: String): String { suspend fun deleteActivityReply(activityId: Int): Boolean {
val encodedText = text.stringSanitizer() val query = """
val query = "mutation{SaveActivityReply(activityId:$activityId,text:$encodedText){id}}" mutation {
DeleteActivityReply(id: $activityId) {
deleted
}
}
""".trimIndent()
val result = executeQuery<JsonObject>(query) val result = executeQuery<JsonObject>(query)
val errors = result?.get("errors") val errors = result?.get("errors")
return errors?.toString() return errors == null
?: (currContext()?.getString(ani.dantotsu.R.string.success) ?: "Success") }
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 { private fun String.stringSanitizer(): String {

View File

@@ -22,7 +22,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
suspend fun getUserId(context: Context, block: () -> Unit) { suspend fun getUserId(context: Context, block: () -> Unit) {
if (!Anilist.initialized) { if (!Anilist.initialized && PrefManager.getVal<String>(PrefName.AnilistToken) != "") {
if (Anilist.query.getUserData()) { if (Anilist.query.getUserData()) {
tryWithSuspend { tryWithSuspend {
if (MAL.token != null && !MAL.query.getUserData()) if (MAL.token != null && !MAL.query.getUserData())
@@ -81,24 +81,26 @@ class AnilistHomeViewModel : ViewModel() {
MutableLiveData<ArrayList<User>>(null) MutableLiveData<ArrayList<User>>(null)
fun getUserStatus(): LiveData<ArrayList<User>> = userStatus 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>> = private val hidden: MutableLiveData<ArrayList<Media>> =
MutableLiveData<ArrayList<Media>>(null) MutableLiveData<ArrayList<Media>>(null)
fun getHidden(): LiveData<ArrayList<Media>> = hidden fun getHidden(): LiveData<ArrayList<Media>> = hidden
@Suppress("UNCHECKED_CAST")
suspend fun initHomePage() { suspend fun initHomePage() {
val res = Anilist.query.initHomePage() val res = Anilist.query.initHomePage()
res["currentAnime"]?.let { animeContinue.postValue(it as ArrayList<Media>?) } res["currentAnime"]?.let { animeContinue.postValue(it) }
res["favoriteAnime"]?.let { animeFav.postValue(it as ArrayList<Media>?) } res["favoriteAnime"]?.let { animeFav.postValue(it) }
res["plannedAnime"]?.let { animePlanned.postValue(it as ArrayList<Media>?) } res["currentAnimePlanned"]?.let { animePlanned.postValue(it) }
res["currentManga"]?.let { mangaContinue.postValue(it as ArrayList<Media>?) } res["currentManga"]?.let { mangaContinue.postValue(it) }
res["favoriteManga"]?.let { mangaFav.postValue(it as ArrayList<Media>?) } res["favoriteManga"]?.let { mangaFav.postValue(it) }
res["plannedManga"]?.let { mangaPlanned.postValue(it as ArrayList<Media>?) } res["currentMangaPlanned"]?.let { mangaPlanned.postValue(it) }
res["recommendations"]?.let { recommendation.postValue(it as ArrayList<Media>?) } res["recommendations"]?.let { recommendation.postValue(it) }
res["hidden"]?.let { hidden.postValue(it as ArrayList<Media>?) } res["hidden"]?.let { hidden.postValue(it) }
res["status"]?.let { userStatus.postValue(it as ArrayList<User>?) }
} }
suspend fun loadMain(context: FragmentActivity) { suspend fun loadMain(context: FragmentActivity) {
@@ -126,7 +128,7 @@ class AnilistHomeViewModel : ViewModel() {
class AnilistAnimeViewModel : ViewModel() { class AnilistAnimeViewModel : ViewModel() {
var searched = false var searched = false
var notSet = true var notSet = true
lateinit var searchResults: SearchResults lateinit var aniMangaSearchResults: AniMangaSearchResults
private val type = "ANIME" private val type = "ANIME"
private val trending: MutableLiveData<MutableList<Media>> = private val trending: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null) MutableLiveData<MutableList<Media>>(null)
@@ -135,7 +137,7 @@ class AnilistAnimeViewModel : ViewModel() {
suspend fun loadTrending(i: Int) { suspend fun loadTrending(i: Int) {
val (season, year) = Anilist.currentSeasons[i] val (season, year) = Anilist.currentSeasons[i]
trending.postValue( trending.postValue(
Anilist.query.search( Anilist.query.searchAniManga(
type, type,
perPage = 12, perPage = 12,
sort = Anilist.sortBy[2], sort = Anilist.sortBy[2],
@@ -148,9 +150,9 @@ class AnilistAnimeViewModel : ViewModel() {
} }
private val animePopular = MutableLiveData<SearchResults?>(null) private val animePopular = MutableLiveData<AniMangaSearchResults?>(null)
fun getPopular(): LiveData<SearchResults?> = animePopular fun getPopular(): LiveData<AniMangaSearchResults?> = animePopular
suspend fun loadPopular( suspend fun loadPopular(
type: String, type: String,
searchVal: String? = null, searchVal: String? = null,
@@ -159,7 +161,7 @@ class AnilistAnimeViewModel : ViewModel() {
onList: Boolean = true, onList: Boolean = true,
) { ) {
animePopular.postValue( animePopular.postValue(
Anilist.query.search( Anilist.query.searchAniManga(
type, type,
search = searchVal, search = searchVal,
onList = if (onList) null else false, onList = if (onList) null else false,
@@ -171,8 +173,8 @@ class AnilistAnimeViewModel : ViewModel() {
} }
suspend fun loadNextPage(r: SearchResults) = animePopular.postValue( suspend fun loadNextPage(r: AniMangaSearchResults) = animePopular.postValue(
Anilist.query.search( Anilist.query.searchAniManga(
r.type, r.type,
r.page + 1, r.page + 1,
r.perPage, r.perPage,
@@ -222,7 +224,7 @@ class AnilistAnimeViewModel : ViewModel() {
class AnilistMangaViewModel : ViewModel() { class AnilistMangaViewModel : ViewModel() {
var searched = false var searched = false
var notSet = true var notSet = true
lateinit var searchResults: SearchResults lateinit var aniMangaSearchResults: AniMangaSearchResults
private val type = "MANGA" private val type = "MANGA"
private val trending: MutableLiveData<MutableList<Media>> = private val trending: MutableLiveData<MutableList<Media>> =
MutableLiveData<MutableList<Media>>(null) MutableLiveData<MutableList<Media>>(null)
@@ -230,7 +232,7 @@ class AnilistMangaViewModel : ViewModel() {
fun getTrending(): LiveData<MutableList<Media>> = trending fun getTrending(): LiveData<MutableList<Media>> = trending
suspend fun loadTrending() = suspend fun loadTrending() =
trending.postValue( trending.postValue(
Anilist.query.search( Anilist.query.searchAniManga(
type, type,
perPage = 10, perPage = 10,
sort = Anilist.sortBy[2], sort = Anilist.sortBy[2],
@@ -240,8 +242,8 @@ class AnilistMangaViewModel : ViewModel() {
) )
private val mangaPopular = MutableLiveData<SearchResults?>(null) private val mangaPopular = MutableLiveData<AniMangaSearchResults?>(null)
fun getPopular(): LiveData<SearchResults?> = mangaPopular fun getPopular(): LiveData<AniMangaSearchResults?> = mangaPopular
suspend fun loadPopular( suspend fun loadPopular(
type: String, type: String,
searchVal: String? = null, searchVal: String? = null,
@@ -250,7 +252,7 @@ class AnilistMangaViewModel : ViewModel() {
onList: Boolean = true, onList: Boolean = true,
) { ) {
mangaPopular.postValue( mangaPopular.postValue(
Anilist.query.search( Anilist.query.searchAniManga(
type, type,
search = searchVal, search = searchVal,
onList = if (onList) null else false, onList = if (onList) null else false,
@@ -262,8 +264,8 @@ class AnilistMangaViewModel : ViewModel() {
} }
suspend fun loadNextPage(r: SearchResults) = mangaPopular.postValue( suspend fun loadNextPage(r: AniMangaSearchResults) = mangaPopular.postValue(
Anilist.query.search( Anilist.query.searchAniManga(
r.type, r.type,
r.page + 1, r.page + 1,
r.perPage, r.perPage,
@@ -323,14 +325,131 @@ class AnilistMangaViewModel : ViewModel() {
} }
class AnilistSearch : ViewModel() { 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 searched = false
var notSet = true var notSet = true
lateinit var searchResults: SearchResults lateinit var aniMangaSearchResults: AniMangaSearchResults
private val result: MutableLiveData<SearchResults?> = MutableLiveData<SearchResults?>(null) private val aniMangaResult: MutableLiveData<AniMangaSearchResults?> =
MutableLiveData<AniMangaSearchResults?>(null)
fun getSearch(): LiveData<SearchResults?> = result lateinit var characterSearchResults: CharacterSearchResults
suspend fun loadSearch(r: SearchResults) = result.postValue( private val characterResult: MutableLiveData<CharacterSearchResults?> =
Anilist.query.search( 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.type,
r.page, r.page,
r.perPage, r.perPage,
@@ -352,8 +471,36 @@ class AnilistSearch : ViewModel() {
) )
) )
suspend fun loadNextPage(r: SearchResults) = result.postValue( private suspend fun loadCharacterSearch(r: CharacterSearchResults) = characterResult.postValue(
Anilist.query.search( 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.type,
r.page + 1, r.page + 1,
r.perPage, r.perPage,
@@ -374,6 +521,35 @@ class AnilistSearch : ViewModel() {
r.season 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() { class GenresViewModel : ViewModel() {

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

@@ -163,13 +163,9 @@ class Query {
@Serializable @Serializable
data class Data( data class Data(
@SerialName("recentUpdates") val recentUpdates: ani.dantotsu.connections.anilist.api.Page?, @SerialName("recentUpdates") val recentUpdates: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("recentUpdates2") val recentUpdates2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingMovies") val trendingMovies: ani.dantotsu.connections.anilist.api.Page?, @SerialName("trendingMovies") val trendingMovies: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingMovies2") val trendingMovies2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("topRated") val topRated: ani.dantotsu.connections.anilist.api.Page?, @SerialName("topRated") val topRated: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("topRated2") val topRated2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("mostFav") val mostFav: ani.dantotsu.connections.anilist.api.Page?, @SerialName("mostFav") val mostFav: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("mostFav2") val mostFav2: ani.dantotsu.connections.anilist.api.Page?,
) )
} }
@@ -181,15 +177,10 @@ class Query {
@Serializable @Serializable
data class Data( data class Data(
@SerialName("trendingManga") val trendingManga: ani.dantotsu.connections.anilist.api.Page?, @SerialName("trendingManga") val trendingManga: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingManga2") val trendingManga2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingManhwa") val trendingManhwa: ani.dantotsu.connections.anilist.api.Page?, @SerialName("trendingManhwa") val trendingManhwa: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingManhwa2") val trendingManhwa2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingNovel") val trendingNovel: ani.dantotsu.connections.anilist.api.Page?, @SerialName("trendingNovel") val trendingNovel: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("trendingNovel2") val trendingNovel2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("topRated") val topRated: ani.dantotsu.connections.anilist.api.Page?, @SerialName("topRated") val topRated: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("topRated2") val topRated2: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("mostFav") val mostFav: ani.dantotsu.connections.anilist.api.Page?, @SerialName("mostFav") val mostFav: ani.dantotsu.connections.anilist.api.Page?,
@SerialName("mostFav2") val mostFav2: ani.dantotsu.connections.anilist.api.Page?,
) )
} }

View File

@@ -14,6 +14,7 @@ data class FeedResponse(
val page: ActivityPage val page: ActivityPage
) : java.io.Serializable ) : java.io.Serializable
} }
@Serializable @Serializable
data class ActivityPage( data class ActivityPage(
@SerialName("activities") @SerialName("activities")

View File

@@ -143,7 +143,7 @@ data class Media(
@SerialName("externalLinks") var externalLinks: List<MediaExternalLink>?, @SerialName("externalLinks") var externalLinks: List<MediaExternalLink>?,
// Data and links to legal streaming episodes on external sites // 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 // The ranking of the media in a particular time span and format compared to other media
// @SerialName("rankings") var rankings: List<MediaRank>?, // @SerialName("rankings") var rankings: List<MediaRank>?,
@@ -189,7 +189,7 @@ data class MediaTitle(
// The currently authenticated users preferred title language. Default romaji for non-authenticated // The currently authenticated users preferred title language. Default romaji for non-authenticated
@SerialName("userPreferred") var userPreferred: String, @SerialName("userPreferred") var userPreferred: String,
): java.io.Serializable ) : java.io.Serializable
@Serializable @Serializable
enum class MediaType { enum class MediaType {
@@ -240,6 +240,21 @@ data class AiringSchedule(
@SerialName("media") var media: Media?, @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 @Serializable
data class MediaCoverImage( 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. // The cover image url of the media at its largest size. If this size isn't available, large will be provided instead.
@@ -433,7 +448,7 @@ data class MediaEdge(
@SerialName("staffRole") var staffRole: String?, @SerialName("staffRole") var staffRole: String?,
// The voice actors of the character // The voice actors of the character
// @SerialName("voiceActors") var voiceActors: List<Staff>?, @SerialName("voiceActors") var voiceActors: List<Staff>?,
// The voice actors of the character with role date // The voice actors of the character with role date
// @SerialName("voiceActorRoles") var voiceActorRoles: List<StaffRoleType>?, // @SerialName("voiceActorRoles") var voiceActorRoles: List<StaffRoleType>?,

View File

@@ -69,12 +69,12 @@ data class User(
// The user's previously used names. // The user's previously used names.
// @SerialName("previousNames") var previousNames: List<UserPreviousName>?, // @SerialName("previousNames") var previousNames: List<UserPreviousName>?,
): java.io.Serializable ) : java.io.Serializable
@Serializable @Serializable
data class UserOptions( data class UserOptions(
// The language the user wants to see media titles in // 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 // Whether the user has enabled viewing of 18+ content
@SerialName("displayAdultContent") var displayAdultContent: Boolean?, @SerialName("displayAdultContent") var displayAdultContent: Boolean?,
@@ -88,17 +88,17 @@ data class UserOptions(
// // Notification options // // Notification options
// // @SerialName("notificationOptions") var notificationOptions: List<NotificationOption>?, // // @SerialName("notificationOptions") var notificationOptions: List<NotificationOption>?,
// //
// // The user's timezone offset (Auth user only) // The user's timezone offset (Auth user only)
// @SerialName("timezone") var timezone: String?, @SerialName("timezone") var timezone: String?,
// //
// // Minutes between activity for them to be merged together. 0 is Never, Above 2 weeks (20160 mins) is Always. // Minutes between activity for them to be merged together. 0 is Never, Above 2 weeks (20160 mins) is Always.
// @SerialName("activityMergeTime") var activityMergeTime: Int?, @SerialName("activityMergeTime") var activityMergeTime: Int?,
// //
// // The language the user wants to see staff and character names in // The language the user wants to see staff and character names in
// // @SerialName("staffNameLanguage") var staffNameLanguage: UserStaffNameLanguage?, @SerialName("staffNameLanguage") var staffNameLanguage: UserStaffNameLanguage?,
// //
// // Whether the user only allow messages from users they follow // Whether the user only allow messages from users they follow
// @SerialName("restrictMessagesToFollowing") var restrictMessagesToFollowing: Boolean?, @SerialName("restrictMessagesToFollowing") var restrictMessagesToFollowing: Boolean?,
// The list activity types the user has disabled from being created from list updates // The list activity types the user has disabled from being created from list updates
// @SerialName("disabledListActivity") var disabledListActivity: List<ListActivityOption>?, // @SerialName("disabledListActivity") var disabledListActivity: List<ListActivityOption>?,
@@ -119,6 +119,48 @@ data class UserStatisticTypes(
@SerialName("manga") var manga: UserStatistics? @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 @Serializable
data class UserStatistics( data class UserStatistics(
// //
@@ -164,7 +206,7 @@ data class Favourites(
@Serializable @Serializable
data class MediaListOptions( data class MediaListOptions(
// The score format the user is using for media lists // The score format the user is using for media lists
@SerialName("scoreFormat") var scoreFormat: String?, @SerialName("scoreFormat") var scoreFormat: ScoreFormat?,
// The default order list rows should be displayed in // The default order list rows should be displayed in
@SerialName("rowOrder") var rowOrder: String?, @SerialName("rowOrder") var rowOrder: String?,
@@ -181,8 +223,8 @@ data class MediaListTypeOptions(
// The order each list should be displayed in // The order each list should be displayed in
@SerialName("sectionOrder") var sectionOrder: List<String>?, @SerialName("sectionOrder") var sectionOrder: List<String>?,
// If the completed sections of the list should be separated by format // // If the completed sections of the list should be separated by format
@SerialName("splitCompletedSectionByFormat") var splitCompletedSectionByFormat: Boolean?, // @SerialName("splitCompletedSectionByFormat") var splitCompletedSectionByFormat: Boolean?,
// The names of the user's custom lists // The names of the user's custom lists
@SerialName("customLists") var customLists: List<String>?, @SerialName("customLists") var customLists: List<String>?,

View File

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

View File

@@ -27,8 +27,11 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
object CommentsAPI { object CommentsAPI {
private const val ADDRESS: String = "https://api.dantotsu.app" 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 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 authToken: String? = null
var userId: String? = null var userId: String? = null
var isBanned: Boolean = false var isBanned: Boolean = false
@@ -371,8 +374,8 @@ object CommentsAPI {
} }
private fun errorMessage(reason: String) { private fun errorMessage(reason: String) {
Logger.log(reason) if (commentsEnabled) Logger.log(reason)
if (isOnline) snackString(reason) if (isOnline && commentsEnabled) snackString(reason)
} }
fun logout() { fun logout() {
@@ -408,7 +411,7 @@ object CommentsAPI {
return map return map
} }
private fun requestBuilder(client: OkHttpClient = Injekt.get<NetworkHelper>().client): Requests { fun requestBuilder(client: OkHttpClient = Injekt.get<NetworkHelper>().client): Requests {
return Requests( return Requests(
client, client,
headerBuilder() headerBuilder()

View File

@@ -70,7 +70,7 @@ object Discord {
const val application_Id = "1163925779692912771" const val application_Id = "1163925779692912771"
const val small_Image: String = const val small_Image: String =
"mp:external/GJEe4hKzr8w56IW6ZKQz43HFVEo8pOtA_C-dJiWwxKo/https/cdn.discordapp.com/app-icons/1163925779692912771/f6b42d41dfdf0b56fcc79d4a12d2ac66.png" "mp:external/9NqpMxXs4ZNQtMG42L7hqINW92GqqDxgxS9Oh0Sp880/%3Fsize%3D48%26quality%3Dlossless%26name%3DDantotsu/https/cdn.discordapp.com/emojis/1167344924874784828.gif"
const val small_Image_AniList: String = const val small_Image_AniList: String =
"mp:external/rHOIjjChluqQtGyL_UHk6Z4oAqiVYlo_B7HSGPLSoUg/%3Fsize%3D128/https/cdn.discordapp.com/icons/210521487378087947/a_f54f910e2add364a3da3bb2f2fce0c72.webp" "https://anilist.co/img/icons/android-chrome-512x512.png"
} }

View File

@@ -47,9 +47,9 @@ class Login : AppCompatActivity() {
view.evaluateJavascript( view.evaluateJavascript(
""" """
(function() { (function() {
const wreq = (webpackChunkdiscord_app.push([[''],{},e=>{m=[];for(let c in e.c)m.push(e.c[c])}]),m).find(m=>m?.exports?.default?.getToken!==void 0).exports.default.getToken(); const m = []; webpackChunkdiscord_app.push([[""], {}, e => {for (let c in e.c)m.push(e.c[c])}]);
return wreq; return m.find(n => n?.exports?.default?.getToken !== void 0)?.exports?.default?.getToken();
})() })()
""".trimIndent() """.trimIndent()
) { result -> ) { result ->
login(result.trim('"')) login(result.trim('"'))

View File

@@ -1,24 +1,19 @@
package ani.dantotsu.connections.discord 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.Activity
import ani.dantotsu.connections.discord.serializers.Presence import ani.dantotsu.connections.discord.serializers.Presence
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit.SECONDS
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import ani.dantotsu.client as app
@Suppress("MemberVisibilityCanBePrivate") @Suppress("MemberVisibilityCanBePrivate")
open class RPC(val token: String, val coroutineContext: CoroutineContext) { open class RPC(val token: String, val coroutineContext: CoroutineContext) {
private val json = Json {
encodeDefaults = true
allowStructuredMapKeys = true
ignoreUnknownKeys = true
}
enum class Type { enum class Type {
PLAYING, STREAMING, LISTENING, WATCHING, COMPETING PLAYING, STREAMING, LISTENING, WATCHING, COMPETING
} }
@@ -27,7 +22,7 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
companion object { companion object {
data class RPCData( data class RPCData(
val applicationId: String? = null, val applicationId: String,
val type: Type? = null, val type: Type? = null,
val activityName: String? = null, val activityName: String? = null,
val details: String? = null, val details: String? = null,
@@ -40,22 +35,21 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
val buttons: MutableList<Link> = mutableListOf() 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 { suspend fun createPresence(data: RPCData): String {
val json = Json { val json = Json {
encodeDefaults = true encodeDefaults = true
allowStructuredMapKeys = true allowStructuredMapKeys = true
ignoreUnknownKeys = true ignoreUnknownKeys = true
} }
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( return json.encodeToString(Presence.Response(
3, 3,
Presence( Presence(

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 @Serializable
data class Timestamps( data class Timestamps(
val start: Long? = null, val start: Long? = null,
@SerialName("end")
val stop: Long? = null val stop: Long? = null
) )
} }

View File

@@ -28,6 +28,7 @@ class Contributors {
"rebelonion" -> "Owner & Maintainer" "rebelonion" -> "Owner & Maintainer"
"sneazy-ibo" -> "Contributor & Comment Moderator" "sneazy-ibo" -> "Contributor & Comment Moderator"
"WaiWhat" -> "Icon Designer" "WaiWhat" -> "Icon Designer"
"itsmechinmoy" -> "Discord and Telegram Admin/Helper, Comment Moderator & Translator"
else -> "Contributor" else -> "Contributor"
} }
developers = developers.plus( developers = developers.plus(
@@ -89,9 +90,15 @@ class Contributors {
"Comment Moderator and Arabic Translator", "Comment Moderator and Arabic Translator",
"https://anilist.co/user/6049773" "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( Developer(
"hastsu", "hastsu",
"https://cdn.discordapp.com/avatars/602422545077108749/20b4a6efa4314550e4ed51cdbe4fef3d.webp?size=160", "https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6183359-9os7zUhYdF64.jpg",
"Comment Moderator and Arabic Translator", "Comment Moderator and Arabic Translator",
"https://anilist.co/user/6183359" "https://anilist.co/user/6183359"
), ),

View File

@@ -125,7 +125,7 @@ class DownloadCompat {
Logger.log(e) Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e) Injekt.get<CrashlyticsInterface>().logException(e)
return OfflineAnimeModel( return OfflineAnimeModel(
"unknown", downloadedType.titleName,
"0", "0",
"??", "??",
"??", "??",
@@ -188,7 +188,7 @@ class DownloadCompat {
Logger.log(e) Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e) Injekt.get<CrashlyticsInterface>().logException(e)
return OfflineMangaModel( return OfflineMangaModel(
"unknown", downloadedType.titleName,
"0", "0",
"??", "??",
"??", "??",
@@ -260,7 +260,7 @@ class DownloadCompat {
"$mangaLink/${it.name}", "$mangaLink/${it.name}",
it.name, it.name,
null, null,
null, "Unknown",
SChapter.create() SChapter.create()
) )
chapters.add(chapter) chapters.add(chapter)

View File

@@ -13,7 +13,6 @@ import ani.dantotsu.snackString
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import com.anggrayudi.storage.callback.FolderCallback import com.anggrayudi.storage.callback.FolderCallback
import com.anggrayudi.storage.file.deleteRecursively import com.anggrayudi.storage.file.deleteRecursively
import com.anggrayudi.storage.file.findFolder
import com.anggrayudi.storage.file.moveFolderTo import com.anggrayudi.storage.file.moveFolderTo
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
@@ -61,7 +60,7 @@ class DownloadsManager(private val context: Context) {
onFinished: () -> Unit onFinished: () -> Unit
) { ) {
removeDownloadCompat(context, downloadedType, toast) removeDownloadCompat(context, downloadedType, toast)
downloadsList.remove(downloadedType) downloadsList.removeAll { it.titleName == downloadedType.titleName && it.chapterName == downloadedType.chapterName }
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
removeDirectory(downloadedType, toast) removeDirectory(downloadedType, toast)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@@ -235,7 +234,7 @@ class DownloadsManager(private val context: Context) {
val directory = val directory =
baseDirectory?.findFolder(downloadedType.titleName) baseDirectory?.findFolder(downloadedType.titleName)
?.findFolder(downloadedType.chapterName) ?.findFolder(downloadedType.chapterName)
downloadsList.remove(downloadedType) downloadsList.removeAll { it.titleName == downloadedType.titleName && it.chapterName == downloadedType.chapterName }
// Check if the directory exists and delete it recursively // Check if the directory exists and delete it recursively
if (directory?.exists() == true) { if (directory?.exists() == true) {
val deleted = directory.deleteRecursively(context, false) val deleted = directory.deleteRecursively(context, false)
@@ -279,6 +278,7 @@ class DownloadsManager(private val context: Context) {
* @param type the type of media * @param type the type of media
* @return the base directory * @return the base directory
*/ */
@Synchronized
private fun getBaseDirectory(context: Context, type: MediaType): DocumentFile? { private fun getBaseDirectory(context: Context, type: MediaType): DocumentFile? {
val baseDirectory = Uri.parse(PrefManager.getVal<String>(PrefName.DownloadsDir)) val baseDirectory = Uri.parse(PrefManager.getVal<String>(PrefName.DownloadsDir))
if (baseDirectory == Uri.EMPTY) return null if (baseDirectory == Uri.EMPTY) return null
@@ -307,6 +307,7 @@ class DownloadsManager(private val context: Context) {
* @param chapter the chapter of the media * @param chapter the chapter of the media
* @return the subdirectory * @return the subdirectory
*/ */
@Synchronized
fun getSubDirectory( fun getSubDirectory(
context: Context, context: Context,
type: MediaType, type: MediaType,
@@ -344,23 +345,34 @@ class DownloadsManager(private val context: Context) {
} }
} }
@Synchronized
private fun getBaseDirectory(context: Context): DocumentFile? { private fun getBaseDirectory(context: Context): DocumentFile? {
val baseDirectory = Uri.parse(PrefManager.getVal<String>(PrefName.DownloadsDir)) val baseDirectory = Uri.parse(PrefManager.getVal<String>(PrefName.DownloadsDir))
if (baseDirectory == Uri.EMPTY) return null if (baseDirectory == Uri.EMPTY) return null
return DocumentFile.fromTreeUri(context, baseDirectory) val base = DocumentFile.fromTreeUri(context, baseDirectory) ?: return null
return base.findOrCreateFolder(BASE_LOCATION, false)
} }
private val lock = Any()
private fun DocumentFile.findOrCreateFolder( private fun DocumentFile.findOrCreateFolder(
name: String, overwrite: Boolean name: String, overwrite: Boolean
): DocumentFile? { ): DocumentFile? {
return if (overwrite) { val validName = name.findValidName()
findFolder(name.findValidName())?.delete() synchronized(lock) {
createDirectory(name.findValidName()) return if (overwrite) {
} else { findFolder(validName)?.delete()
findFolder(name.findValidName()) ?: createDirectory(name.findValidName()) 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 private const val RATIO_THRESHOLD = 95
fun Media.compareName(name: String): Boolean { fun Media.compareName(name: String): Boolean {
val mainName = mainName().findValidName().lowercase() val mainName = mainName().findValidName().lowercase()
@@ -379,7 +391,7 @@ class DownloadsManager(private val context: Context) {
private const val RESERVED_CHARS = "|\\?*<\":>+[]/'" private const val RESERVED_CHARS = "|\\?*<\":>+[]/'"
fun String?.findValidName(): String { fun String?.findValidName(): String {
return this?.replace("/","_")?.filterNot { RESERVED_CHARS.contains(it) } ?: "" return this?.replace("/", "_")?.filterNot { RESERVED_CHARS.contains(it) } ?: ""
} }
data class DownloadedType( data class DownloadedType(
@@ -389,10 +401,13 @@ data class DownloadedType(
@Deprecated("use pTitle instead") @Deprecated("use pTitle instead")
private val title: String? = null, private val title: String? = null,
@Deprecated("use pChapter instead") @Deprecated("use pChapter instead")
private val chapter: String? = null private val chapter: String? = null,
val scanlator: String = "Unknown"
) : Serializable { ) : Serializable {
val titleName: String val titleName: String
get() = title ?: pTitle.findValidName() get() = title ?: pTitle.findValidName()
val chapterName: String val chapterName: String
get() = chapter ?: pChapter.findValidName() get() = chapter ?: pChapter.findValidName()
val uniqueName: String
get() = "$chapterName-${scanlator}"
} }

View File

@@ -181,7 +181,6 @@ class AnimeDownloaderService : Service() {
} }
private fun updateNotification() { private fun updateNotification() {
// Update the notification to reflect the current state of the queue
val pendingDownloads = AnimeServiceDataSingleton.downloadQueue.size val pendingDownloads = AnimeServiceDataSingleton.downloadQueue.size
val text = if (pendingDownloads > 0) { val text = if (pendingDownloads > 0) {
"Pending downloads: $pendingDownloads" "Pending downloads: $pendingDownloads"
@@ -201,8 +200,8 @@ class AnimeDownloaderService : Service() {
@androidx.annotation.OptIn(UnstableApi::class) @androidx.annotation.OptIn(UnstableApi::class)
suspend fun download(task: AnimeDownloadTask) { suspend fun download(task: AnimeDownloadTask) {
try { withContext(Dispatchers.IO) {
withContext(Dispatchers.Main) { try {
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission( ContextCompat.checkSelfPermission(
this@AnimeDownloaderService, this@AnimeDownloaderService,
@@ -214,22 +213,34 @@ class AnimeDownloaderService : Service() {
builder.setContentText("Downloading ${getTaskName(task.title, task.episode)}") builder.setContentText("Downloading ${getTaskName(task.title, task.episode)}")
if (notifi) { if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build()) withContext(Dispatchers.Main) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
} }
val outputDir = getSubDirectory( val baseOutputDir = getSubDirectory(
this@AnimeDownloaderService, this@AnimeDownloaderService,
MediaType.ANIME, MediaType.ANIME,
false, false,
task.title
) ?: throw Exception("Failed to create output directory")
val outputDir = getSubDirectory(
this@AnimeDownloaderService,
MediaType.ANIME,
true,
task.title, task.title,
task.episode task.episode
) ?: throw Exception("Failed to create output directory") ) ?: throw Exception("Failed to create output directory")
val extension = ffExtension!!.getFileExtension() val extension = ffExtension!!.getFileExtension()
outputDir.findFile("${task.getTaskName().findValidName()}.${extension.first}")?.delete() outputDir.findFile("${task.getTaskName().findValidName()}.${extension.first}")
?.delete()
val outputFile = val outputFile =
outputDir.createFile(extension.second, "${task.getTaskName()}.${extension.first}") outputDir.createFile(
extension.second,
"${task.getTaskName()}.${extension.first}"
)
?: throw Exception("Failed to create output file") ?: throw Exception("Failed to create output file")
var percent = 0 var percent = 0
@@ -273,7 +284,7 @@ class AnimeDownloaderService : Service() {
currentTasks.find { it.getTaskName() == task.getTaskName() }?.sessionId = currentTasks.find { it.getTaskName() == task.getTaskName() }?.sessionId =
ffTask ffTask
saveMediaInfo(task) saveMediaInfo(task, baseOutputDir)
// periodically check if the download is complete // periodically check if the download is complete
while (ffExtension.getState(ffTask) != "COMPLETED") { while (ffExtension.getState(ffTask) != "COMPLETED") {
@@ -287,7 +298,11 @@ class AnimeDownloaderService : Service() {
) )
} Download failed" } Download failed"
) )
notificationManager.notify(NOTIFICATION_ID, builder.build()) if (notifi) {
withContext(Dispatchers.Main) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
}
toast("${getTaskName(task.title, task.episode)} Download failed") toast("${getTaskName(task.title, task.episode)} Download failed")
Logger.log("Download failed: ${ffExtension.getStackTrace(ffTask)}") Logger.log("Download failed: ${ffExtension.getStackTrace(ffTask)}")
downloadsManager.removeDownload( downloadsManager.removeDownload(
@@ -320,7 +335,9 @@ class AnimeDownloaderService : Service() {
percent.coerceAtMost(99) percent.coerceAtMost(99)
) )
if (notifi) { if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build()) withContext(Dispatchers.Main) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
} }
kotlinx.coroutines.delay(2000) kotlinx.coroutines.delay(2000)
} }
@@ -335,7 +352,11 @@ class AnimeDownloaderService : Service() {
) )
} Download failed" } Download failed"
) )
notificationManager.notify(NOTIFICATION_ID, builder.build()) if (notifi) {
withContext(Dispatchers.Main) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
}
snackString("${getTaskName(task.title, task.episode)} Download failed") snackString("${getTaskName(task.title, task.episode)} Download failed")
downloadsManager.removeDownload( downloadsManager.removeDownload(
DownloadedType( DownloadedType(
@@ -367,7 +388,11 @@ class AnimeDownloaderService : Service() {
) )
} Download completed" } Download completed"
) )
notificationManager.notify(NOTIFICATION_ID, builder.build()) if (notifi) {
withContext(Dispatchers.Main) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
}
snackString("${getTaskName(task.title, task.episode)} Download completed") snackString("${getTaskName(task.title, task.episode)} Download completed")
PrefManager.getAnimeDownloadPreferences().edit().putString( PrefManager.getAnimeDownloadPreferences().edit().putString(
task.getTaskName(), task.getTaskName(),
@@ -385,23 +410,20 @@ class AnimeDownloaderService : Service() {
broadcastDownloadFinished(task.episode) broadcastDownloadFinished(task.episode)
} else throw Exception("Download failed") } 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.log("Exception while downloading file: ${e.message}")
snackString("Exception while downloading file: ${e.message}")
e.printStackTrace()
Injekt.get<CrashlyticsInterface>().logException(e)
}
broadcastDownloadFailed(task.episode)
} }
} }
private fun saveMediaInfo(task: AnimeDownloadTask) { private fun saveMediaInfo(task: AnimeDownloadTask, directory: DocumentFile) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val directory =
getSubDirectory(this@AnimeDownloaderService, MediaType.ANIME, false, task.title)
?: throw Exception("Directory not found")
directory.findFile("media.json")?.forceDelete(this@AnimeDownloaderService) directory.findFile("media.json")?.forceDelete(this@AnimeDownloaderService)
val file = directory.createFile("application/json", "media.json") val file = directory.createFile("application/json", "media.json")
?: throw Exception("File not created") ?: throw Exception("File not created")

View File

@@ -48,6 +48,7 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import ani.dantotsu.util.customAlertDialog
import com.anggrayudi.storage.file.openInputStream import com.anggrayudi.storage.file.openInputStream
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.google.android.material.imageview.ShapeableImageView import com.google.android.material.imageview.ShapeableImageView
@@ -202,25 +203,24 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
val type: MediaType = MediaType.ANIME val type: MediaType = MediaType.ANIME
// Alert dialog to confirm deletion // Alert dialog to confirm deletion
val builder = requireContext().customAlertDialog().apply {
androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup) setTitle("Delete ${item.title}?")
builder.setTitle("Delete ${item.title}?") setMessage("Are you sure you want to delete ${item.title}?")
builder.setMessage("Are you sure you want to delete ${item.title}?") setPosButton(R.string.yes) {
builder.setPositiveButton("Yes") { _, _ -> downloadManager.removeMedia(item.title, type)
downloadManager.removeMedia(item.title, type) val mediaIds =
val mediaIds = PrefManager.getAnimeDownloadPreferences().all?.filter { it.key.contains(item.title) }?.values
PrefManager.getAnimeDownloadPreferences().all?.filter { it.key.contains(item.title) }?.values ?: emptySet()
?: emptySet() if (mediaIds.isEmpty()) {
if (mediaIds.isEmpty()) { snackString("No media found") // if this happens, terrible things have happened
snackString("No media found") // if this happens, terrible things have happened }
getDownloads()
} }
getDownloads() setNegButton(R.string.no) {
// Do nothing
}
show()
} }
builder.setNegativeButton("No") { _, _ ->
// Do nothing
}
val dialog = builder.show()
dialog.window?.setDimAmount(0.8f)
true true
} }
} }
@@ -288,10 +288,12 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
} }
downloadsJob = Job() downloadsJob = Job()
CoroutineScope(Dispatchers.IO + downloadsJob).launch { CoroutineScope(Dispatchers.IO + downloadsJob).launch {
val animeTitles = downloadManager.animeDownloadedTypes.map { it.titleName.findValidName() }.distinct() val animeTitles =
downloadManager.animeDownloadedTypes.map { it.titleName.findValidName() }.distinct()
val newAnimeDownloads = mutableListOf<OfflineAnimeModel>() val newAnimeDownloads = mutableListOf<OfflineAnimeModel>()
for (title in animeTitles) { for (title in animeTitles) {
val tDownloads = downloadManager.animeDownloadedTypes.filter { it.titleName.findValidName() == title } val tDownloads =
downloadManager.animeDownloadedTypes.filter { it.titleName.findValidName() == title }
val download = tDownloads.firstOrNull() ?: continue val download = tDownloads.firstOrNull() ?: continue
val offlineAnimeModel = loadOfflineAnimeModel(download) val offlineAnimeModel = loadOfflineAnimeModel(download)
if (offlineAnimeModel.title == "unknown") offlineAnimeModel.title = title if (offlineAnimeModel.title == "unknown") offlineAnimeModel.title = title
@@ -319,17 +321,20 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
) )
val gson = GsonBuilder() val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> { .registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl SChapterImpl()
}) })
.registerTypeAdapter(SAnime::class.java, InstanceCreator<SAnime> { .registerTypeAdapter(SAnime::class.java, InstanceCreator<SAnime> {
SAnimeImpl() // Provide an instance of SAnimeImpl SAnimeImpl()
}) })
.registerTypeAdapter(SEpisode::class.java, InstanceCreator<SEpisode> { .registerTypeAdapter(SEpisode::class.java, InstanceCreator<SEpisode> {
SEpisodeImpl() // Provide an instance of SEpisodeImpl SEpisodeImpl()
}) })
.create() .create()
val media = directory?.findFile("media.json") val media = directory?.findFile("media.json")
?: return loadMediaCompat(downloadedType) if (media == null) {
Logger.log("No media.json found at ${directory?.uri?.path}")
return loadMediaCompat(downloadedType)
}
val mediaJson = val mediaJson =
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use { media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
it?.readText() it?.readText()
@@ -394,6 +399,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
bannerUri bannerUri
) )
} catch (e: Exception) { } catch (e: Exception) {
Logger.log(e)
return try { return try {
loadOfflineAnimeModelCompat(downloadedType) loadOfflineAnimeModelCompat(downloadedType)
} catch (e: Exception) { } catch (e: Exception) {
@@ -401,7 +407,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
Logger.log(e) Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e) Injekt.get<CrashlyticsInterface>().logException(e)
OfflineAnimeModel( OfflineAnimeModel(
"unknown", downloadedType.titleName,
"0", "0",
"??", "??",
"??", "??",

View File

@@ -32,6 +32,7 @@ import ani.dantotsu.media.manga.MangaReadFragment.Companion.ACTION_DOWNLOAD_STAR
import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER import ani.dantotsu.media.manga.MangaReadFragment.Companion.EXTRA_CHAPTER_NUMBER
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import ani.dantotsu.util.NumberConverter.Companion.ofLength
import com.anggrayudi.storage.file.deleteRecursively import com.anggrayudi.storage.file.deleteRecursively
import com.anggrayudi.storage.file.forceDelete import com.anggrayudi.storage.file.forceDelete
import com.anggrayudi.storage.file.openOutputStream import com.anggrayudi.storage.file.openOutputStream
@@ -134,15 +135,15 @@ class MangaDownloaderService : Service() {
mutex.withLock { mutex.withLock {
downloadJobs[task.chapter] = job downloadJobs[task.chapter] = job
} }
job.join() // Wait for the job to complete before continuing to the next task job.join()
mutex.withLock { mutex.withLock {
downloadJobs.remove(task.chapter) downloadJobs.remove(task.chapter)
} }
updateNotification() // Update the notification after each task is completed updateNotification()
} }
if (MangaServiceDataSingleton.downloadQueue.isEmpty()) { if (MangaServiceDataSingleton.downloadQueue.isEmpty()) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
stopSelf() // Stop the service when the queue is empty stopSelf()
} }
} }
} }
@@ -181,7 +182,7 @@ class MangaDownloaderService : Service() {
suspend fun download(task: DownloadTask) { suspend fun download(task: DownloadTask) {
try { try {
withContext(Dispatchers.Main) { withContext(Dispatchers.IO) {
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission( ContextCompat.checkSelfPermission(
this@MangaDownloaderService, this@MangaDownloaderService,
@@ -194,18 +195,27 @@ class MangaDownloaderService : Service() {
val deferredMap = mutableMapOf<Int, Deferred<Bitmap?>>() val deferredMap = mutableMapOf<Int, Deferred<Bitmap?>>()
builder.setContentText("Downloading ${task.title} - ${task.chapter}") builder.setContentText("Downloading ${task.title} - ${task.chapter}")
if (notifi) { if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build()) withContext(Dispatchers.Main) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
} }
getSubDirectory( val baseOutputDir = getSubDirectory(
this@MangaDownloaderService,
MediaType.MANGA,
false,
task.title
) ?: throw Exception("Base output directory not found")
val outputDir = getSubDirectory(
this@MangaDownloaderService, this@MangaDownloaderService,
MediaType.MANGA, MediaType.MANGA,
false, false,
task.title, task.title,
task.chapter task.chapter
)?.deleteRecursively(this@MangaDownloaderService) ) ?: throw Exception("Output directory not found")
outputDir.deleteRecursively(this@MangaDownloaderService, true)
// Loop through each ImageData object from the task
var farthest = 0 var farthest = 0
for ((index, image) in task.imageData.withIndex()) { for ((index, image) in task.imageData.withIndex()) {
if (deferredMap.size >= task.simultaneousDownloads) { if (deferredMap.size >= task.simultaneousDownloads) {
@@ -222,64 +232,76 @@ class MangaDownloaderService : Service() {
image.page, image.page,
image.source image.source
) )
if (bitmap == null) {
snackString("${task.chapter} - Retrying to download page ${index.ofLength(3)}, attempt ${retryCount + 1}.")
}
retryCount++ retryCount++
} }
if (bitmap != null) { if (bitmap == null) {
saveToDisk("$index.jpg", bitmap, task.title, task.chapter) 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++ farthest++
builder.setProgress(task.imageData.size, farthest, false) builder.setProgress(task.imageData.size, farthest, false)
broadcastDownloadProgress( broadcastDownloadProgress(
task.chapter, task.uniqueName,
farthest * 100 / task.imageData.size farthest * 100 / task.imageData.size
) )
if (notifi) { if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build()) withContext(Dispatchers.Main) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
} }
bitmap bitmap
} }
} }
// Wait for any remaining deferred to complete
deferredMap.values.awaitAll() deferredMap.values.awaitAll()
builder.setContentText("${task.title} - ${task.chapter} Download complete") withContext(Dispatchers.Main) {
.setProgress(0, 0, false) builder.setContentText("${task.title} - ${task.chapter} Download complete")
notificationManager.notify(NOTIFICATION_ID, builder.build()) .setProgress(0, 0, false)
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
}
saveMediaInfo(task) saveMediaInfo(task, baseOutputDir)
downloadsManager.addDownload( downloadsManager.addDownload(
DownloadedType( DownloadedType(
task.title, task.title,
task.chapter, task.chapter,
MediaType.MANGA MediaType.MANGA,
scanlator = task.scanlator,
) )
) )
broadcastDownloadFinished(task.chapter) broadcastDownloadFinished(task.uniqueName)
snackString("${task.title} - ${task.chapter} Download finished") snackString("${task.title} - ${task.chapter} Download finished")
} }
} catch (e: Exception) { } catch (e: Exception) {
Logger.log("Exception while downloading file: ${e.message}") Logger.log("Exception while downloading file: ${e.message}")
snackString("Exception while downloading file: ${e.message}") snackString("Exception while downloading file: ${e.message}")
Injekt.get<CrashlyticsInterface>().logException(e) Injekt.get<CrashlyticsInterface>().logException(e)
broadcastDownloadFailed(task.chapter) broadcastDownloadFailed(task.uniqueName)
} }
} }
private fun saveToDisk(fileName: String, bitmap: Bitmap, title: String, chapter: String) { private fun saveToDisk(
fileName: String,
directory: DocumentFile,
bitmap: Bitmap
) {
try { try {
// Define the directory within the private external storage space
val directory = getSubDirectory(this, MediaType.MANGA, false, title, chapter)
?: throw Exception("Directory not found")
directory.findFile(fileName)?.forceDelete(this) directory.findFile(fileName)?.forceDelete(this)
// Create a file reference within that directory for the image
val file = val file =
directory.createFile("image/jpeg", fileName) ?: throw Exception("File not created") directory.createFile("image/jpeg", fileName) ?: throw Exception("File not created")
// Use a FileOutputStream to write the bitmap to the file
file.openOutputStream(this, false).use { outputStream -> file.openOutputStream(this, false).use { outputStream ->
if (outputStream == null) throw Exception("Output stream is null") if (outputStream == null) throw Exception("Output stream is null")
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
@@ -292,11 +314,8 @@ class MangaDownloaderService : Service() {
} }
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
private fun saveMediaInfo(task: DownloadTask) { private fun saveMediaInfo(task: DownloadTask, directory: DocumentFile) {
launchIO { launchIO {
val directory =
getSubDirectory(this@MangaDownloaderService, MediaType.MANGA, false, task.title)
?: throw Exception("Directory not found")
directory.findFile("media.json")?.forceDelete(this@MangaDownloaderService) directory.findFile("media.json")?.forceDelete(this@MangaDownloaderService)
val file = directory.createFile("application/json", "media.json") val file = directory.createFile("application/json", "media.json")
?: throw Exception("File not created") ?: throw Exception("File not created")
@@ -411,11 +430,15 @@ class MangaDownloaderService : Service() {
data class DownloadTask( data class DownloadTask(
val title: String, val title: String,
val chapter: String, val chapter: String,
val scanlator: String,
val imageData: List<ImageData>, val imageData: List<ImageData>,
val sourceMedia: Media? = null, val sourceMedia: Media? = null,
val retries: Int = 2, val retries: Int = 2,
val simultaneousDownloads: Int = 2, val simultaneousDownloads: Int = 2,
) ) {
val uniqueName: String
get() = "$chapter-$scanlator"
}
companion object { companion object {
private const val NOTIFICATION_ID = 1103 private const val NOTIFICATION_ID = 1103

View File

@@ -46,6 +46,7 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import ani.dantotsu.util.customAlertDialog
import com.anggrayudi.storage.file.openInputStream import com.anggrayudi.storage.file.openInputStream
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.google.android.material.imageview.ShapeableImageView import com.google.android.material.imageview.ShapeableImageView
@@ -171,7 +172,11 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
val item = adapter.getItem(position) as OfflineMangaModel val item = adapter.getItem(position) as OfflineMangaModel
val media = val media =
downloadManager.mangaDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) } downloadManager.mangaDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) }
?: downloadManager.novelDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) } ?: downloadManager.novelDownloadedTypes.firstOrNull {
it.titleName.compareName(
item.title
)
}
media?.let { media?.let {
lifecycleScope.launch { lifecycleScope.launch {
ContextCompat.startActivity( ContextCompat.startActivity(
@@ -197,19 +202,15 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
MediaType.NOVEL MediaType.NOVEL
} }
// Alert dialog to confirm deletion // Alert dialog to confirm deletion
val builder = requireContext().customAlertDialog().apply {
androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup) setTitle("Delete ${item.title}?")
builder.setTitle("Delete ${item.title}?") setMessage("Are you sure you want to delete ${item.title}?")
builder.setMessage("Are you sure you want to delete ${item.title}?") setPosButton(R.string.yes) {
builder.setPositiveButton("Yes") { _, _ -> downloadManager.removeMedia(item.title, type)
downloadManager.removeMedia(item.title, type) getDownloads()
getDownloads() }
} setNegButton(R.string.no)
builder.setNegativeButton("No") { _, _ -> }.show()
// Do nothing
}
val dialog = builder.show()
dialog.window?.setDimAmount(0.8f)
true true
} }
} }
@@ -279,10 +280,12 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
downloads = listOf() downloads = listOf()
downloadsJob = Job() downloadsJob = Job()
CoroutineScope(Dispatchers.IO + downloadsJob).launch { CoroutineScope(Dispatchers.IO + downloadsJob).launch {
val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.titleName.findValidName() }.distinct() val mangaTitles =
downloadManager.mangaDownloadedTypes.map { it.titleName.findValidName() }.distinct()
val newMangaDownloads = mutableListOf<OfflineMangaModel>() val newMangaDownloads = mutableListOf<OfflineMangaModel>()
for (title in mangaTitles) { for (title in mangaTitles) {
val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.titleName.findValidName() == title } val tDownloads =
downloadManager.mangaDownloadedTypes.filter { it.titleName.findValidName() == title }
val download = tDownloads.firstOrNull() ?: continue val download = tDownloads.firstOrNull() ?: continue
val offlineMangaModel = loadOfflineMangaModel(download) val offlineMangaModel = loadOfflineMangaModel(download)
newMangaDownloads += offlineMangaModel newMangaDownloads += offlineMangaModel
@@ -291,7 +294,8 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
val novelTitles = downloadManager.novelDownloadedTypes.map { it.titleName }.distinct() val novelTitles = downloadManager.novelDownloadedTypes.map { it.titleName }.distinct()
val newNovelDownloads = mutableListOf<OfflineMangaModel>() val newNovelDownloads = mutableListOf<OfflineMangaModel>()
for (title in novelTitles) { for (title in novelTitles) {
val tDownloads = downloadManager.novelDownloadedTypes.filter { it.titleName.findValidName() == title } val tDownloads =
downloadManager.novelDownloadedTypes.filter { it.titleName.findValidName() == title }
val download = tDownloads.firstOrNull() ?: continue val download = tDownloads.firstOrNull() ?: continue
val offlineMangaModel = loadOfflineMangaModel(download) val offlineMangaModel = loadOfflineMangaModel(download)
newNovelDownloads += offlineMangaModel newNovelDownloads += offlineMangaModel
@@ -320,11 +324,14 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
) )
val gson = GsonBuilder() val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> { .registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl SChapterImpl()
}) })
.create() .create()
val media = directory?.findFile("media.json") val media = directory?.findFile("media.json")
?: return DownloadCompat.loadMediaCompat(downloadedType) if (media == null) {
Logger.log("No media.json found at ${directory?.uri?.path}")
return DownloadCompat.loadMediaCompat(downloadedType)
}
val mediaJson = val mediaJson =
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use { media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
it?.readText() it?.readText()
@@ -340,7 +347,6 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
private suspend fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel { private suspend fun loadOfflineMangaModel(downloadedType: DownloadedType): OfflineMangaModel {
val type = downloadedType.type.asText() val type = downloadedType.type.asText()
//load media.json and convert to media class with gson
try { try {
val directory = getSubDirectory( val directory = getSubDirectory(
context ?: currContext()!!, downloadedType.type, context ?: currContext()!!, downloadedType.type,
@@ -378,6 +384,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
bannerUri bannerUri
) )
} catch (e: Exception) { } catch (e: Exception) {
Logger.log(e)
return try { return try {
loadOfflineMangaModelCompat(downloadedType) loadOfflineMangaModelCompat(downloadedType)
} catch (e: Exception) { } catch (e: Exception) {
@@ -385,7 +392,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener {
Logger.log(e) Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e) Injekt.get<CrashlyticsInterface>().logException(e)
return OfflineMangaModel( return OfflineMangaModel(
"unknown", downloadedType.titleName,
"0", "0",
"??", "??",
"??", "??",

View File

@@ -239,6 +239,13 @@ class NovelDownloaderService : Service() {
return@withContext return@withContext
} }
val baseDirectory = getSubDirectory(
this@NovelDownloaderService,
MediaType.NOVEL,
false,
task.title
) ?: throw Exception("Directory not found")
// Start the download // Start the download
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
@@ -334,7 +341,7 @@ class NovelDownloaderService : Service() {
notificationManager.notify(NOTIFICATION_ID, builder.build()) notificationManager.notify(NOTIFICATION_ID, builder.build())
} }
saveMediaInfo(task) saveMediaInfo(task, baseDirectory)
downloadsManager.addDownload( downloadsManager.addDownload(
DownloadedType( DownloadedType(
task.title, task.title,
@@ -354,15 +361,8 @@ class NovelDownloaderService : Service() {
} }
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
private fun saveMediaInfo(task: DownloadTask) { private fun saveMediaInfo(task: DownloadTask, directory: DocumentFile) {
launchIO { launchIO {
val directory =
getSubDirectory(
this@NovelDownloaderService,
MediaType.NOVEL,
false,
task.title
) ?: throw Exception("Directory not found")
directory.findFile("media.json")?.forceDelete(this@NovelDownloaderService) directory.findFile("media.json")?.forceDelete(this@NovelDownloaderService)
val file = directory.createFile("application/json", "media.json") val file = directory.createFile("application/json", "media.json")
?: throw Exception("File not created") ?: throw Exception("File not created")

View File

@@ -3,7 +3,6 @@ package ani.dantotsu.download.video
import android.Manifest import android.Manifest
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.AlertDialog
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
@@ -29,10 +28,10 @@ import ani.dantotsu.download.anime.AnimeDownloaderService
import ani.dantotsu.download.anime.AnimeServiceDataSingleton import ani.dantotsu.download.anime.AnimeServiceDataSingleton
import ani.dantotsu.media.Media import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaType import ani.dantotsu.media.MediaType
import ani.dantotsu.parsers.Subtitle
import ani.dantotsu.parsers.Video import ani.dantotsu.parsers.Video
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import ani.dantotsu.util.customAlertDialog
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@@ -72,19 +71,19 @@ object Helper {
episodeImage episodeImage
) )
val downloadsManger = Injekt.get<DownloadsManager>() val downloadsManager = Injekt.get<DownloadsManager>()
val downloadCheck = downloadsManger val downloadCheck = downloadsManager
.queryDownload(title, episode, MediaType.ANIME) .queryDownload(title, episode, MediaType.ANIME)
if (downloadCheck) { if (downloadCheck) {
AlertDialog.Builder(context, R.style.MyPopup) context.customAlertDialog().apply {
.setTitle("Download Exists") setTitle("Download Exists")
.setMessage("A download for this episode already exists. Do you want to overwrite it?") setMessage("A download for this episode already exists. Do you want to overwrite it?")
.setPositiveButton("Yes") { _, _ -> setPosButton(R.string.yes) {
PrefManager.getAnimeDownloadPreferences().edit() PrefManager.getAnimeDownloadPreferences().edit()
.remove(animeDownloadTask.getTaskName()) .remove(animeDownloadTask.getTaskName())
.apply() .apply()
downloadsManger.removeDownload( downloadsManager.removeDownload(
DownloadedType( DownloadedType(
title, title,
episode, episode,
@@ -99,8 +98,9 @@ object Helper {
} }
} }
} }
.setNegativeButton("No") { _, _ -> } setNegButton(R.string.no)
.show() show()
}
} else { } else {
AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask) AnimeServiceDataSingleton.downloadQueue.offer(animeDownloadTask)
if (!AnimeServiceDataSingleton.isServiceRunning) { if (!AnimeServiceDataSingleton.isServiceRunning) {
@@ -177,6 +177,7 @@ object Helper {
downloadManager downloadManager
} }
} }
@Deprecated("exoplayer download manager is no longer used") @Deprecated("exoplayer download manager is no longer used")
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
fun getSimpleCache(context: Context): SimpleCache { fun getSimpleCache(context: Context): SimpleCache {
@@ -189,6 +190,7 @@ object Helper {
simpleCache!! simpleCache!!
} }
} }
@Synchronized @Synchronized
@Deprecated("exoplayer download manager is no longer used") @Deprecated("exoplayer download manager is no longer used")
private fun getDownloadDirectory(context: Context): File { private fun getDownloadDirectory(context: Context): File {
@@ -200,12 +202,16 @@ object Helper {
} }
return downloadDirectory!! return downloadDirectory!!
} }
@Deprecated("exoplayer download manager is no longer used") @Deprecated("exoplayer download manager is no longer used")
private var download: DownloadManager? = null private var download: DownloadManager? = null
@Deprecated("exoplayer download manager is no longer used") @Deprecated("exoplayer download manager is no longer used")
private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads" private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads"
@Deprecated("exoplayer download manager is no longer used") @Deprecated("exoplayer download manager is no longer used")
private var simpleCache: SimpleCache? = null private var simpleCache: SimpleCache? = null
@Deprecated("exoplayer download manager is no longer used") @Deprecated("exoplayer download manager is no longer used")
private var downloadDirectory: File? = null private var downloadDirectory: File? = null
} }

View File

@@ -22,9 +22,9 @@ import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.Refresh import ani.dantotsu.Refresh
import ani.dantotsu.bottomBar import ani.dantotsu.bottomBar
import ani.dantotsu.connections.anilist.AniMangaSearchResults
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistAnimeViewModel import ani.dantotsu.connections.anilist.AnilistAnimeViewModel
import ani.dantotsu.connections.anilist.SearchResults
import ani.dantotsu.connections.anilist.getUserId import ani.dantotsu.connections.anilist.getUserId
import ani.dantotsu.databinding.FragmentAnimeBinding import ani.dantotsu.databinding.FragmentAnimeBinding
import ani.dantotsu.media.MediaAdaptor import ani.dantotsu.media.MediaAdaptor
@@ -38,6 +38,7 @@ import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -99,7 +100,7 @@ class AnimeFragment : Fragment() {
var loading = true var loading = true
if (model.notSet) { if (model.notSet) {
model.notSet = false model.notSet = false
model.searchResults = SearchResults( model.aniMangaSearchResults = AniMangaSearchResults(
"ANIME", "ANIME",
isAdult = false, isAdult = false,
onList = false, onList = false,
@@ -108,7 +109,7 @@ class AnimeFragment : Fragment() {
sort = Anilist.sortBy[1] 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 progressAdaptor = ProgressAdapter(searched = model.searched)
val adapter = ConcatAdapter(animePageAdapter, popularAdaptor, progressAdaptor) val adapter = ConcatAdapter(animePageAdapter, popularAdaptor, progressAdaptor)
binding.animePageRecyclerView.adapter = adapter binding.animePageRecyclerView.adapter = adapter
@@ -141,7 +142,7 @@ class AnimeFragment : Fragment() {
animePageAdapter.onIncludeListClick = { checked -> animePageAdapter.onIncludeListClick = { checked ->
oldIncludeList = !checked oldIncludeList = !checked
loading = true loading = true
model.searchResults.results.clear() model.aniMangaSearchResults.results.clear()
popularAdaptor.notifyDataSetChanged() popularAdaptor.notifyDataSetChanged()
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
model.loadPopular("ANIME", sort = Anilist.sortBy[1], onList = checked) model.loadPopular("ANIME", sort = Anilist.sortBy[1], onList = checked)
@@ -151,17 +152,17 @@ class AnimeFragment : Fragment() {
model.getPopular().observe(viewLifecycleOwner) { model.getPopular().observe(viewLifecycleOwner) {
if (it != null) { if (it != null) {
if (oldIncludeList == (it.onList != false)) { if (oldIncludeList == (it.onList != false)) {
val prev = model.searchResults.results.size val prev = model.aniMangaSearchResults.results.size
model.searchResults.results.addAll(it.results) model.aniMangaSearchResults.results.addAll(it.results)
popularAdaptor.notifyItemRangeInserted(prev, it.results.size) popularAdaptor.notifyItemRangeInserted(prev, it.results.size)
} else { } else {
model.searchResults.results.addAll(it.results) model.aniMangaSearchResults.results.addAll(it.results)
popularAdaptor.notifyDataSetChanged() popularAdaptor.notifyDataSetChanged()
oldIncludeList = it.onList ?: true oldIncludeList = it.onList ?: true
} }
model.searchResults.onList = it.onList model.aniMangaSearchResults.onList = it.onList
model.searchResults.hasNextPage = it.hasNextPage model.aniMangaSearchResults.hasNextPage = it.hasNextPage
model.searchResults.page = it.page model.aniMangaSearchResults.page = it.page
if (it.hasNextPage) if (it.hasNextPage)
progressAdaptor.bar?.visibility = View.VISIBLE progressAdaptor.bar?.visibility = View.VISIBLE
else { else {
@@ -176,10 +177,10 @@ class AnimeFragment : Fragment() {
RecyclerView.OnScrollListener() { RecyclerView.OnScrollListener() {
override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) {
if (!v.canScrollVertically(1)) { 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) { scope.launch(Dispatchers.IO) {
loading = true loading = true
model.loadNextPage(model.searchResults) model.loadNextPage(model.aniMangaSearchResults)
} }
} }
} }
@@ -276,8 +277,9 @@ class AnimeFragment : Fragment() {
running = true running = true
scope.launch { scope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
Anilist.userid = PrefManager.getNullableVal<String>(PrefName.AnilistUserId, null) Anilist.userid =
?.toIntOrNull() PrefManager.getNullableVal<String>(PrefName.AnilistUserId, null)
?.toIntOrNull()
if (Anilist.userid == null) { if (Anilist.userid == null) {
getUserId(requireContext()) { getUserId(requireContext()) {
load() load()
@@ -289,15 +291,20 @@ class AnimeFragment : Fragment() {
} }
} }
} }
model.loaded = true }
model.loadTrending(1) model.loaded = true
model.loadAll() val loadTrending = async(Dispatchers.IO) { model.loadTrending(1) }
val loadAll = async(Dispatchers.IO) { model.loadAll() }
val loadPopular = async(Dispatchers.IO) {
model.loadPopular( model.loadPopular(
"ANIME", sort = Anilist.sortBy[1], onList = PrefManager.getVal( "ANIME",
PrefName.PopularAnimeList sort = Anilist.sortBy[1],
) onList = PrefManager.getVal(PrefName.PopularAnimeList)
) )
} }
loadTrending.await()
loadAll.await()
loadPopular.await()
live.postValue(false) live.postValue(false)
_binding?.animeRefresh?.isRefreshing = false _binding?.animeRefresh?.isRefreshing = false
running = false running = false

View File

@@ -13,7 +13,6 @@ import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@@ -21,7 +20,6 @@ import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.MediaPageTransformer import ani.dantotsu.MediaPageTransformer
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemAnimePageBinding import ani.dantotsu.databinding.ItemAnimePageBinding
import ani.dantotsu.databinding.LayoutTrendingBinding import ani.dantotsu.databinding.LayoutTrendingBinding
import ani.dantotsu.getAppString import ani.dantotsu.getAppString
@@ -83,13 +81,21 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
updateAvatar() updateAvatar()
trendingBinding.searchBar.hint = "ANIME" trendingBinding.searchBar.hint = binding.root.context.getString(R.string.search)
trendingBinding.searchBarText.setOnClickListener { trendingBinding.searchBarText.setOnClickListener {
ContextCompat.startActivity( val context = binding.root.context
it.context, if (PrefManager.getVal(PrefName.AniMangaSearchDirect) && Anilist.token != null) {
Intent(it.context, SearchActivity::class.java).putExtra("type", "ANIME"), ContextCompat.startActivity(
null context,
) Intent(context, SearchActivity::class.java).putExtra("type", "ANIME"),
null
)
} else {
SearchBottomSheet.newInstance().show(
(context as AppCompatActivity).supportFragmentManager,
"search"
)
}
} }
trendingBinding.userAvatar.setSafeOnClickListener { trendingBinding.userAvatar.setSafeOnClickListener {
@@ -111,8 +117,8 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
trendingBinding.searchBar.performClick() trendingBinding.searchBar.performClick()
} }
trendingBinding.notificationCount.visibility = trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE && PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString() trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
listOf( listOf(
@@ -259,7 +265,15 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
} }
} }
fun init(adaptor: MediaAdaptor, recyclerView: RecyclerView, progress: View, title: View , more: View , string: String, media : MutableList<Media>) { fun init(
adaptor: MediaAdaptor,
recyclerView: RecyclerView,
progress: View,
title: View,
more: View,
string: String,
media: MutableList<Media>
) {
progress.visibility = View.GONE progress.visibility = View.GONE
recyclerView.adapter = adaptor recyclerView.adapter = adaptor
recyclerView.layoutManager = recyclerView.layoutManager =
@@ -268,8 +282,9 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
LinearLayoutManager.HORIZONTAL, LinearLayoutManager.HORIZONTAL,
false false
) )
MediaListViewActivity.passedMedia = media.toCollection(ArrayList())
more.setOnClickListener { more.setOnClickListener {
MediaListViewActivity.passedMedia = media.toCollection(ArrayList())
ContextCompat.startActivity( ContextCompat.startActivity(
it.context, Intent(it.context, MediaListViewActivity::class.java) it.context, Intent(it.context, MediaListViewActivity::class.java)
.putExtra("title", string), .putExtra("title", string),
@@ -294,8 +309,8 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
fun updateNotificationCount() { fun updateNotificationCount() {
if (this::binding.isInitialized) { if (this::binding.isInitialized) {
trendingBinding.notificationCount.visibility = trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE && PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString() trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
} }
} }

View File

@@ -48,8 +48,8 @@ import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlin.math.max import kotlin.math.max
@@ -92,6 +92,7 @@ class HomeFragment : Fragment() {
) )
binding.homeUserDataProgressBar.visibility = View.GONE binding.homeUserDataProgressBar.visibility = View.GONE
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0 binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString() binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
binding.homeAnimeList.setOnClickListener { binding.homeAnimeList.setOnClickListener {
@@ -132,6 +133,12 @@ class HomeFragment : Fragment() {
"dialog" "dialog"
) )
} }
binding.searchImageContainer.setSafeOnClickListener {
SearchBottomSheet.newInstance().show(
(it.context as androidx.appcompat.app.AppCompatActivity).supportFragmentManager,
"search"
)
}
binding.homeUserAvatarContainer.setOnLongClickListener { binding.homeUserAvatarContainer.setOnLongClickListener {
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
ContextCompat.startActivity( ContextCompat.startActivity(
@@ -456,51 +463,58 @@ class HomeFragment : Fragment() {
var running = false var running = false
val live = Refresh.activity.getOrPut(1) { MutableLiveData(true) } val live = Refresh.activity.getOrPut(1) { MutableLiveData(true) }
live.observe(viewLifecycleOwner) live.observe(viewLifecycleOwner) { shouldRefresh ->
{ if (!running && shouldRefresh) {
if (!running && it) {
running = true running = true
scope.launch { scope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
//Get userData First // Get user data first
Anilist.userid = Anilist.userid =
PrefManager.getNullableVal<String>(PrefName.AnilistUserId, null) PrefManager.getNullableVal<String>(PrefName.AnilistUserId, null)
?.toIntOrNull() ?.toIntOrNull()
if (Anilist.userid == null) { if (Anilist.userid == null) {
getUserId(requireContext()) { withContext(Dispatchers.Main) {
load()
}
} else {
CoroutineScope(Dispatchers.IO).launch {
getUserId(requireContext()) { getUserId(requireContext()) {
load() load()
} }
} }
} else {
getUserId(requireContext()) {
load()
}
} }
model.loaded = true model.loaded = true
CoroutineScope(Dispatchers.IO).launch { model.setListImages()
model.setListImages() }
}
var empty = true var empty = true
val homeLayoutShow: List<Boolean> = val homeLayoutShow: List<Boolean> = PrefManager.getVal(PrefName.HomeLayout)
PrefManager.getVal(PrefName.HomeLayout)
model.initHomePage() withContext(Dispatchers.Main) {
(array.indices).forEach { i -> homeLayoutShow.indices.forEach { i ->
if (homeLayoutShow.elementAt(i)) { if (homeLayoutShow.elementAt(i)) {
empty = false empty = false
} else withContext(Dispatchers.Main) { } else {
containers[i].visibility = View.GONE 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) live.postValue(false)
_binding?.homeRefresh?.isRefreshing = false _binding?.homeRefresh?.isRefreshing = false
running = false running = false
} }
binding.homeHiddenItemsContainer.visibility = View.GONE
} }
} }
} }
@@ -508,6 +522,7 @@ class HomeFragment : Fragment() {
if (!model.loaded) Refresh.activity[1]!!.postValue(true) if (!model.loaded) Refresh.activity[1]!!.postValue(true)
if (_binding != null) { if (_binding != null) {
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0 binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString() binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
} }
super.onResume() super.onResume()

View File

@@ -1,24 +1,23 @@
package ani.dantotsu.home package ani.dantotsu.home
import android.app.AlertDialog
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.databinding.DialogUserAgentBinding
import ani.dantotsu.databinding.FragmentLoginBinding import ani.dantotsu.databinding.FragmentLoginBinding
import ani.dantotsu.openLinkInBrowser import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.settings.saving.internal.PreferenceKeystore import ani.dantotsu.settings.saving.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferencePackager import ani.dantotsu.settings.saving.internal.PreferencePackager
import ani.dantotsu.toast import ani.dantotsu.toast
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import com.google.android.material.textfield.TextInputEditText import ani.dantotsu.util.customAlertDialog
class LoginFragment : Fragment() { class LoginFragment : Fragment() {
@@ -94,38 +93,31 @@ class LoginFragment : Fragment() {
val password = CharArray(16).apply { fill('0') } val password = CharArray(16).apply { fill('0') }
// Inflate the dialog layout // Inflate the dialog layout
val dialogView = val dialogView = DialogUserAgentBinding.inflate(layoutInflater).apply {
LayoutInflater.from(requireActivity()).inflate(R.layout.dialog_user_agent, null) userAgentTextBox.hint = "Password"
dialogView.findViewById<TextInputEditText>(R.id.userAgentTextBox)?.hint = "Password" subtitle.visibility = View.VISIBLE
val subtitleTextView = dialogView.findViewById<TextView>(R.id.subtitle) subtitle.text = getString(R.string.enter_password_to_decrypt_file)
subtitleTextView?.visibility = View.VISIBLE }
subtitleTextView?.text = "Enter your password to decrypt the file"
val dialog = AlertDialog.Builder(requireActivity(), R.style.MyPopup) requireActivity().customAlertDialog().apply {
.setTitle("Enter Password") setTitle("Enter Password")
.setView(dialogView) setCustomView(dialogView.root)
.setPositiveButton("OK", null) setPosButton(R.string.ok) {
.setNegativeButton("Cancel") { dialog, _ -> 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') password.fill('0')
dialog.dismiss()
callback(null) callback(null)
} }
.create() }.show()
dialog.window?.setDimAmount(0.8f)
dialog.show()
// Override the positive button here
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val editText = dialog.findViewById<TextInputEditText>(R.id.userAgentTextBox)
if (editText?.text?.isNotBlank() == true) {
editText.text?.toString()?.trim()?.toCharArray(password)
dialog.dismiss()
callback(password)
} else {
toast("Password cannot be empty")
}
}
} }
private fun restartApp() { private fun restartApp() {

View File

@@ -20,9 +20,9 @@ import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.Refresh import ani.dantotsu.Refresh
import ani.dantotsu.bottomBar import ani.dantotsu.bottomBar
import ani.dantotsu.connections.anilist.AniMangaSearchResults
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistMangaViewModel import ani.dantotsu.connections.anilist.AnilistMangaViewModel
import ani.dantotsu.connections.anilist.SearchResults
import ani.dantotsu.connections.anilist.getUserId import ani.dantotsu.connections.anilist.getUserId
import ani.dantotsu.databinding.FragmentMangaBinding import ani.dantotsu.databinding.FragmentMangaBinding
import ani.dantotsu.media.MediaAdaptor import ani.dantotsu.media.MediaAdaptor
@@ -35,6 +35,7 @@ import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -93,7 +94,7 @@ class MangaFragment : Fragment() {
var loading = true var loading = true
if (model.notSet) { if (model.notSet) {
model.notSet = false model.notSet = false
model.searchResults = SearchResults( model.aniMangaSearchResults = AniMangaSearchResults(
"MANGA", "MANGA",
isAdult = false, isAdult = false,
onList = false, onList = false,
@@ -102,7 +103,7 @@ class MangaFragment : Fragment() {
sort = Anilist.sortBy[1] 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 progressAdaptor = ProgressAdapter(searched = model.searched)
binding.mangaPageRecyclerView.adapter = binding.mangaPageRecyclerView.adapter =
ConcatAdapter(mangaPageAdapter, popularAdaptor, progressAdaptor) ConcatAdapter(mangaPageAdapter, popularAdaptor, progressAdaptor)
@@ -134,10 +135,10 @@ class MangaFragment : Fragment() {
RecyclerView.OnScrollListener() { RecyclerView.OnScrollListener() {
override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) {
if (!v.canScrollVertically(1)) { 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) { scope.launch(Dispatchers.IO) {
loading = true loading = true
model.loadNextPage(model.searchResults) model.loadNextPage(model.aniMangaSearchResults)
} }
} }
} }
@@ -168,7 +169,10 @@ class MangaFragment : Fragment() {
} }
model.getPopularManga().observe(viewLifecycleOwner) { model.getPopularManga().observe(viewLifecycleOwner) {
if (it != null) { if (it != null) {
mangaPageAdapter.updateTrendingManga(MediaAdaptor(0, it, requireActivity()), it) mangaPageAdapter.updateTrendingManga(
MediaAdaptor(0, it, requireActivity()),
it
)
} }
} }
model.getPopularManhwa().observe(viewLifecycleOwner) { model.getPopularManhwa().observe(viewLifecycleOwner) {
@@ -219,7 +223,7 @@ class MangaFragment : Fragment() {
mangaPageAdapter.onIncludeListClick = { checked -> mangaPageAdapter.onIncludeListClick = { checked ->
oldIncludeList = !checked oldIncludeList = !checked
loading = true loading = true
model.searchResults.results.clear() model.aniMangaSearchResults.results.clear()
popularAdaptor.notifyDataSetChanged() popularAdaptor.notifyDataSetChanged()
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
model.loadPopular("MANGA", sort = Anilist.sortBy[1], onList = checked) model.loadPopular("MANGA", sort = Anilist.sortBy[1], onList = checked)
@@ -229,17 +233,17 @@ class MangaFragment : Fragment() {
model.getPopular().observe(viewLifecycleOwner) { model.getPopular().observe(viewLifecycleOwner) {
if (it != null) { if (it != null) {
if (oldIncludeList == (it.onList != false)) { if (oldIncludeList == (it.onList != false)) {
val prev = model.searchResults.results.size val prev = model.aniMangaSearchResults.results.size
model.searchResults.results.addAll(it.results) model.aniMangaSearchResults.results.addAll(it.results)
popularAdaptor.notifyItemRangeInserted(prev, it.results.size) popularAdaptor.notifyItemRangeInserted(prev, it.results.size)
} else { } else {
model.searchResults.results.addAll(it.results) model.aniMangaSearchResults.results.addAll(it.results)
popularAdaptor.notifyDataSetChanged() popularAdaptor.notifyDataSetChanged()
oldIncludeList = it.onList ?: true oldIncludeList = it.onList ?: true
} }
model.searchResults.onList = it.onList model.aniMangaSearchResults.onList = it.onList
model.searchResults.hasNextPage = it.hasNextPage model.aniMangaSearchResults.hasNextPage = it.hasNextPage
model.searchResults.page = it.page model.aniMangaSearchResults.page = it.page
if (it.hasNextPage) if (it.hasNextPage)
progressAdaptor.bar?.visibility = View.VISIBLE progressAdaptor.bar?.visibility = View.VISIBLE
else { else {
@@ -261,8 +265,9 @@ class MangaFragment : Fragment() {
running = true running = true
scope.launch { scope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
Anilist.userid = PrefManager.getNullableVal<String>(PrefName.AnilistUserId, null) Anilist.userid =
?.toIntOrNull() PrefManager.getNullableVal<String>(PrefName.AnilistUserId, null)
?.toIntOrNull()
if (Anilist.userid == null) { if (Anilist.userid == null) {
getUserId(requireContext()) { getUserId(requireContext()) {
load() load()
@@ -274,15 +279,22 @@ class MangaFragment : Fragment() {
} }
} }
} }
model.loaded = true }
model.loadTrending() model.loaded = true
model.loadAll() val loadTrending = async(Dispatchers.IO) { model.loadTrending() }
val loadAll = async(Dispatchers.IO) { model.loadAll() }
val loadPopular = async(Dispatchers.IO) {
model.loadPopular( model.loadPopular(
"MANGA", sort = Anilist.sortBy[1], onList = PrefManager.getVal( "MANGA",
PrefName.PopularMangaList sort = Anilist.sortBy[1],
) onList = PrefManager.getVal(PrefName.PopularAnimeList)
) )
} }
loadTrending.await()
loadAll.await()
loadPopular.await()
live.postValue(false) live.postValue(false)
_binding?.mangaRefresh?.isRefreshing = false _binding?.mangaRefresh?.isRefreshing = false
running = false running = false

View File

@@ -80,14 +80,23 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
updateAvatar() updateAvatar()
trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0 trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString() trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
trendingBinding.searchBar.hint = "MANGA" trendingBinding.searchBar.hint = binding.root.context.getString(R.string.search)
trendingBinding.searchBarText.setOnClickListener { trendingBinding.searchBarText.setOnClickListener {
ContextCompat.startActivity( val context = binding.root.context
it.context, if (PrefManager.getVal(PrefName.AniMangaSearchDirect) && Anilist.token != null) {
Intent(it.context, SearchActivity::class.java).putExtra("type", "MANGA"), ContextCompat.startActivity(
null context,
) Intent(context, SearchActivity::class.java).putExtra("type", "MANGA"),
null
)
} else {
SearchBottomSheet.newInstance().show(
(context as AppCompatActivity).supportFragmentManager,
"search"
)
}
} }
trendingBinding.userAvatar.setSafeOnClickListener { trendingBinding.userAvatar.setSafeOnClickListener {
@@ -257,10 +266,10 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
adaptor: MediaAdaptor, adaptor: MediaAdaptor,
recyclerView: RecyclerView, recyclerView: RecyclerView,
progress: View, progress: View,
title: View , title: View,
more: View , more: View,
string: String, string: String,
media : MutableList<Media> media: MutableList<Media>
) { ) {
progress.visibility = View.GONE progress.visibility = View.GONE
recyclerView.adapter = adaptor recyclerView.adapter = adaptor
@@ -296,8 +305,8 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
fun updateNotificationCount() { fun updateNotificationCount() {
if (this::binding.isInitialized) { if (this::binding.isInitialized) {
trendingBinding.notificationCount.visibility = trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE && PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString() trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
} }
} }

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

@@ -38,7 +38,7 @@ class CircleView(context: Context, attrs: AttributeSet?) : View(context, attrs)
fun setColor(int: Int) { fun setColor(int: Int) {
paint.color = if (int < booleanList.size && booleanList[int]) { paint.color = if (int < booleanList.size && booleanList[int]) {
Color.GRAY Color.GRAY
} else { } else {
if (isUser) secondColor else primaryColor if (isUser) secondColor else primaryColor
} }
@@ -58,7 +58,7 @@ class CircleView(context: Context, attrs: AttributeSet?) : View(context, attrs)
} else { } else {
val effectiveAngle = totalAngle / parts val effectiveAngle = totalAngle / parts
for (i in 0 until parts) { for (i in 0 until parts) {
val startAngle = i * (effectiveAngle + gapAngle) -90f val startAngle = i * (effectiveAngle + gapAngle) - 90f
path.reset() path.reset()
path.addArc( path.addArc(
centerX - radius, centerX - radius,
@@ -74,7 +74,7 @@ class CircleView(context: Context, attrs: AttributeSet?) : View(context, attrs)
} }
fun setParts(parts: Int, list : List<Boolean> = mutableListOf(), isUser: Boolean) { fun setParts(parts: Int, list: List<Boolean> = mutableListOf(), isUser: Boolean) {
this.parts = parts this.parts = parts
this.booleanList = list this.booleanList = list
this.isUser = isUser this.isUser = isUser

View File

@@ -9,13 +9,14 @@ import androidx.core.view.updateLayoutParams
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.anilist.api.Activity import ani.dantotsu.connections.anilist.api.Activity
import ani.dantotsu.databinding.ActivityStatusBinding import ani.dantotsu.databinding.ActivityStatusBinding
import ani.dantotsu.initActivity
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.home.status.listener.StoriesCallback import ani.dantotsu.home.status.listener.StoriesCallback
import ani.dantotsu.initActivity
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.profile.User import ani.dantotsu.profile.User
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.Logger
class StatusActivity : AppCompatActivity(), StoriesCallback { class StatusActivity : AppCompatActivity(), StoriesCallback {
private lateinit var activity: ArrayList<User> private lateinit var activity: ArrayList<User>
@@ -44,12 +45,20 @@ class StatusActivity : AppCompatActivity(), StoriesCallback {
val key = "activities" val key = "activities"
val watchedActivity = PrefManager.getCustomVal<Set<Int>>(key, setOf()) val watchedActivity = PrefManager.getCustomVal<Set<Int>>(key, setOf())
val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity ) if (activity.getOrNull(position) != null) {
val startIndex = if ( startFrom > 0) startFrom else 0 val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity)
binding.stories.setStoriesList(activity[position].activity, this, startIndex + 1) 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 { private fun findFirstNonMatch(watchedActivity: Set<Int>, activity: List<Activity>): Int {
for (activityItem in activity) { for (activityItem in activity) {
if (activityItem.id !in watchedActivity) { if (activityItem.id !in watchedActivity) {
@@ -58,13 +67,16 @@ class StatusActivity : AppCompatActivity(), StoriesCallback {
} }
return -1 return -1
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
binding.stories.pause() binding.stories.pause()
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
binding.stories.resume() if (hasWindowFocus())
binding.stories.resume()
} }
override fun onWindowFocusChanged(hasFocus: Boolean) { override fun onWindowFocusChanged(hasFocus: Boolean) {
@@ -75,15 +87,16 @@ class StatusActivity : AppCompatActivity(), StoriesCallback {
binding.stories.pause() binding.stories.pause()
} }
} }
override fun onStoriesEnd() { override fun onStoriesEnd() {
position += 1 position += 1
if (position < activity.size) { if (position < activity.size) {
val key = "activities" val key = "activities"
val watchedActivity = PrefManager.getCustomVal<Set<Int>>(key, setOf()) val watchedActivity = PrefManager.getCustomVal<Set<Int>>(key, setOf())
val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity ) val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity)
val startIndex= if ( startFrom > 0) startFrom else 0 val startIndex = if (startFrom > 0) startFrom else 0
binding.stories.startAnimation(slideOutLeft) binding.stories.startAnimation(slideOutLeft)
binding.stories.setStoriesList(activity[position].activity, this, startIndex + 1) binding.stories.setStoriesList(activity[position].activity, startIndex + 1)
binding.stories.startAnimation(slideInRight) binding.stories.startAnimation(slideInRight)
} else { } else {
finish() finish()
@@ -92,18 +105,19 @@ class StatusActivity : AppCompatActivity(), StoriesCallback {
override fun onStoriesStart() { override fun onStoriesStart() {
position -= 1 position -= 1
if (position >= 0) { if (position >= 0 && activity[position].activity.isNotEmpty()) {
val key = "activities" val key = "activities"
val watchedActivity = PrefManager.getCustomVal<Set<Int>>(key, setOf()) val watchedActivity = PrefManager.getCustomVal<Set<Int>>(key, setOf())
val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity ) val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity)
val startIndex = if ( startFrom > 0) startFrom else 0 val startIndex = if (startFrom > 0) startFrom else 0
binding.stories.startAnimation(slideOutRight) binding.stories.startAnimation(slideOutRight)
binding.stories.setStoriesList(activity[position].activity, this, startIndex + 1) binding.stories.setStoriesList(activity[position].activity, startIndex + 1)
binding.stories.startAnimation(slideInLeft) binding.stories.startAnimation(slideInLeft)
} else { } else {
finish() finish()
} }
} }
companion object { companion object {
var user: ArrayList<User> = arrayListOf() var user: ArrayList<User> = arrayListOf()
} }

View File

@@ -30,6 +30,7 @@ import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.User import ani.dantotsu.profile.User
import ani.dantotsu.profile.UsersDialogFragment import ani.dantotsu.profile.UsersDialogFragment
import ani.dantotsu.profile.activity.ActivityItemBuilder import ani.dantotsu.profile.activity.ActivityItemBuilder
import ani.dantotsu.profile.activity.RepliesBottomDialog
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
@@ -48,7 +49,6 @@ import kotlin.math.abs
class Stories @JvmOverloads constructor( class Stories @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr), View.OnTouchListener { ) : ConstraintLayout(context, attrs, defStyleAttr), View.OnTouchListener {
private lateinit var activity: FragmentActivity
private lateinit var binding: FragmentStatusBinding private lateinit var binding: FragmentStatusBinding
private lateinit var activityList: List<Activity> private lateinit var activityList: List<Activity>
private lateinit var storiesListener: StoriesCallback private lateinit var storiesListener: StoriesCallback
@@ -74,16 +74,14 @@ class Stories @JvmOverloads constructor(
if (context is StoriesCallback) storiesListener = context as StoriesCallback if (context is StoriesCallback) storiesListener = context as StoriesCallback
binding.leftTouchPanel.setOnTouchListener(this) binding.touchPanel.setOnTouchListener(this)
binding.rightTouchPanel.setOnTouchListener(this)
} }
fun setStoriesList( fun setStoriesList(
activityList: List<Activity>, activity: FragmentActivity, startIndex: Int = 1 activityList: List<Activity>, startIndex: Int = 1
) { ) {
this.activityList = activityList this.activityList = activityList
this.activity = activity
this.storyIndex = startIndex this.storyIndex = startIndex
addLoadingViews(activityList) addLoadingViews(activityList)
} }
@@ -264,50 +262,6 @@ class Stories @JvmOverloads constructor(
} }
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 {
val maxClickDuration = 200
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 (clickDuration < maxClickDuration && !isLongPress) {
when (view?.id) {
R.id.leftTouchPanel -> leftPanelTouch()
R.id.rightTouchPanel -> rightPanelTouch()
}
} else {
resume()
}
val deltaX = event.x - startX
if (abs(deltaX) > swipeThreshold) {
if (deltaX > 0) onStoriesPrevious()
else onStoriesCompleted()
}
}
}
return true
}
private fun rightPanelTouch() { private fun rightPanelTouch() {
Logger.log("rightPanelTouch: $storyIndex") Logger.log("rightPanelTouch: $storyIndex")
if (storyIndex == activityList.size) { if (storyIndex == activityList.size) {
@@ -359,6 +313,7 @@ class Stories @JvmOverloads constructor(
timer.resume() timer.resume()
} }
@SuppressLint("ClickableViewAccessibility")
private fun loadStory(story: Activity) { private fun loadStory(story: Activity) {
val key = "activities" val key = "activities"
val set = PrefManager.getCustomVal<Set<Int>>(key, setOf()).plus((story.id)) val set = PrefManager.getCustomVal<Set<Int>>(key, setOf()).plus((story.id))
@@ -374,6 +329,15 @@ class Stories @JvmOverloads constructor(
null 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) { fun visible(isList: Boolean) {
binding.textActivity.isVisible = !isList binding.textActivity.isVisible = !isList
binding.textActivityContainer.isVisible = !isList binding.textActivityContainer.isVisible = !isList
@@ -397,15 +361,17 @@ class Stories @JvmOverloads constructor(
} }
} }
} ${story.progress ?: story.media?.title?.userPreferred} " + } ${story.progress ?: story.media?.title?.userPreferred} " +
if ( if (
story.status?.contains("completed") == false && story.status?.contains("completed") == false &&
!story.status.contains("plans") && !story.status.contains("plans") &&
!story.status.contains("repeating") !story.status.contains("repeating") &&
) { !story.status.contains("paused") &&
"of ${story.media?.title?.userPreferred}" !story.status.contains("dropped")
} else { ) {
"" "of ${story.media?.title?.userPreferred}"
} } else {
""
}
binding.infoText.text = text binding.infoText.text = text
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations) val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
blurImage( blurImage(
@@ -421,7 +387,7 @@ class Stories @JvmOverloads constructor(
story.media?.id story.media?.id
), ),
ActivityOptionsCompat.makeSceneTransitionAnimation( ActivityOptionsCompat.makeSceneTransitionAnimation(
activity, (it.context as FragmentActivity),
binding.coverImage, binding.coverImage,
ViewCompat.getTransitionName(binding.coverImage)!! ViewCompat.getTransitionName(binding.coverImage)!!
).toBundle() ).toBundle()
@@ -455,22 +421,21 @@ class Stories @JvmOverloads constructor(
} }
val likeColor = ContextCompat.getColor(context, R.color.yt_red) val likeColor = ContextCompat.getColor(context, R.color.yt_red)
val notLikeColor = ContextCompat.getColor(context, R.color.bg_opp) 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 { binding.activityRepliesContainer.setOnClickListener {
RepliesBottomDialog.newInstance(story.id) RepliesBottomDialog.newInstance(story.id)
.show(activity.supportFragmentManager, "replies") .show((it.context as FragmentActivity).supportFragmentManager, "replies")
} }
binding.activityLike.setColorFilter(if (story.isLiked == true) likeColor else notLikeColor) binding.activityLike.setColorFilter(if (story.isLiked == true) likeColor else notLikeColor)
binding.replyCount.text = story.replyCount.toString()
binding.activityLikeCount.text = story.likeCount.toString() binding.activityLikeCount.text = story.likeCount.toString()
binding.activityReplies.setColorFilter(ContextCompat.getColor(context, R.color.bg_opp))
binding.activityLikeContainer.setOnClickListener { binding.activityLikeContainer.setOnClickListener {
like() like()
} }
binding.activityLikeContainer.setOnLongClickListener { binding.activityLikeContainer.setOnLongClickListener {
val context = activity
UsersDialogFragment().apply { UsersDialogFragment().apply {
userList(userList) userList(userList)
show(context.supportFragmentManager, "dialog") show((it.context as FragmentActivity).supportFragmentManager, "dialog")
} }
true true
} }
@@ -484,7 +449,7 @@ class Stories @JvmOverloads constructor(
val notLikeColor = ContextCompat.getColor(context, R.color.bg_opp) val notLikeColor = ContextCompat.getColor(context, R.color.bg_opp)
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
scope.launch { scope.launch {
val res = Anilist.query.toggleLike(story.id, "ACTIVITY") val res = Anilist.mutation.toggleLike(story.id, "ACTIVITY")
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (res != null) { if (res != null) {
if (story.isLiked == true) { if (story.isLiked == true) {
@@ -502,4 +467,73 @@ class Stories @JvmOverloads constructor(
} }
} }
} }
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

@@ -1,6 +1,5 @@
package ani.dantotsu.home.status package ani.dantotsu.home.status
import android.content.Context
import android.content.Intent import android.content.Intent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
@@ -15,6 +14,8 @@ import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.User import ani.dantotsu.profile.User
import ani.dantotsu.setAnimation import ani.dantotsu.setAnimation
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.snackString
import ani.dantotsu.util.ActivityMarkdownCreator
class UserStatusAdapter(private val user: ArrayList<User>) : class UserStatusAdapter(private val user: ArrayList<User>) :
RecyclerView.Adapter<UserStatusAdapter.UsersViewHolder>() { RecyclerView.Adapter<UserStatusAdapter.UsersViewHolder>() {
@@ -23,6 +24,10 @@ class UserStatusAdapter(private val user: ArrayList<User>) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
init { init {
itemView.setOnClickListener { itemView.setOnClickListener {
if (user[bindingAdapterPosition].activity.isEmpty()) {
snackString("No activity")
return@setOnClickListener
}
StatusActivity.user = user StatusActivity.user = user
ContextCompat.startActivity( ContextCompat.startActivity(
itemView.context, itemView.context,
@@ -34,14 +39,23 @@ class UserStatusAdapter(private val user: ArrayList<User>) :
) )
} }
itemView.setOnLongClickListener { itemView.setOnLongClickListener {
ContextCompat.startActivity( if (user[bindingAdapterPosition].id == Anilist.userid) {
itemView.context, ContextCompat.startActivity(
Intent(
itemView.context, itemView.context,
ProfileActivity::class.java Intent(itemView.context, ActivityMarkdownCreator::class.java)
).putExtra("userId", user[bindingAdapterPosition].id), .putExtra("type", "activity"),
null null
) )
} else {
ContextCompat.startActivity(
itemView.context,
Intent(
itemView.context,
ProfileActivity::class.java
).putExtra("userId", user[bindingAdapterPosition].id),
null
)
}
true true
} }
} }
@@ -62,10 +76,15 @@ class UserStatusAdapter(private val user: ArrayList<User>) :
setAnimation(b.root.context, b.root) setAnimation(b.root.context, b.root)
val user = user[position] val user = user[position]
b.profileUserAvatar.loadImage(user.pfp) b.profileUserAvatar.loadImage(user.pfp)
b.profileUserName.text = if (Anilist.userid == user.id) getAppString(R.string.your_story) else user.name 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 watchedActivity = PrefManager.getCustomVal<Set<Int>>("activities", setOf())
val booleanList = user.activity.map { watchedActivity.contains(it.id) } val booleanList = user.activity.map { watchedActivity.contains(it.id) }
b.profileUserStatusIndicator.setParts(user.activity.size, booleanList, user.id == Anilist.userid) b.profileUserStatusIndicator.setParts(
user.activity.size,
booleanList,
user.id == Anilist.userid
)
} }

View File

@@ -7,6 +7,12 @@ data class Author(
var name: String?, var name: String?,
var image: String?, var image: String?,
var role: String?, var role: String?,
var age: Int? = null,
var yearsActive: List<Int>? = null,
var dateOfBirth: String? = null,
var dateOfDeath: String? = null,
var homeTown: String? = null,
var yearMedia: MutableMap<String, ArrayList<Media>>? = null, var yearMedia: MutableMap<String, ArrayList<Media>>? = null,
var character: ArrayList<Character>? = null var character: ArrayList<Character>? = null,
var isFav: Boolean = false
) : Serializable ) : Serializable

View File

@@ -1,11 +1,13 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils.clamp
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@@ -16,57 +18,127 @@ import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.EmptyAdapter import ani.dantotsu.EmptyAdapter
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.Refresh import ani.dantotsu.Refresh
import ani.dantotsu.databinding.ActivityAuthorBinding import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistMutations
import ani.dantotsu.databinding.ActivityCharacterBinding
import ani.dantotsu.initActivity import ani.dantotsu.initActivity
import ani.dantotsu.loadImage
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.others.SpoilerPlugin
import ani.dantotsu.others.getSerialized import ani.dantotsu.others.getSerialized
import ani.dantotsu.px import ani.dantotsu.px
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import com.google.android.material.appbar.AppBarLayout
import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlin.math.abs
class AuthorActivity : AppCompatActivity() { class AuthorActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
private lateinit var binding: ActivityAuthorBinding private lateinit var binding: ActivityCharacterBinding
private val scope = lifecycleScope private val scope = lifecycleScope
private val model: OtherDetailsViewModel by viewModels() private val model: OtherDetailsViewModel by viewModels()
private var author: Author? = null private lateinit var author: Author
private var loaded = false private var loaded = false
private var screenWidth: Float = 0f
private val percent = 30
private var mMaxScrollSize = 0
private var isCollapsed = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
binding = ActivityAuthorBinding.inflate(layoutInflater) binding = ActivityCharacterBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
initActivity(this) initActivity(this)
this.window.statusBarColor = ContextCompat.getColor(this, R.color.nav_bg) screenWidth = resources.displayMetrics.run { widthPixels / density }
if (PrefManager.getVal(PrefName.ImmersiveMode)) this.window.statusBarColor =
ContextCompat.getColor(this, R.color.transparent)
val screenWidth = resources.displayMetrics.run { widthPixels / density } val banner =
if (PrefManager.getVal(PrefName.BannerAnimations)) binding.characterBanner else binding.characterBannerNoKen
binding.root.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight } banner.updateLayoutParams { height += statusBarHeight }
binding.studioRecycler.updatePadding(bottom = 64f.px + navBarHeight) binding.characterClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.studioTitle.isSelected = true binding.characterCollapsing.minimumHeight = statusBarHeight
binding.characterCover.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
binding.characterRecyclerView.updatePadding(bottom = 64f.px + navBarHeight)
binding.characterTitle.isSelected = true
binding.characterAppBar.addOnOffsetChangedListener(this)
author = intent.getSerialized("author") binding.characterClose.setOnClickListener {
binding.studioTitle.text = author?.name
binding.studioClose.setOnClickListener {
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
} }
author = intent.getSerialized("author") ?: return
binding.characterTitle.text = author.name
binding.characterCoverImage.loadImage(author.image)
binding.characterCoverImage.setOnLongClickListener {
ImageViewDialog.newInstance(
this,
author.name,
author.image
)
}
val link = "https://anilist.co/staff/${author.id}"
binding.characterShare.setOnClickListener {
val i = Intent(Intent.ACTION_SEND)
i.type = "text/plain"
i.putExtra(Intent.EXTRA_TEXT, link)
startActivity(Intent.createChooser(i, author.name))
}
binding.characterShare.setOnLongClickListener {
openLinkInBrowser(link)
true
}
lifecycleScope.launch {
withContext(Dispatchers.IO) {
author.isFav =
Anilist.query.isUserFav(AnilistMutations.FavType.STAFF, author.id)
}
withContext(Dispatchers.Main) {
binding.characterFav.setImageResource(
if (author.isFav) R.drawable.ic_round_favorite_24 else R.drawable.ic_round_favorite_border_24
)
}
}
binding.characterFav.setOnClickListener {
scope.launch {
lifecycleScope.launch {
if (Anilist.mutation.toggleFav(AnilistMutations.FavType.CHARACTER, author.id)) {
author.isFav = !author.isFav
binding.characterFav.setImageResource(
if (author.isFav) R.drawable.ic_round_favorite_24 else R.drawable.ic_round_favorite_border_24
)
} else {
snackString("Failed to toggle favorite")
}
}
}
}
model.getAuthor().observe(this) { model.getAuthor().observe(this) {
if (it != null) { if (it != null) {
author = it author = it
loaded = true loaded = true
binding.studioProgressBar.visibility = View.GONE binding.characterProgress.visibility = View.GONE
binding.studioRecycler.visibility = View.VISIBLE binding.characterRecyclerView.visibility = View.VISIBLE
if (author!!.yearMedia.isNullOrEmpty()) { if (author.yearMedia.isNullOrEmpty()) {
binding.studioRecycler.visibility = View.GONE binding.characterRecyclerView.visibility = View.GONE
} }
val titlePosition = arrayListOf<Int>() val titlePosition = arrayListOf<Int>()
val concatAdapter = ConcatAdapter() val concatAdapter = ConcatAdapter()
val map = author!!.yearMedia ?: return@observe val map = author.yearMedia ?: return@observe
val keys = map.keys.toTypedArray() val keys = map.keys.toTypedArray()
var pos = 0 var pos = 0
@@ -80,6 +152,10 @@ class AuthorActivity : AppCompatActivity() {
} }
} }
} }
val desc = createDesc(author)
val markWon = Markwon.builder(this).usePlugin(SoftBreakAddsNewLinePlugin.create())
.usePlugin(SpoilerPlugin()).build()
markWon.setMarkdown(binding.authorCharacterDesc, desc)
for (i in keys.indices) { for (i in keys.indices) {
val medias = map[keys[i]]!! val medias = map[keys[i]]!!
val empty = if (medias.size >= 4) medias.size % 4 else 4 - medias.size val empty = if (medias.size >= 4) medias.size % 4 else 4 - medias.size
@@ -90,18 +166,18 @@ class AuthorActivity : AppCompatActivity() {
concatAdapter.addAdapter(MediaAdaptor(0, medias, this, true)) concatAdapter.addAdapter(MediaAdaptor(0, medias, this, true))
concatAdapter.addAdapter(EmptyAdapter(empty)) concatAdapter.addAdapter(EmptyAdapter(empty))
} }
binding.studioRecycler.adapter = concatAdapter binding.characterRecyclerView.adapter = concatAdapter
binding.studioRecycler.layoutManager = gridLayoutManager binding.characterRecyclerView.layoutManager = gridLayoutManager
binding.charactersRecycler.visibility = View.VISIBLE binding.authorCharactersRecycler.visibility = View.VISIBLE
binding.charactersText.visibility = View.VISIBLE binding.AuthorCharactersText.visibility = View.VISIBLE
binding.charactersRecycler.adapter = binding.authorCharactersRecycler.adapter =
CharacterAdapter(author!!.character ?: arrayListOf()) CharacterAdapter(author.character ?: arrayListOf())
binding.charactersRecycler.layoutManager = binding.authorCharactersRecycler.layoutManager =
LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
if (author!!.character.isNullOrEmpty()) { if (author.character.isNullOrEmpty()) {
binding.charactersRecycler.visibility = View.GONE binding.authorCharactersRecycler.visibility = View.GONE
binding.charactersText.visibility = View.GONE binding.AuthorCharactersText.visibility = View.GONE
} }
} }
} }
@@ -109,14 +185,28 @@ class AuthorActivity : AppCompatActivity() {
live.observe(this) { live.observe(this) {
if (it) { if (it) {
scope.launch { scope.launch {
if (author != null) withContext(Dispatchers.IO) { model.loadAuthor(author) }
withContext(Dispatchers.IO) { model.loadAuthor(author!!) }
live.postValue(false) live.postValue(false)
} }
} }
} }
} }
private fun createDesc(author: Author): String {
val age = if (author.age != null) "${getString(R.string.age)} ${author.age}" else ""
val yearsActive =
if (author.yearsActive != null) "${getString(R.string.years_active)} ${author.yearsActive}" else ""
val dob =
if (author.dateOfBirth != null) "${getString(R.string.birthday)} ${author.dateOfBirth}" else ""
val homeTown =
if (author.homeTown != null) "${getString(R.string.hometown)} ${author.homeTown}" else ""
val dod =
if (author.dateOfDeath != null) "${getString(R.string.date_of_death)} ${author.dateOfDeath}" else ""
return "$age $yearsActive $dob $homeTown $dod"
}
override fun onDestroy() { override fun onDestroy() {
if (Refresh.activity.containsKey(this.hashCode())) { if (Refresh.activity.containsKey(this.hashCode())) {
Refresh.activity.remove(this.hashCode()) Refresh.activity.remove(this.hashCode())
@@ -125,7 +215,31 @@ class AuthorActivity : AppCompatActivity() {
} }
override fun onResume() { override fun onResume() {
binding.studioProgressBar.visibility = if (!loaded) View.VISIBLE else View.GONE binding.characterProgress.visibility = if (!loaded) View.VISIBLE else View.GONE
super.onResume() super.onResume()
} }
override fun onOffsetChanged(appBar: AppBarLayout, i: Int) {
if (mMaxScrollSize == 0) mMaxScrollSize = appBar.totalScrollRange
val percentage = abs(i) * 100 / mMaxScrollSize
val cap = clamp((percent - percentage) / percent.toFloat(), 0f, 1f)
binding.characterCover.scaleX = 1f * cap
binding.characterCover.scaleY = 1f * cap
binding.characterCover.cardElevation = 32f * cap
binding.characterCover.visibility =
if (binding.characterCover.scaleX == 0f) View.GONE else View.VISIBLE
val immersiveMode: Boolean = PrefManager.getVal(PrefName.ImmersiveMode)
if (percentage >= percent && !isCollapsed) {
isCollapsed = true
if (immersiveMode) this.window.statusBarColor =
ContextCompat.getColor(this, R.color.nav_bg)
}
if (percentage <= percent && isCollapsed) {
isCollapsed = false
if (immersiveMode) this.window.statusBarColor =
ContextCompat.getColor(this, R.color.transparent)
}
}
} }

View File

@@ -15,7 +15,7 @@ import ani.dantotsu.setAnimation
import java.io.Serializable import java.io.Serializable
class AuthorAdapter( class AuthorAdapter(
private val authorList: ArrayList<Author>, private val authorList: MutableList<Author>,
) : RecyclerView.Adapter<AuthorAdapter.AuthorViewHolder>() { ) : RecyclerView.Adapter<AuthorAdapter.AuthorViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AuthorViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AuthorViewHolder {
val binding = val binding =
@@ -26,7 +26,7 @@ class AuthorAdapter(
override fun onBindViewHolder(holder: AuthorViewHolder, position: Int) { override fun onBindViewHolder(holder: AuthorViewHolder, position: Int) {
val binding = holder.binding val binding = holder.binding
setAnimation(binding.root.context, holder.binding.root) setAnimation(binding.root.context, holder.binding.root)
val author = authorList[position] val author = authorList.getOrNull(position) ?: return
binding.itemCompactRelation.text = author.role binding.itemCompactRelation.text = author.role
binding.itemCompactImage.loadImage(author.image) binding.itemCompactImage.loadImage(author.image)
binding.itemCompactTitle.text = author.name binding.itemCompactTitle.text = author.name

View File

@@ -30,6 +30,7 @@ class CalendarActivity : AppCompatActivity() {
private lateinit var binding: ActivityListBinding private lateinit var binding: ActivityListBinding
private val scope = lifecycleScope private val scope = lifecycleScope
private var selectedTabIdx = 1 private var selectedTabIdx = 1
private var showOnlyLibrary = false
private val model: OtherDetailsViewModel by viewModels() private val model: OtherDetailsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -38,8 +39,6 @@ class CalendarActivity : AppCompatActivity() {
ThemeManager(this).applyTheme() ThemeManager(this).applyTheme()
binding = ActivityListBinding.inflate(layoutInflater) binding = ActivityListBinding.inflate(layoutInflater)
val primaryColor = getThemeColor(com.google.android.material.R.attr.colorSurface) val primaryColor = getThemeColor(com.google.android.material.R.attr.colorSurface)
val primaryTextColor = getThemeColor(com.google.android.material.R.attr.colorPrimary) val primaryTextColor = getThemeColor(com.google.android.material.R.attr.colorPrimary)
val secondaryTextColor = getThemeColor(com.google.android.material.R.attr.colorOutline) val secondaryTextColor = getThemeColor(com.google.android.material.R.attr.colorOutline)
@@ -79,6 +78,17 @@ class CalendarActivity : AppCompatActivity() {
override fun onTabReselected(tab: TabLayout.Tab?) {} override fun onTabReselected(tab: TabLayout.Tab?) {}
}) })
binding.listed.setOnClickListener {
showOnlyLibrary = !showOnlyLibrary
binding.listed.setImageResource(
if (showOnlyLibrary) R.drawable.ic_round_collections_bookmark_24
else R.drawable.ic_round_library_books_24
)
scope.launch {
model.loadCalendar(showOnlyLibrary)
}
}
model.getCalendar().observe(this) { model.getCalendar().observe(this) {
if (it != null) { if (it != null) {
binding.listProgressBar.visibility = View.GONE binding.listProgressBar.visibility = View.GONE
@@ -97,11 +107,10 @@ class CalendarActivity : AppCompatActivity() {
live.observe(this) { live.observe(this) {
if (it) { if (it) {
scope.launch { scope.launch {
withContext(Dispatchers.IO) { model.loadCalendar() } withContext(Dispatchers.IO) { model.loadCalendar(showOnlyLibrary) }
live.postValue(false) live.postValue(false)
} }
} }
} }
} }
} }

View File

@@ -9,13 +9,14 @@ import androidx.core.content.ContextCompat
import androidx.core.util.Pair import androidx.core.util.Pair
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.copyToClipboard
import ani.dantotsu.databinding.ItemCharacterBinding import ani.dantotsu.databinding.ItemCharacterBinding
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
import ani.dantotsu.setAnimation import ani.dantotsu.setAnimation
import java.io.Serializable import java.io.Serializable
class CharacterAdapter( class CharacterAdapter(
private val characterList: ArrayList<Character> private val characterList: MutableList<Character>
) : RecyclerView.Adapter<CharacterAdapter.CharacterViewHolder>() { ) : RecyclerView.Adapter<CharacterAdapter.CharacterViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CharacterViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CharacterViewHolder {
val binding = val binding =
@@ -26,9 +27,8 @@ class CharacterAdapter(
override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) { override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) {
val binding = holder.binding val binding = holder.binding
setAnimation(binding.root.context, holder.binding.root) setAnimation(binding.root.context, holder.binding.root)
val character = characterList[position] val character = characterList.getOrNull(position) ?: return
val whitespace = "${character.role} " val whitespace = "${if (character.role.lowercase() == "null") "" else character.role} "
character.voiceActor
binding.itemCompactRelation.text = whitespace binding.itemCompactRelation.text = whitespace
binding.itemCompactImage.loadImage(character.image) binding.itemCompactImage.loadImage(character.image)
binding.itemCompactTitle.text = character.name binding.itemCompactTitle.text = character.name
@@ -55,6 +55,11 @@ class CharacterAdapter(
).toBundle() ).toBundle()
) )
} }
itemView.setOnLongClickListener {
copyToClipboard(
characterList[bindingAdapterPosition].name ?: ""
); true
}
} }
} }
} }

View File

@@ -9,6 +9,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils.clamp import androidx.core.math.MathUtils.clamp
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@@ -45,6 +46,11 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
private lateinit var character: Character private lateinit var character: Character
private var loaded = false private var loaded = false
private var isCollapsed = false
private val percent = 30
private var mMaxScrollSize = 0
private var screenWidth: Float = 0f
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -71,6 +77,11 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
binding.characterClose.setOnClickListener { binding.characterClose.setOnClickListener {
onBackPressedDispatcher.onBackPressed() onBackPressedDispatcher.onBackPressed()
} }
binding.authorCharactersRecycler.isVisible = false
binding.AuthorCharactersText.isVisible = false
binding.authorCharacterDesc.isVisible = false
character = intent.getSerialized("character") ?: return character = intent.getSerialized("character") ?: return
binding.characterTitle.text = character.name binding.characterTitle.text = character.name
banner.loadImage(character.banner) banner.loadImage(character.banner)
@@ -158,11 +169,6 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
super.onResume() super.onResume()
} }
private var isCollapsed = false
private val percent = 30
private var mMaxScrollSize = 0
private var screenWidth: Float = 0f
override fun onOffsetChanged(appBar: AppBarLayout, i: Int) { override fun onOffsetChanged(appBar: AppBarLayout, i: Int) {
if (mMaxScrollSize == 0) mMaxScrollSize = appBar.totalScrollRange if (mMaxScrollSize == 0) mMaxScrollSize = appBar.totalScrollRange
val percentage = abs(i) * 100 / mMaxScrollSize val percentage = abs(i) * 100 / mMaxScrollSize

View File

@@ -7,11 +7,9 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.buildMarkwon
import ani.dantotsu.currActivity import ani.dantotsu.currActivity
import ani.dantotsu.databinding.ItemCharacterDetailsBinding import ani.dantotsu.databinding.ItemCharacterDetailsBinding
import ani.dantotsu.others.SpoilerPlugin
import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin
class CharacterDetailsAdapter(private val character: Character, private val activity: Activity) : class CharacterDetailsAdapter(private val character: Character, private val activity: Activity) :
RecyclerView.Adapter<CharacterDetailsAdapter.GenreViewHolder>() { RecyclerView.Adapter<CharacterDetailsAdapter.GenreViewHolder>() {
@@ -24,7 +22,9 @@ class CharacterDetailsAdapter(private val character: Character, private val acti
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) { override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
val binding = holder.binding val binding = holder.binding
val desc = val desc =
(if (character.age != "null") "${currActivity()!!.getString(R.string.age)} ${character.age}" else "") + (if (character.id == 4004)
"![za wardo](https://media1.tenor.com/m/_z1tmCJnL2wAAAAd/za-warudo.gif) \n" else "") +
(if (character.age != "null") "${currActivity()!!.getString(R.string.age)} ${character.age}" else "") +
(if (character.dateOfBirth.toString() != "") (if (character.dateOfBirth.toString() != "")
"${currActivity()!!.getString(R.string.birthday)} ${character.dateOfBirth.toString()}" else "") + "${currActivity()!!.getString(R.string.birthday)} ${character.dateOfBirth.toString()}" else "") +
(if (character.gender != "null") (if (character.gender != "null")
@@ -41,8 +41,7 @@ class CharacterDetailsAdapter(private val character: Character, private val acti
} else "") + "\n" + character.description } else "") + "\n" + character.description
binding.characterDesc.isTextSelectable binding.characterDesc.isTextSelectable
val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create()) val markWon = buildMarkwon(activity)
.usePlugin(SpoilerPlugin()).build()
markWon.setMarkdown(binding.characterDesc, desc.replace("~!", "||").replace("!~", "||")) markWon.setMarkdown(binding.characterDesc, desc.replace("~!", "||").replace("!~", "||"))
binding.voiceActorRecycler.adapter = AuthorAdapter(character.voiceActor ?: arrayListOf()) binding.voiceActorRecycler.adapter = AuthorAdapter(character.voiceActor ?: arrayListOf())
binding.voiceActorRecycler.layoutManager = LinearLayoutManager( binding.voiceActorRecycler.layoutManager = LinearLayoutManager(

View File

@@ -0,0 +1,77 @@
package ani.dantotsu.media
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.databinding.ItemSearchHeaderBinding
abstract class HeaderInterface : RecyclerView.Adapter<HeaderInterface.SearchHeaderViewHolder>() {
private val itemViewType = 6969
var search: Runnable? = null
var requestFocus: Runnable? = null
protected var textWatcher: TextWatcher? = null
protected lateinit var searchHistoryAdapter: SearchHistoryAdapter
protected lateinit var binding: ItemSearchHeaderBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder {
val binding =
ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SearchHeaderViewHolder(binding)
}
fun setHistoryVisibility(visible: Boolean) {
if (visible) {
binding.searchResultLayout.startAnimation(fadeOutAnimation())
binding.searchHistoryList.startAnimation(fadeInAnimation())
binding.searchResultLayout.visibility = View.GONE
binding.searchHistoryList.visibility = View.VISIBLE
binding.searchByImage.visibility = View.VISIBLE
} else {
if (binding.searchResultLayout.visibility != View.VISIBLE) {
binding.searchResultLayout.startAnimation(fadeInAnimation())
binding.searchHistoryList.startAnimation(fadeOutAnimation())
}
binding.searchResultLayout.visibility = View.VISIBLE
binding.clearHistory.visibility = View.GONE
binding.searchHistoryList.visibility = View.GONE
binding.searchByImage.visibility = View.GONE
}
}
private fun fadeInAnimation(): Animation {
return AlphaAnimation(0f, 1f).apply {
duration = 150
}
}
protected fun fadeOutAnimation(): Animation {
return AlphaAnimation(1f, 0f).apply {
duration = 150
}
}
protected fun updateClearHistoryVisibility() {
binding.clearHistory.visibility =
if (searchHistoryAdapter.itemCount > 0) View.VISIBLE else View.GONE
}
fun addHistory() {
if (::searchHistoryAdapter.isInitialized && binding.searchBarText.text.toString()
.isNotBlank()
) searchHistoryAdapter.add(binding.searchBarText.text.toString())
}
inner class SearchHeaderViewHolder(val binding: ItemSearchHeaderBinding) :
RecyclerView.ViewHolder(binding.root)
override fun getItemCount(): Int = 1
override fun getItemViewType(position: Int): Int {
return itemViewType
}
}

View File

@@ -1,14 +1,22 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.graphics.Bitmap import android.graphics.Bitmap
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.FuzzyDate import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.anilist.api.MediaEdge import ani.dantotsu.connections.anilist.api.MediaEdge
import ani.dantotsu.connections.anilist.api.MediaList import ani.dantotsu.connections.anilist.api.MediaList
import ani.dantotsu.connections.anilist.api.MediaStreamingEpisode
import ani.dantotsu.connections.anilist.api.MediaType import ani.dantotsu.connections.anilist.api.MediaType
import ani.dantotsu.connections.anilist.api.Query import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.media.anime.Anime import ani.dantotsu.media.anime.Anime
import ani.dantotsu.media.manga.Manga import ani.dantotsu.media.manga.Manga
import ani.dantotsu.profile.User import ani.dantotsu.profile.User
import ani.dantotsu.settings.saving.PrefManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.Serializable import java.io.Serializable
import ani.dantotsu.connections.anilist.api.Media as ApiMedia import ani.dantotsu.connections.anilist.api.Media as ApiMedia
@@ -76,7 +84,7 @@ data class Media(
var nameMAL: String? = null, var nameMAL: String? = null,
var shareLink: String? = null, var shareLink: String? = null,
var selected: Selected? = null, var selected: Selected? = null,
var streamingEpisodes: List<MediaStreamingEpisode>? = null,
var idKitsu: String? = null, var idKitsu: String? = null,
var cameFromContinue: Boolean = false var cameFromContinue: Boolean = false
@@ -129,6 +137,37 @@ data class Media(
fun mangaName() = if (countryOfOrigin != "JP") mainName() else nameRomaji fun mangaName() = if (countryOfOrigin != "JP") mainName() else nameRomaji
} }
fun Media?.deleteFromList(
scope: CoroutineScope,
onSuccess: suspend () -> Unit,
onError: suspend (e: Exception) -> Unit,
onNotFound: suspend () -> Unit
) {
val id = this?.userListId
scope.launch {
withContext(Dispatchers.IO) {
this@deleteFromList?.let { media ->
val _id = id ?: Anilist.query.userMediaDetails(media).userListId
_id?.let { listId ->
try {
Anilist.mutation.deleteList(listId)
MAL.query.deleteList(media.anime != null, media.idMAL)
val removeList = PrefManager.getCustomVal("removeList", setOf<Int>())
PrefManager.setCustomVal(
"removeList", removeList.minus(listId)
)
onSuccess()
} catch (e: Exception) {
onError(e)
}
} ?: onNotFound()
}
}
}
}
fun emptyMedia() = Media( fun emptyMedia() = Media(
id = 0, id = 0,
name = "No media found", name = "No media found",

View File

@@ -4,6 +4,7 @@ import android.animation.ObjectAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.view.GestureDetector import android.view.GestureDetector
@@ -12,6 +13,8 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.ImageView import android.widget.ImageView
import androidx.activity.SystemBarStyle
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
@@ -19,8 +22,10 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.text.bold import androidx.core.text.bold
import androidx.core.text.color import androidx.core.text.color
import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.marginBottom import androidx.core.view.marginBottom
import androidx.core.view.setPadding
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updateMargins import androidx.core.view.updateMargins
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@@ -79,6 +84,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
var media: Media = intent.getSerialized("media") ?: mediaSingleton ?: emptyMedia() var media: Media = intent.getSerialized("media") ?: mediaSingleton ?: emptyMedia()
val id = intent.getIntExtra("mediaId", -1) val id = intent.getIntExtra("mediaId", -1)
@@ -109,6 +115,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
// Ui init // Ui init
initActivity(this) initActivity(this)
binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> { binding.mediaViewPager.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = navBarHeight bottomMargin = navBarHeight
} }
@@ -132,10 +139,12 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
val navBarBottomMargin = if (resources.configuration.orientation == val navBarBottomMargin = if (resources.configuration.orientation ==
Configuration.ORIENTATION_LANDSCAPE Configuration.ORIENTATION_LANDSCAPE
) 0 else navBarHeight ) 0 else navBarHeight
navBar.updateLayoutParams<ViewGroup.MarginLayoutParams> { navBar.setPadding(
rightMargin = navBarRightMargin navBar.paddingLeft,
bottomMargin = navBarBottomMargin navBar.paddingTop,
} navBar.paddingRight + navBarRightMargin,
navBar.paddingBottom + navBarBottomMargin
)
binding.mediaBanner.updateLayoutParams { height += statusBarHeight } binding.mediaBanner.updateLayoutParams { height += statusBarHeight }
binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight } binding.mediaBannerNoKen.updateLayoutParams { height += statusBarHeight }
binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight } binding.mediaClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
@@ -251,10 +260,12 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
fun total() { fun total() {
val text = SpannableStringBuilder().apply { val text = SpannableStringBuilder().apply {
val white = this@MediaDetailsActivity.getThemeColor(com.google.android.material.R.attr.colorOnBackground) val white =
this@MediaDetailsActivity.getThemeColor(com.google.android.material.R.attr.colorOnBackground)
if (media.userStatus != null) { if (media.userStatus != null) {
append(if (media.anime != null) getString(R.string.watched_num) else getString(R.string.read_num)) append(if (media.anime != null) getString(R.string.watched_num) else getString(R.string.read_num))
val colorSecondary = getThemeColor(com.google.android.material.R.attr.colorSecondary) val colorSecondary =
getThemeColor(com.google.android.material.R.attr.colorSecondary)
bold { color(colorSecondary) { append("${media.userProgress}") } } bold { color(colorSecondary) { append("${media.userProgress}") } }
append( append(
if (media.anime != null) getString(R.string.episodes_out_of) else getString( if (media.anime != null) getString(R.string.episodes_out_of) else getString(
@@ -293,7 +304,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
binding.mediaTotal.visibility = View.VISIBLE binding.mediaTotal.visibility = View.VISIBLE
binding.mediaAddToList.text = userStatus binding.mediaAddToList.text = userStatus
} else { } else {
binding.mediaAddToList.setText(R.string.add) binding.mediaAddToList.setText(R.string.add_list)
} }
total() total()
binding.mediaAddToList.setOnClickListener { binding.mediaAddToList.setOnClickListener {
@@ -372,7 +383,9 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
navBar.createTab(R.drawable.ic_round_comment_24, R.string.comments, R.id.comment) navBar.createTab(R.drawable.ic_round_comment_24, R.string.comments, R.id.comment)
navBar.addTab(infoTab) navBar.addTab(infoTab)
navBar.addTab(watchTab) navBar.addTab(watchTab)
navBar.addTab(commentTab) if (PrefManager.getVal<Int>(PrefName.CommentsEnabled) == 1) {
navBar.addTab(commentTab)
}
if (model.continueMedia == null && media.cameFromContinue) { if (model.continueMedia == null && media.cameFromContinue) {
model.continueMedia = PrefManager.getVal(PrefName.ContinueMedia) model.continueMedia = PrefManager.getVal(PrefName.ContinueMedia)
selected = 1 selected = 1
@@ -424,7 +437,8 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
} }
override fun onResume() { override fun onResume() {
navBar.selectTabAt(selected) if (::navBar.isInitialized)
navBar.selectTabAt(selected)
super.onResume() super.onResume()
} }

View File

@@ -13,6 +13,7 @@ import ani.dantotsu.media.anime.Episode
import ani.dantotsu.media.anime.SelectorDialogFragment import ani.dantotsu.media.anime.SelectorDialogFragment
import ani.dantotsu.media.manga.MangaChapter import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.others.AniSkip import ani.dantotsu.others.AniSkip
import ani.dantotsu.others.Anify
import ani.dantotsu.others.Jikan import ani.dantotsu.others.Jikan
import ani.dantotsu.others.Kitsu import ani.dantotsu.others.Kitsu
import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.AnimeSources
@@ -100,6 +101,16 @@ class MediaDetailsViewModel : ViewModel() {
} }
} }
private val anifyEpisodes: MutableLiveData<Map<String, Episode>> =
MutableLiveData<Map<String, Episode>>(null)
fun getAnifyEpisodes(): LiveData<Map<String, Episode>> = anifyEpisodes
suspend fun loadAnifyEpisodes(s: Int) {
tryWithSuspend {
if (anifyEpisodes.value == null) anifyEpisodes.postValue(Anify.fetchAndParseMetadata(s))
}
}
private val fillerEpisodes: MutableLiveData<Map<String, Episode>> = private val fillerEpisodes: MutableLiveData<Map<String, Episode>> =
MutableLiveData<Map<String, Episode>>(null) MutableLiveData<Map<String, Episode>>(null)

View File

@@ -105,8 +105,8 @@ class MediaInfoFragment : Fragment() {
} }
if (media.name != null) binding.mediaInfoNameRomajiContainer.visibility = if (media.name != null) binding.mediaInfoNameRomajiContainer.visibility =
View.VISIBLE View.VISIBLE
val infoNameRomanji = tripleTab + media.nameRomaji val infoNameRomaji = tripleTab + media.nameRomaji
binding.mediaInfoNameRomaji.text = infoNameRomanji binding.mediaInfoNameRomaji.text = infoNameRomaji
binding.mediaInfoNameRomaji.setOnLongClickListener { binding.mediaInfoNameRomaji.setOnLongClickListener {
copyToClipboard(media.nameRomaji) copyToClipboard(media.nameRomaji)
true true

View File

@@ -271,29 +271,23 @@ class MediaListDialogFragment : BottomSheetDialogFragment() {
} }
binding.mediaListDelete.setOnClickListener { binding.mediaListDelete.setOnClickListener {
var id = media!!.userListId
scope.launch { scope.launch {
withContext(Dispatchers.IO) { media?.deleteFromList(scope, onSuccess = {
if (id != null) { Refresh.all()
Anilist.mutation.deleteList(id!!) snackString(getString(R.string.deleted_from_list))
MAL.query.deleteList(media?.anime != null, media?.idMAL) dismissAllowingStateLoss()
} else { }, onError = { e ->
val profile = Anilist.query.userMediaDetails(media!!) withContext(Dispatchers.Main) {
profile.userListId?.let { listId -> snackString(
id = listId getString(
Anilist.mutation.deleteList(listId) R.string.delete_fail_reason, e.message
MAL.query.deleteList(media?.anime != null, media?.idMAL) )
} )
} }
} }, onNotFound = {
PrefManager.setCustomVal("removeList", removeList.minus(media?.id)) snackString(getString(R.string.no_list_id))
} })
if (id != null) {
Refresh.all()
snackString(getString(R.string.deleted_from_list))
dismissAllowingStateLoss()
} else {
snackString(getString(R.string.no_list_id))
} }
} }
} }

View File

@@ -63,36 +63,24 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight } binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
val scope = viewLifecycleOwner.lifecycleScope val scope = viewLifecycleOwner.lifecycleScope
binding.mediaListDelete.setOnClickListener { binding.mediaListDelete.setOnClickListener {
var id = media.userListId
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
withContext(Dispatchers.IO) { scope.launch {
if (id != null) { media.deleteFromList(scope, onSuccess = {
try {
Anilist.mutation.deleteList(id!!)
MAL.query.deleteList(media.anime != null, media.idMAL)
} catch (e: Exception) {
withContext(Dispatchers.Main) {
snackString(getString(R.string.delete_fail_reason, e.message))
}
return@withContext
}
} else {
val profile = Anilist.query.userMediaDetails(media)
profile.userListId?.let { listId ->
id = listId
Anilist.mutation.deleteList(listId)
MAL.query.deleteList(media.anime != null, media.idMAL)
}
}
}
withContext(Dispatchers.Main) {
if (id != null) {
Refresh.all() Refresh.all()
snackString(getString(R.string.deleted_from_list)) snackString(getString(R.string.deleted_from_list))
dismissAllowingStateLoss() dismissAllowingStateLoss()
} else { }, onError = { e ->
withContext(Dispatchers.Main) {
snackString(
getString(
R.string.delete_fail_reason, e.message
)
)
}
}, onNotFound = {
snackString(getString(R.string.no_list_id)) snackString(getString(R.string.no_list_id))
} })
} }
} }
} }

View File

@@ -18,9 +18,8 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import java.util.ArrayList
class MediaListViewActivity: AppCompatActivity() { class MediaListViewActivity : AppCompatActivity() {
private lateinit var binding: ActivityMediaListViewBinding private lateinit var binding: ActivityMediaListViewBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -52,7 +51,8 @@ class MediaListViewActivity: AppCompatActivity() {
binding.listAppBar.setBackgroundColor(primaryColor) binding.listAppBar.setBackgroundColor(primaryColor)
binding.listTitle.setTextColor(primaryTextColor) binding.listTitle.setTextColor(primaryTextColor)
val screenWidth = resources.displayMetrics.run { widthPixels / density } val screenWidth = resources.displayMetrics.run { widthPixels / density }
val mediaList = passedMedia ?: intent.getSerialized("media") as? ArrayList<Media> ?: ArrayList() val mediaList =
passedMedia ?: intent.getSerialized("media") as? ArrayList<Media> ?: ArrayList()
if (passedMedia != null) passedMedia = null if (passedMedia != null) passedMedia = null
val view = PrefManager.getCustomVal("mediaView", 0) val view = PrefManager.getCustomVal("mediaView", 0)
var mediaView: View = when (view) { var mediaView: View = when (view) {

View File

@@ -44,7 +44,10 @@ class MediaSocialAdapter(
profileUserName.text = user.name profileUserName.text = user.name
profileInfo.apply { profileInfo.apply {
text = when (user.status) { text = when (user.status) {
"CURRENT" -> if (type == "ANIME") getAppString(R.string.watching) else getAppString(R.string.reading) "CURRENT" -> if (type == "ANIME") getAppString(R.string.watching) else getAppString(
R.string.reading
)
else -> user.status ?: "" else -> user.status ?: ""
} }
visibility = View.VISIBLE visibility = View.VISIBLE
@@ -63,10 +66,12 @@ class MediaSocialAdapter(
profileCompactProgressContainer.visibility = View.VISIBLE profileCompactProgressContainer.visibility = View.VISIBLE
profileUserAvatar.setOnClickListener { profileUserAvatar.setOnClickListener {
ContextCompat.startActivity(root.context, ContextCompat.startActivity(
root.context,
Intent(root.context, ProfileActivity::class.java) Intent(root.context, ProfileActivity::class.java)
.putExtra("userId", user.id), .putExtra("userId", user.id),
null) null
)
} }
profileUserAvatarContainer.setOnLongClickListener { profileUserAvatarContainer.setOnLongClickListener {
ImageViewDialog.newInstance( ImageViewDialog.newInstance(

View File

@@ -26,25 +26,50 @@ class OtherDetailsViewModel : ViewModel() {
if (author.value == null) author.postValue(Anilist.query.getAuthorDetails(m)) if (author.value == null) author.postValue(Anilist.query.getAuthorDetails(m))
} }
private var cachedAllCalendarData: Map<String, MutableList<Media>>? = null
private var cachedLibraryCalendarData: Map<String, MutableList<Media>>? = null
private val calendar: MutableLiveData<Map<String, MutableList<Media>>> = MutableLiveData(null) private val calendar: MutableLiveData<Map<String, MutableList<Media>>> = MutableLiveData(null)
fun getCalendar(): LiveData<Map<String, MutableList<Media>>> = calendar fun getCalendar(): LiveData<Map<String, MutableList<Media>>> = calendar
suspend fun loadCalendar() { suspend fun loadCalendar(showOnlyLibrary: Boolean = false) {
val curr = System.currentTimeMillis() / 1000 if (cachedAllCalendarData == null || cachedLibraryCalendarData == null) {
val res = Anilist.query.recentlyUpdated(curr - 86400, curr + (86400 * 6)) val curr = System.currentTimeMillis() / 1000
val df = DateFormat.getDateInstance(DateFormat.FULL) val res = Anilist.query.recentlyUpdated(curr - 86400, curr + (86400 * 6))
val map = mutableMapOf<String, MutableList<Media>>() val df = DateFormat.getDateInstance(DateFormat.FULL)
val idMap = mutableMapOf<String, MutableList<Int>>() val allMap = mutableMapOf<String, MutableList<Media>>()
res?.forEach { val libraryMap = mutableMapOf<String, MutableList<Media>>()
val v = it.relation?.split(",")?.map { i -> i.toLong() }!! val idMap = mutableMapOf<String, MutableList<Int>>()
val dateInfo = df.format(Date(v[1] * 1000))
val list = map.getOrPut(dateInfo) { mutableListOf() } val userId = Anilist.userid ?: 0
val idList = idMap.getOrPut(dateInfo) { mutableListOf() } val userLibrary = Anilist.query.getMediaLists(true, userId)
it.relation = "Episode ${v[0]}" val libraryMediaIds = userLibrary.flatMap { it.value }.map { it.id }
if (!idList.contains(it.id)) {
idList.add(it.id) res.forEach {
list.add(it) val v = it.relation?.split(",")?.map { i -> i.toLong() }!!
val dateInfo = df.format(Date(v[1] * 1000))
val list = allMap.getOrPut(dateInfo) { mutableListOf() }
val libraryList = if (libraryMediaIds.contains(it.id)) {
libraryMap.getOrPut(dateInfo) { mutableListOf() }
} else {
null
}
val idList = idMap.getOrPut(dateInfo) { mutableListOf() }
it.relation = "Episode ${v[0]}"
if (!idList.contains(it.id)) {
idList.add(it.id)
list.add(it)
libraryList?.add(it)
}
} }
cachedAllCalendarData = allMap
cachedLibraryCalendarData = libraryMap
} }
calendar.postValue(map)
val cacheToUse: Map<String, MutableList<Media>> = if (showOnlyLibrary) {
cachedLibraryCalendarData ?: emptyMap()
} else {
cachedAllCalendarData ?: emptyMap()
}
calendar.postValue(cacheToUse)
} }
} }

View File

@@ -3,7 +3,6 @@ package ani.dantotsu.media
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.SpannableString
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@@ -21,7 +20,7 @@ import ani.dantotsu.initActivity
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.MarkdownCreatorActivity import ani.dantotsu.util.ActivityMarkdownCreator
import com.xwray.groupie.GroupieAdapter import com.xwray.groupie.GroupieAdapter
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -59,7 +58,7 @@ class ReviewActivity : AppCompatActivity() {
binding.followFilterButton.setOnClickListener { binding.followFilterButton.setOnClickListener {
ContextCompat.startActivity( ContextCompat.startActivity(
this, this,
Intent(this, MarkdownCreatorActivity::class.java) Intent(this, ActivityMarkdownCreator::class.java)
.putExtra("type", "review"), .putExtra("type", "review"),
null null
) )

View File

@@ -1,23 +1,15 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.view.View import android.view.View
import androidx.activity.ComponentActivity
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.util.Pair
import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.Query import ani.dantotsu.connections.anilist.api.Query
import ani.dantotsu.databinding.ItemReviewsBinding import ani.dantotsu.databinding.ItemReviewsBinding
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
import ani.dantotsu.openImage import ani.dantotsu.openImage
import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.profile.ProfileActivity import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.activity.ActivityItemBuilder import ani.dantotsu.profile.activity.ActivityItemBuilder
import ani.dantotsu.toast import ani.dantotsu.toast
@@ -40,7 +32,7 @@ class ReviewAdapter(
binding.reviewUserAvatar.loadImage(review.user?.avatar?.medium) binding.reviewUserAvatar.loadImage(review.user?.avatar?.medium)
binding.reviewText.text = review.summary binding.reviewText.text = review.summary
binding.reviewPostTime.text = ActivityItemBuilder.getDateTime(review.createdAt) binding.reviewPostTime.text = ActivityItemBuilder.getDateTime(review.createdAt)
val text = "[${review.score/ 10.0f}]" val text = "[${review.score / 10.0f}]"
binding.reviewTag.text = text binding.reviewTag.text = text
binding.root.setOnClickListener { binding.root.setOnClickListener {
ContextCompat.startActivity( ContextCompat.startActivity(
@@ -85,6 +77,7 @@ class ReviewAdapter(
override fun initializeViewBinding(view: View): ItemReviewsBinding { override fun initializeViewBinding(view: View): ItemReviewsBinding {
return ItemReviewsBinding.bind(view) return ItemReviewsBinding.bind(view)
} }
private fun userVote(type: String) { private fun userVote(type: String) {
when (type) { when (type) {
"NO_VOTE" -> { "NO_VOTE" -> {

View File

@@ -20,7 +20,6 @@ import ani.dantotsu.initActivity
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.openImage import ani.dantotsu.openImage
import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.profile.ProfileActivity import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.activity.ActivityItemBuilder import ani.dantotsu.profile.activity.ActivityItemBuilder
import ani.dantotsu.statusBarHeight import ani.dantotsu.statusBarHeight
@@ -52,8 +51,9 @@ class ReviewViewActivity : AppCompatActivity() {
binding.userAvatar.loadImage(review.user?.avatar?.medium) binding.userAvatar.loadImage(review.user?.avatar?.medium)
binding.userTime.text = ActivityItemBuilder.getDateTime(review.createdAt) binding.userTime.text = ActivityItemBuilder.getDateTime(review.createdAt)
binding.userContainer.setOnClickListener { binding.userContainer.setOnClickListener {
startActivity(Intent(this, ProfileActivity::class.java) startActivity(
.putExtra("userId", review.user?.id) Intent(this, ProfileActivity::class.java)
.putExtra("userId", review.user?.id)
) )
} }
binding.userAvatar.openImage( binding.userAvatar.openImage(
@@ -61,8 +61,9 @@ class ReviewViewActivity : AppCompatActivity() {
review.user?.avatar?.medium ?: "" review.user?.avatar?.medium ?: ""
) )
binding.userAvatar.setOnClickListener { binding.userAvatar.setOnClickListener {
startActivity(Intent(this, ProfileActivity::class.java) startActivity(
.putExtra("userId", review.user?.id) Intent(this, ProfileActivity::class.java)
.putExtra("userId", review.user?.id)
) )
} }
binding.profileUserBio.settings.loadWithOverviewMode = true binding.profileUserBio.settings.loadWithOverviewMode = true

View File

@@ -13,12 +13,18 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.connections.anilist.AniMangaSearchResults
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistSearch import ani.dantotsu.connections.anilist.AnilistSearch
import ani.dantotsu.connections.anilist.SearchResults import ani.dantotsu.connections.anilist.AnilistSearch.SearchType
import ani.dantotsu.connections.anilist.CharacterSearchResults
import ani.dantotsu.connections.anilist.StaffSearchResults
import ani.dantotsu.connections.anilist.StudioSearchResults
import ani.dantotsu.connections.anilist.UserSearchResults
import ani.dantotsu.databinding.ActivitySearchBinding import ani.dantotsu.databinding.ActivitySearchBinding
import ani.dantotsu.initActivity import ani.dantotsu.initActivity
import ani.dantotsu.navBarHeight import ani.dantotsu.navBarHeight
import ani.dantotsu.profile.UsersAdapter
import ani.dantotsu.px import ani.dantotsu.px
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
@@ -35,14 +41,25 @@ class SearchActivity : AppCompatActivity() {
val model: AnilistSearch by viewModels() val model: AnilistSearch by viewModels()
var style: Int = 0 var style: Int = 0
lateinit var searchType: SearchType
private var screenWidth: Float = 0f private var screenWidth: Float = 0f
private lateinit var mediaAdaptor: MediaAdaptor private lateinit var mediaAdaptor: MediaAdaptor
private lateinit var characterAdaptor: CharacterAdapter
private lateinit var studioAdaptor: StudioAdapter
private lateinit var staffAdaptor: AuthorAdapter
private lateinit var usersAdapter: UsersAdapter
private lateinit var progressAdapter: ProgressAdapter private lateinit var progressAdapter: ProgressAdapter
private lateinit var concatAdapter: ConcatAdapter private lateinit var concatAdapter: ConcatAdapter
private lateinit var headerAdaptor: SearchAdapter private lateinit var headerAdaptor: HeaderInterface
lateinit var aniMangaResult: AniMangaSearchResults
lateinit var characterResult: CharacterSearchResults
lateinit var studioResult: StudioSearchResults
lateinit var staffResult: StaffSearchResults
lateinit var userResult: UserSearchResults
lateinit var result: SearchResults
lateinit var updateChips: (() -> Unit) lateinit var updateChips: (() -> Unit)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -59,39 +76,117 @@ class SearchActivity : AppCompatActivity() {
bottom = navBarHeight + 80f.px bottom = navBarHeight + 80f.px
) )
style = PrefManager.getVal(PrefName.SearchStyle)
var listOnly: Boolean? = intent.getBooleanExtra("listOnly", false)
if (!listOnly!!) listOnly = null
val notSet = model.notSet val notSet = model.notSet
if (model.notSet) { searchType = SearchType.fromString(intent.getStringExtra("type") ?: "ANIME")
model.notSet = false when (searchType) {
model.searchResults = SearchResults( SearchType.ANIME, SearchType.MANGA -> {
intent.getStringExtra("type") ?: "ANIME", style = PrefManager.getVal(PrefName.SearchStyle)
isAdult = if (Anilist.adult) intent.getBooleanExtra("hentai", false) else false, var listOnly: Boolean? = intent.getBooleanExtra("listOnly", false)
onList = listOnly, if (!listOnly!!) listOnly = null
search = intent.getStringExtra("query"),
genres = intent.getStringExtra("genre")?.let { mutableListOf(it) }, if (model.notSet) {
tags = intent.getStringExtra("tag")?.let { mutableListOf(it) }, model.notSet = false
sort = intent.getStringExtra("sortBy"), model.aniMangaSearchResults = AniMangaSearchResults(
status = intent.getStringExtra("status"), intent.getStringExtra("type") ?: "ANIME",
source = intent.getStringExtra("source"), isAdult = if (Anilist.adult) intent.getBooleanExtra(
countryOfOrigin = intent.getStringExtra("country"), "hentai",
season = intent.getStringExtra("season"), false
seasonYear = if (intent.getStringExtra("type") == "ANIME") intent.getStringExtra("seasonYear") ) else false,
?.toIntOrNull() else null, onList = listOnly,
startYear = if (intent.getStringExtra("type") == "MANGA") intent.getStringExtra("seasonYear") search = intent.getStringExtra("query"),
?.toIntOrNull() else null, genres = intent.getStringExtra("genre")?.let { mutableListOf(it) },
results = mutableListOf(), tags = intent.getStringExtra("tag")?.let { mutableListOf(it) },
hasNextPage = false sort = intent.getStringExtra("sortBy"),
) status = intent.getStringExtra("status"),
source = intent.getStringExtra("source"),
countryOfOrigin = intent.getStringExtra("country"),
season = intent.getStringExtra("season"),
seasonYear = if (intent.getStringExtra("type") == "ANIME") intent.getStringExtra(
"seasonYear"
)
?.toIntOrNull() else null,
startYear = if (intent.getStringExtra("type") == "MANGA") intent.getStringExtra(
"seasonYear"
)
?.toIntOrNull() else null,
results = mutableListOf(),
hasNextPage = false
)
}
aniMangaResult = model.aniMangaSearchResults
mediaAdaptor =
MediaAdaptor(
style,
model.aniMangaSearchResults.results,
this,
matchParent = true
)
}
SearchType.CHARACTER -> {
if (model.notSet) {
model.notSet = false
model.characterSearchResults = CharacterSearchResults(
search = intent.getStringExtra("query"),
results = mutableListOf(),
hasNextPage = false
)
characterResult = model.characterSearchResults
characterAdaptor = CharacterAdapter(model.characterSearchResults.results)
}
}
SearchType.STUDIO -> {
if (model.notSet) {
model.notSet = false
model.studioSearchResults = StudioSearchResults(
search = intent.getStringExtra("query"),
results = mutableListOf(),
hasNextPage = false
)
studioResult = model.studioSearchResults
studioAdaptor = StudioAdapter(model.studioSearchResults.results)
}
}
SearchType.STAFF -> {
if (model.notSet) {
model.notSet = false
model.staffSearchResults = StaffSearchResults(
search = intent.getStringExtra("query"),
results = mutableListOf(),
hasNextPage = false
)
staffResult = model.staffSearchResults
staffAdaptor = AuthorAdapter(model.staffSearchResults.results)
}
}
SearchType.USER -> {
if (model.notSet) {
model.notSet = false
model.userSearchResults = UserSearchResults(
search = intent.getStringExtra("query"),
results = mutableListOf(),
hasNextPage = false
)
userResult = model.userSearchResults
usersAdapter = UsersAdapter(model.userSearchResults.results, grid = true)
}
}
} }
result = model.searchResults
progressAdapter = ProgressAdapter(searched = model.searched) progressAdapter = ProgressAdapter(searched = model.searched)
mediaAdaptor = MediaAdaptor(style, model.searchResults.results, this, matchParent = true) headerAdaptor = if (searchType == SearchType.ANIME || searchType == SearchType.MANGA) {
headerAdaptor = SearchAdapter(this, model.searchResults.type) SearchAdapter(this, searchType)
} else {
SupportingSearchAdapter(this, searchType)
}
val gridSize = (screenWidth / 120f).toInt() val gridSize = (screenWidth / 120f).toInt()
val gridLayoutManager = GridLayoutManager(this, gridSize) val gridLayoutManager = GridLayoutManager(this, gridSize)
@@ -108,7 +203,27 @@ class SearchActivity : AppCompatActivity() {
} }
} }
concatAdapter = ConcatAdapter(headerAdaptor, mediaAdaptor, progressAdapter) concatAdapter = when (searchType) {
SearchType.ANIME, SearchType.MANGA -> {
ConcatAdapter(headerAdaptor, mediaAdaptor, progressAdapter)
}
SearchType.CHARACTER -> {
ConcatAdapter(headerAdaptor, characterAdaptor, progressAdapter)
}
SearchType.STUDIO -> {
ConcatAdapter(headerAdaptor, studioAdaptor, progressAdapter)
}
SearchType.STAFF -> {
ConcatAdapter(headerAdaptor, staffAdaptor, progressAdapter)
}
SearchType.USER -> {
ConcatAdapter(headerAdaptor, usersAdapter, progressAdapter)
}
}
binding.searchRecyclerView.layoutManager = gridLayoutManager binding.searchRecyclerView.layoutManager = gridLayoutManager
binding.searchRecyclerView.adapter = concatAdapter binding.searchRecyclerView.adapter = concatAdapter
@@ -117,9 +232,9 @@ class SearchActivity : AppCompatActivity() {
RecyclerView.OnScrollListener() { RecyclerView.OnScrollListener() {
override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) {
if (!v.canScrollVertically(1)) { if (!v.canScrollVertically(1)) {
if (model.searchResults.hasNextPage && model.searchResults.results.isNotEmpty() && !loading) { if (model.hasNextPage(searchType) && model.resultsIsNotEmpty(searchType) && !loading) {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
model.loadNextPage(model.searchResults) model.loadNextPage(searchType)
} }
} }
} }
@@ -127,34 +242,110 @@ class SearchActivity : AppCompatActivity() {
} }
}) })
model.getSearch().observe(this) { when (searchType) {
if (it != null) { SearchType.ANIME, SearchType.MANGA -> {
model.searchResults.apply { model.getSearch<AniMangaSearchResults>(searchType).observe(this) {
onList = it.onList if (it != null) {
isAdult = it.isAdult model.aniMangaSearchResults.apply {
perPage = it.perPage onList = it.onList
search = it.search isAdult = it.isAdult
sort = it.sort perPage = it.perPage
genres = it.genres search = it.search
excludedGenres = it.excludedGenres sort = it.sort
excludedTags = it.excludedTags genres = it.genres
tags = it.tags excludedGenres = it.excludedGenres
season = it.season excludedTags = it.excludedTags
startYear = it.startYear tags = it.tags
seasonYear = it.seasonYear season = it.season
status = it.status startYear = it.startYear
source = it.source seasonYear = it.seasonYear
format = it.format status = it.status
countryOfOrigin = it.countryOfOrigin source = it.source
page = it.page format = it.format
hasNextPage = it.hasNextPage countryOfOrigin = it.countryOfOrigin
page = it.page
hasNextPage = it.hasNextPage
}
val prev = model.aniMangaSearchResults.results.size
model.aniMangaSearchResults.results.addAll(it.results)
mediaAdaptor.notifyItemRangeInserted(prev, it.results.size)
progressAdapter.bar?.isVisible = it.hasNextPage
}
} }
}
val prev = model.searchResults.results.size SearchType.CHARACTER -> {
model.searchResults.results.addAll(it.results) model.getSearch<CharacterSearchResults>(searchType).observe(this) {
mediaAdaptor.notifyItemRangeInserted(prev, it.results.size) if (it != null) {
model.characterSearchResults.apply {
search = it.search
page = it.page
hasNextPage = it.hasNextPage
}
progressAdapter.bar?.isVisible = it.hasNextPage val prev = model.characterSearchResults.results.size
model.characterSearchResults.results.addAll(it.results)
characterAdaptor.notifyItemRangeInserted(prev, it.results.size)
progressAdapter.bar?.isVisible = it.hasNextPage
}
}
}
SearchType.STUDIO -> {
model.getSearch<StudioSearchResults>(searchType).observe(this) {
if (it != null) {
model.studioSearchResults.apply {
search = it.search
page = it.page
hasNextPage = it.hasNextPage
}
val prev = model.studioSearchResults.results.size
model.studioSearchResults.results.addAll(it.results)
studioAdaptor.notifyItemRangeInserted(prev, it.results.size)
progressAdapter.bar?.isVisible = it.hasNextPage
}
}
}
SearchType.STAFF -> {
model.getSearch<StaffSearchResults>(searchType).observe(this) {
if (it != null) {
model.staffSearchResults.apply {
search = it.search
page = it.page
hasNextPage = it.hasNextPage
}
val prev = model.staffSearchResults.results.size
model.staffSearchResults.results.addAll(it.results)
staffAdaptor.notifyItemRangeInserted(prev, it.results.size)
progressAdapter.bar?.isVisible = it.hasNextPage
}
}
}
SearchType.USER -> {
model.getSearch<UserSearchResults>(searchType).observe(this) {
if (it != null) {
model.userSearchResults.apply {
search = it.search
page = it.page
hasNextPage = it.hasNextPage
}
val prev = model.userSearchResults.results.size
model.userSearchResults.results.addAll(it.results)
usersAdapter.notifyItemRangeInserted(prev, it.results.size)
progressAdapter.bar?.isVisible = it.hasNextPage
}
}
} }
} }
@@ -179,8 +370,35 @@ class SearchActivity : AppCompatActivity() {
fun emptyMediaAdapter() { fun emptyMediaAdapter() {
searchTimer.cancel() searchTimer.cancel()
searchTimer.purge() searchTimer.purge()
mediaAdaptor.notifyItemRangeRemoved(0, model.searchResults.results.size) when (searchType) {
model.searchResults.results.clear() SearchType.ANIME, SearchType.MANGA -> {
mediaAdaptor.notifyItemRangeRemoved(0, model.aniMangaSearchResults.results.size)
model.aniMangaSearchResults.results.clear()
}
SearchType.CHARACTER -> {
characterAdaptor.notifyItemRangeRemoved(
0,
model.characterSearchResults.results.size
)
model.characterSearchResults.results.clear()
}
SearchType.STUDIO -> {
studioAdaptor.notifyItemRangeRemoved(0, model.studioSearchResults.results.size)
model.studioSearchResults.results.clear()
}
SearchType.STAFF -> {
staffAdaptor.notifyItemRangeRemoved(0, model.staffSearchResults.results.size)
model.staffSearchResults.results.clear()
}
SearchType.USER -> {
usersAdapter.notifyItemRangeRemoved(0, model.userSearchResults.results.size)
model.userSearchResults.results.clear()
}
}
progressAdapter.bar?.visibility = View.GONE progressAdapter.bar?.visibility = View.GONE
} }
@@ -188,10 +406,30 @@ class SearchActivity : AppCompatActivity() {
private var loading = false private var loading = false
fun search() { fun search() {
headerAdaptor.setHistoryVisibility(false) headerAdaptor.setHistoryVisibility(false)
val size = model.searchResults.results.size val size = model.size(searchType)
model.searchResults.results.clear() model.clearResults(searchType)
binding.searchRecyclerView.post { binding.searchRecyclerView.post {
mediaAdaptor.notifyItemRangeRemoved(0, size) when (searchType) {
SearchType.ANIME, SearchType.MANGA -> {
mediaAdaptor.notifyItemRangeRemoved(0, size)
}
SearchType.CHARACTER -> {
characterAdaptor.notifyItemRangeRemoved(0, size)
}
SearchType.STUDIO -> {
studioAdaptor.notifyItemRangeRemoved(0, size)
}
SearchType.STAFF -> {
staffAdaptor.notifyItemRangeRemoved(0, size)
}
SearchType.USER -> {
usersAdapter.notifyItemRangeRemoved(0, size)
}
}
} }
progressAdapter.bar?.visibility = View.VISIBLE progressAdapter.bar?.visibility = View.VISIBLE
@@ -202,7 +440,7 @@ class SearchActivity : AppCompatActivity() {
override fun run() { override fun run() {
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
loading = true loading = true
model.loadSearch(result) model.loadSearch(searchType)
loading = false loading = false
} }
} }
@@ -213,8 +451,10 @@ class SearchActivity : AppCompatActivity() {
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
fun recycler() { fun recycler() {
mediaAdaptor.type = style if (searchType == SearchType.ANIME || searchType == SearchType.MANGA) {
mediaAdaptor.notifyDataSetChanged() mediaAdaptor.type = style
mediaAdaptor.notifyDataSetChanged()
}
} }
var state: Parcelable? = null var state: Parcelable? = null

View File

@@ -9,8 +9,6 @@ import android.view.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.PopupMenu import android.widget.PopupMenu
@@ -22,8 +20,8 @@ import androidx.recyclerview.widget.RecyclerView.HORIZONTAL
import ani.dantotsu.App.Companion.context import ani.dantotsu.App.Companion.context
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistSearch.SearchType
import ani.dantotsu.databinding.ItemChipBinding import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.databinding.ItemSearchHeaderBinding
import ani.dantotsu.openLinkInBrowser import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.imagesearch.ImageSearchActivity import ani.dantotsu.others.imagesearch.ImageSearchActivity
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
@@ -36,18 +34,11 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class SearchAdapter(private val activity: SearchActivity, private val type: SearchType) :
class SearchAdapter(private val activity: SearchActivity, private val type: String) : HeaderInterface() {
RecyclerView.Adapter<SearchAdapter.SearchHeaderViewHolder>() {
private val itemViewType = 6969
var search: Runnable? = null
var requestFocus: Runnable? = null
private var textWatcher: TextWatcher? = null
private lateinit var searchHistoryAdapter: SearchHistoryAdapter
private lateinit var binding: ItemSearchHeaderBinding
private fun updateFilterTextViewDrawable() { private fun updateFilterTextViewDrawable() {
val filterDrawable = when (activity.result.sort) { val filterDrawable = when (activity.aniMangaResult.sort) {
Anilist.sortBy[0] -> R.drawable.ic_round_area_chart_24 Anilist.sortBy[0] -> R.drawable.ic_round_area_chart_24
Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24 Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24
Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24 Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24
@@ -60,18 +51,13 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
binding.filterTextView.setCompoundDrawablesWithIntrinsicBounds(filterDrawable, 0, 0, 0) binding.filterTextView.setCompoundDrawablesWithIntrinsicBounds(filterDrawable, 0, 0, 0)
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder {
val binding =
ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SearchHeaderViewHolder(binding)
}
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: SearchHeaderViewHolder, position: Int) { override fun onBindViewHolder(holder: SearchHeaderViewHolder, position: Int) {
binding = holder.binding binding = holder.binding
searchHistoryAdapter = SearchHistoryAdapter(type) { searchHistoryAdapter = SearchHistoryAdapter(type) {
binding.searchBarText.setText(it) binding.searchBarText.setText(it)
binding.searchBarText.setSelection(it.length)
} }
binding.searchHistoryList.layoutManager = LinearLayoutManager(binding.root.context) binding.searchHistoryList.layoutManager = LinearLayoutManager(binding.root.context)
binding.searchHistoryList.adapter = searchHistoryAdapter binding.searchHistoryList.adapter = searchHistoryAdapter
@@ -79,6 +65,10 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
val imm: InputMethodManager = val imm: InputMethodManager =
activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
if (activity.searchType != SearchType.MANGA && activity.searchType != SearchType.ANIME) {
throw IllegalArgumentException("Invalid search type (wrong adapter)")
}
when (activity.style) { when (activity.style) {
0 -> { 0 -> {
binding.searchResultGrid.alpha = 1f binding.searchResultGrid.alpha = 1f
@@ -91,7 +81,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
} }
} }
binding.searchBar.hint = activity.result.type binding.searchBar.hint = activity.aniMangaResult.type
if (PrefManager.getVal(PrefName.Incognito)) { if (PrefManager.getVal(PrefName.Incognito)) {
val startIconDrawableRes = R.drawable.ic_incognito_24 val startIconDrawableRes = R.drawable.ic_incognito_24
val startIconDrawable: Drawable? = val startIconDrawable: Drawable? =
@@ -99,11 +89,11 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
binding.searchBar.startIconDrawable = startIconDrawable binding.searchBar.startIconDrawable = startIconDrawable
} }
var adult = activity.result.isAdult var adult = activity.aniMangaResult.isAdult
var listOnly = activity.result.onList var listOnly = activity.aniMangaResult.onList
binding.searchBarText.removeTextChangedListener(textWatcher) binding.searchBarText.removeTextChangedListener(textWatcher)
binding.searchBarText.setText(activity.result.search) binding.searchBarText.setText(activity.aniMangaResult.search)
binding.searchAdultCheck.isChecked = adult binding.searchAdultCheck.isChecked = adult
binding.searchList.isChecked = listOnly == true binding.searchList.isChecked = listOnly == true
@@ -124,49 +114,49 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
popupMenu.setOnMenuItemClickListener { item -> popupMenu.setOnMenuItemClickListener { item ->
when (item.itemId) { when (item.itemId) {
R.id.sort_by_score -> { R.id.sort_by_score -> {
activity.result.sort = Anilist.sortBy[0] activity.aniMangaResult.sort = Anilist.sortBy[0]
activity.updateChips.invoke() activity.updateChips.invoke()
activity.search() activity.search()
updateFilterTextViewDrawable() updateFilterTextViewDrawable()
} }
R.id.sort_by_popular -> { R.id.sort_by_popular -> {
activity.result.sort = Anilist.sortBy[1] activity.aniMangaResult.sort = Anilist.sortBy[1]
activity.updateChips.invoke() activity.updateChips.invoke()
activity.search() activity.search()
updateFilterTextViewDrawable() updateFilterTextViewDrawable()
} }
R.id.sort_by_trending -> { R.id.sort_by_trending -> {
activity.result.sort = Anilist.sortBy[2] activity.aniMangaResult.sort = Anilist.sortBy[2]
activity.updateChips.invoke() activity.updateChips.invoke()
activity.search() activity.search()
updateFilterTextViewDrawable() updateFilterTextViewDrawable()
} }
R.id.sort_by_recent -> { R.id.sort_by_recent -> {
activity.result.sort = Anilist.sortBy[3] activity.aniMangaResult.sort = Anilist.sortBy[3]
activity.updateChips.invoke() activity.updateChips.invoke()
activity.search() activity.search()
updateFilterTextViewDrawable() updateFilterTextViewDrawable()
} }
R.id.sort_by_a_z -> { R.id.sort_by_a_z -> {
activity.result.sort = Anilist.sortBy[4] activity.aniMangaResult.sort = Anilist.sortBy[4]
activity.updateChips.invoke() activity.updateChips.invoke()
activity.search() activity.search()
updateFilterTextViewDrawable() updateFilterTextViewDrawable()
} }
R.id.sort_by_z_a -> { R.id.sort_by_z_a -> {
activity.result.sort = Anilist.sortBy[5] activity.aniMangaResult.sort = Anilist.sortBy[5]
activity.updateChips.invoke() activity.updateChips.invoke()
activity.search() activity.search()
updateFilterTextViewDrawable() updateFilterTextViewDrawable()
} }
R.id.sort_by_pure_pain -> { R.id.sort_by_pure_pain -> {
activity.result.sort = Anilist.sortBy[6] activity.aniMangaResult.sort = Anilist.sortBy[6]
activity.updateChips.invoke() activity.updateChips.invoke()
activity.search() activity.search()
updateFilterTextViewDrawable() updateFilterTextViewDrawable()
@@ -177,14 +167,20 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
popupMenu.show() popupMenu.show()
true true
} }
if (activity.result.type != "ANIME") { if (activity.aniMangaResult.type != "ANIME") {
binding.searchByImage.visibility = View.GONE binding.searchByImage.visibility = View.GONE
} }
binding.searchByImage.setOnClickListener { binding.searchByImage.setOnClickListener {
activity.startActivity(Intent(activity, ImageSearchActivity::class.java)) activity.startActivity(Intent(activity, ImageSearchActivity::class.java))
} }
binding.clearHistory.setOnClickListener {
it.startAnimation(fadeOutAnimation())
it.visibility = View.GONE
searchHistoryAdapter.clearHistory()
}
updateClearHistoryVisibility()
fun searchTitle() { fun searchTitle() {
activity.result.apply { activity.aniMangaResult.apply {
search = search =
if (binding.searchBarText.text.toString() != "") binding.searchBarText.text.toString() else null if (binding.searchBarText.text.toString() != "") binding.searchBarText.text.toString() else null
onList = listOnly onList = listOnly
@@ -286,61 +282,12 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
requestFocus = Runnable { binding.searchBarText.requestFocus() } requestFocus = Runnable { binding.searchBarText.requestFocus() }
} }
fun setHistoryVisibility(visible: Boolean) {
if (visible) {
binding.searchResultLayout.startAnimation(fadeOutAnimation())
binding.searchHistoryList.startAnimation(fadeInAnimation())
binding.searchResultLayout.visibility = View.GONE
binding.searchHistoryList.visibility = View.VISIBLE
binding.searchByImage.visibility = View.VISIBLE
} else {
if (binding.searchResultLayout.visibility != View.VISIBLE) {
binding.searchResultLayout.startAnimation(fadeInAnimation())
binding.searchHistoryList.startAnimation(fadeOutAnimation())
}
binding.searchResultLayout.visibility = View.VISIBLE
binding.searchHistoryList.visibility = View.GONE
binding.searchByImage.visibility = View.GONE
}
}
private fun fadeInAnimation(): Animation {
return AlphaAnimation(0f, 1f).apply {
duration = 150
}
}
private fun fadeOutAnimation(): Animation {
return AlphaAnimation(1f, 0f).apply {
duration = 150
}
}
fun addHistory() {
if (::searchHistoryAdapter.isInitialized &&
binding.searchBarText.text.toString().isNotBlank()
)
searchHistoryAdapter.add(binding.searchBarText.text.toString())
}
override fun getItemCount(): Int = 1
inner class SearchHeaderViewHolder(val binding: ItemSearchHeaderBinding) :
RecyclerView.ViewHolder(binding.root)
override fun getItemViewType(position: Int): Int {
return itemViewType
}
class SearchChipAdapter( class SearchChipAdapter(
val activity: SearchActivity, val activity: SearchActivity,
private val searchAdapter: SearchAdapter private val searchAdapter: SearchAdapter
) : ) :
RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() { RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() {
private var chips = activity.result.toChipList() private var chips = activity.aniMangaResult.toChipList()
inner class SearchChipViewHolder(val binding: ItemChipBinding) : inner class SearchChipViewHolder(val binding: ItemChipBinding) :
RecyclerView.ViewHolder(binding.root) RecyclerView.ViewHolder(binding.root)
@@ -357,7 +304,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
holder.binding.root.apply { holder.binding.root.apply {
text = chip.text.replace("_", " ") text = chip.text.replace("_", " ")
setOnClickListener { setOnClickListener {
activity.result.removeChip(chip) activity.aniMangaResult.removeChip(chip)
update() update()
activity.search() activity.search()
searchAdapter.updateFilterTextViewDrawable() searchAdapter.updateFilterTextViewDrawable()
@@ -367,7 +314,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
fun update() { fun update() {
chips = activity.result.toChipList() chips = activity.aniMangaResult.toChipList()
notifyDataSetChanged() notifyDataSetChanged()
searchAdapter.updateFilterTextViewDrawable() searchAdapter.updateFilterTextViewDrawable()
} }
@@ -375,4 +322,3 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
override fun getItemCount(): Int = chips.size override fun getItemCount(): Int = chips.size
} }
} }

View File

@@ -57,7 +57,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
} }
private fun setSortByFilterImage() { private fun setSortByFilterImage() {
val filterDrawable = when (activity.result.sort) { val filterDrawable = when (activity.aniMangaResult.sort) {
Anilist.sortBy[0] -> R.drawable.ic_round_area_chart_24 Anilist.sortBy[0] -> R.drawable.ic_round_area_chart_24
Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24 Anilist.sortBy[1] -> R.drawable.ic_round_filter_peak_24
Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24 Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24
@@ -71,10 +71,10 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
} }
private fun resetSearchFilter() { private fun resetSearchFilter() {
activity.result.sort = null activity.aniMangaResult.sort = null
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_alt_24) binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_alt_24)
startBounceZoomAnimation(binding.sortByFilter) startBounceZoomAnimation(binding.sortByFilter)
activity.result.countryOfOrigin = null activity.aniMangaResult.countryOfOrigin = null
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_search_googlefonts) binding.countryFilter.setImageResource(R.drawable.ic_round_globe_search_googlefonts)
startBounceZoomAnimation(binding.countryFilter) startBounceZoomAnimation(binding.countryFilter)
@@ -98,10 +98,10 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
activity = requireActivity() as SearchActivity activity = requireActivity() as SearchActivity
selectedGenres = activity.result.genres ?: mutableListOf() selectedGenres = activity.aniMangaResult.genres ?: mutableListOf()
exGenres = activity.result.excludedGenres ?: mutableListOf() exGenres = activity.aniMangaResult.excludedGenres ?: mutableListOf()
selectedTags = activity.result.tags ?: mutableListOf() selectedTags = activity.aniMangaResult.tags ?: mutableListOf()
exTags = activity.result.excludedTags ?: mutableListOf() exTags = activity.aniMangaResult.excludedTags ?: mutableListOf()
setSortByFilterImage() setSortByFilterImage()
binding.resetSearchFilter.setOnClickListener { binding.resetSearchFilter.setOnClickListener {
@@ -126,7 +126,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
resetSearchFilter() resetSearchFilter()
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
activity.result.apply { activity.aniMangaResult.apply {
status = status =
binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null } binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null }
source = source =
@@ -135,7 +135,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
season = binding.searchSeason.text.toString().ifBlank { null } season = binding.searchSeason.text.toString().ifBlank { null }
startYear = binding.searchYear.text.toString().toIntOrNull() startYear = binding.searchYear.text.toString().toIntOrNull()
seasonYear = binding.searchYear.text.toString().toIntOrNull() seasonYear = binding.searchYear.text.toString().toIntOrNull()
sort = activity.result.sort sort = activity.aniMangaResult.sort
genres = selectedGenres genres = selectedGenres
tags = selectedTags tags = selectedTags
excludedGenres = exGenres excludedGenres = exGenres
@@ -155,43 +155,43 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
popupMenu.setOnMenuItemClickListener { menuItem -> popupMenu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) { when (menuItem.itemId) {
R.id.sort_by_score -> { R.id.sort_by_score -> {
activity.result.sort = Anilist.sortBy[0] activity.aniMangaResult.sort = Anilist.sortBy[0]
binding.sortByFilter.setImageResource(R.drawable.ic_round_area_chart_24) binding.sortByFilter.setImageResource(R.drawable.ic_round_area_chart_24)
startBounceZoomAnimation() startBounceZoomAnimation()
} }
R.id.sort_by_popular -> { R.id.sort_by_popular -> {
activity.result.sort = Anilist.sortBy[1] activity.aniMangaResult.sort = Anilist.sortBy[1]
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_peak_24) binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_peak_24)
startBounceZoomAnimation() startBounceZoomAnimation()
} }
R.id.sort_by_trending -> { R.id.sort_by_trending -> {
activity.result.sort = Anilist.sortBy[2] activity.aniMangaResult.sort = Anilist.sortBy[2]
binding.sortByFilter.setImageResource(R.drawable.ic_round_star_graph_24) binding.sortByFilter.setImageResource(R.drawable.ic_round_star_graph_24)
startBounceZoomAnimation() startBounceZoomAnimation()
} }
R.id.sort_by_recent -> { R.id.sort_by_recent -> {
activity.result.sort = Anilist.sortBy[3] activity.aniMangaResult.sort = Anilist.sortBy[3]
binding.sortByFilter.setImageResource(R.drawable.ic_round_new_releases_24) binding.sortByFilter.setImageResource(R.drawable.ic_round_new_releases_24)
startBounceZoomAnimation() startBounceZoomAnimation()
} }
R.id.sort_by_a_z -> { R.id.sort_by_a_z -> {
activity.result.sort = Anilist.sortBy[4] activity.aniMangaResult.sort = Anilist.sortBy[4]
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_list_24) binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_list_24)
startBounceZoomAnimation() startBounceZoomAnimation()
} }
R.id.sort_by_z_a -> { R.id.sort_by_z_a -> {
activity.result.sort = Anilist.sortBy[5] activity.aniMangaResult.sort = Anilist.sortBy[5]
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_list_24_reverse) binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_list_24_reverse)
startBounceZoomAnimation() startBounceZoomAnimation()
} }
R.id.sort_by_pure_pain -> { R.id.sort_by_pure_pain -> {
activity.result.sort = Anilist.sortBy[6] activity.aniMangaResult.sort = Anilist.sortBy[6]
binding.sortByFilter.setImageResource(R.drawable.ic_round_assist_walker_24) binding.sortByFilter.setImageResource(R.drawable.ic_round_assist_walker_24)
startBounceZoomAnimation() startBounceZoomAnimation()
} }
@@ -212,25 +212,25 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
} }
R.id.country_china -> { R.id.country_china -> {
activity.result.countryOfOrigin = "CN" activity.aniMangaResult.countryOfOrigin = "CN"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_china_googlefonts) binding.countryFilter.setImageResource(R.drawable.ic_round_globe_china_googlefonts)
startBounceZoomAnimation(binding.countryFilter) startBounceZoomAnimation(binding.countryFilter)
} }
R.id.country_south_korea -> { R.id.country_south_korea -> {
activity.result.countryOfOrigin = "KR" activity.aniMangaResult.countryOfOrigin = "KR"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_south_korea_googlefonts) binding.countryFilter.setImageResource(R.drawable.ic_round_globe_south_korea_googlefonts)
startBounceZoomAnimation(binding.countryFilter) startBounceZoomAnimation(binding.countryFilter)
} }
R.id.country_japan -> { R.id.country_japan -> {
activity.result.countryOfOrigin = "JP" activity.aniMangaResult.countryOfOrigin = "JP"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_japan_googlefonts) binding.countryFilter.setImageResource(R.drawable.ic_round_globe_japan_googlefonts)
startBounceZoomAnimation(binding.countryFilter) startBounceZoomAnimation(binding.countryFilter)
} }
R.id.country_taiwan -> { R.id.country_taiwan -> {
activity.result.countryOfOrigin = "TW" activity.aniMangaResult.countryOfOrigin = "TW"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_taiwan_googlefonts) binding.countryFilter.setImageResource(R.drawable.ic_round_globe_taiwan_googlefonts)
startBounceZoomAnimation(binding.countryFilter) startBounceZoomAnimation(binding.countryFilter)
} }
@@ -241,18 +241,18 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
} }
binding.searchFilterApply.setOnClickListener { binding.searchFilterApply.setOnClickListener {
activity.result.apply { activity.aniMangaResult.apply {
status = binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null } status = binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null }
source = binding.searchSource.text.toString().replace(" ", "_").ifBlank { null } source = binding.searchSource.text.toString().replace(" ", "_").ifBlank { null }
format = binding.searchFormat.text.toString().ifBlank { null } format = binding.searchFormat.text.toString().ifBlank { null }
season = binding.searchSeason.text.toString().ifBlank { null } season = binding.searchSeason.text.toString().ifBlank { null }
if (activity.result.type == "ANIME") { if (activity.aniMangaResult.type == "ANIME") {
seasonYear = binding.searchYear.text.toString().toIntOrNull() seasonYear = binding.searchYear.text.toString().toIntOrNull()
} else { } else {
startYear = binding.searchYear.text.toString().toIntOrNull() startYear = binding.searchYear.text.toString().toIntOrNull()
} }
sort = activity.result.sort sort = activity.aniMangaResult.sort
countryOfOrigin = activity.result.countryOfOrigin countryOfOrigin = activity.aniMangaResult.countryOfOrigin
genres = selectedGenres genres = selectedGenres
tags = selectedTags tags = selectedTags
excludedGenres = exGenres excludedGenres = exGenres
@@ -266,8 +266,8 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
dismiss() dismiss()
} }
val format = val format =
if (activity.result.type == "ANIME") Anilist.animeStatus else Anilist.mangaStatus if (activity.aniMangaResult.type == "ANIME") Anilist.animeStatus else Anilist.mangaStatus
binding.searchStatus.setText(activity.result.status?.replace("_", " ")) binding.searchStatus.setText(activity.aniMangaResult.status?.replace("_", " "))
binding.searchStatus.setAdapter( binding.searchStatus.setAdapter(
ArrayAdapter( ArrayAdapter(
binding.root.context, binding.root.context,
@@ -276,7 +276,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
) )
) )
binding.searchSource.setText(activity.result.source?.replace("_", " ")) binding.searchSource.setText(activity.aniMangaResult.source?.replace("_", " "))
binding.searchSource.setAdapter( binding.searchSource.setAdapter(
ArrayAdapter( ArrayAdapter(
binding.root.context, binding.root.context,
@@ -285,19 +285,19 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
) )
) )
binding.searchFormat.setText(activity.result.format) binding.searchFormat.setText(activity.aniMangaResult.format)
binding.searchFormat.setAdapter( binding.searchFormat.setAdapter(
ArrayAdapter( ArrayAdapter(
binding.root.context, binding.root.context,
R.layout.item_dropdown, R.layout.item_dropdown,
(if (activity.result.type == "ANIME") Anilist.animeFormats else Anilist.mangaFormats).toTypedArray() (if (activity.aniMangaResult.type == "ANIME") Anilist.animeFormats else Anilist.mangaFormats).toTypedArray()
) )
) )
if (activity.result.type == "ANIME") { if (activity.aniMangaResult.type == "ANIME") {
binding.searchYear.setText(activity.result.seasonYear?.toString()) binding.searchYear.setText(activity.aniMangaResult.seasonYear?.toString())
} else { } else {
binding.searchYear.setText(activity.result.startYear?.toString()) binding.searchYear.setText(activity.aniMangaResult.startYear?.toString())
} }
binding.searchYear.setAdapter( binding.searchYear.setAdapter(
ArrayAdapter( ArrayAdapter(
@@ -308,9 +308,9 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
) )
) )
if (activity.result.type == "MANGA") binding.searchSeasonCont.visibility = GONE if (activity.aniMangaResult.type == "MANGA") binding.searchSeasonCont.visibility = GONE
else { else {
binding.searchSeason.setText(activity.result.season) binding.searchSeason.setText(activity.aniMangaResult.season)
binding.searchSeason.setAdapter( binding.searchSeason.setAdapter(
ArrayAdapter( ArrayAdapter(
binding.root.context, binding.root.context,
@@ -346,7 +346,9 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
binding.searchGenresGrid.isChecked = false binding.searchGenresGrid.isChecked = false
binding.searchFilterTags.adapter = binding.searchFilterTags.adapter =
FilterChipAdapter(Anilist.tags?.get(activity.result.isAdult) ?: listOf()) { chip -> FilterChipAdapter(
Anilist.tags?.get(activity.aniMangaResult.isAdult) ?: listOf()
) { chip ->
val tag = chip.text.toString() val tag = chip.text.toString()
chip.isChecked = selectedTags.contains(tag) chip.isChecked = selectedTags.contains(tag)
chip.isCloseIconVisible = exTags.contains(tag) chip.isCloseIconVisible = exTags.contains(tag)

View File

@@ -7,48 +7,75 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.anilist.AnilistSearch.SearchType
import ani.dantotsu.databinding.ItemSearchHistoryBinding import ani.dantotsu.databinding.ItemSearchHistoryBinding
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefManager.asLiveStringSet import ani.dantotsu.settings.saving.PrefManager.asLiveClass
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.settings.saving.SharedPreferenceStringSetLiveData import ani.dantotsu.settings.saving.SharedPreferenceClassLiveData
import java.util.Locale import java.io.Serializable
class SearchHistoryAdapter(private val type: String, private val searchClicked: (String) -> Unit) : data class SearchHistory(val search: String, val time: Long) : Serializable {
companion object {
private const val serialVersionUID = 1L
}
}
class SearchHistoryAdapter(type: SearchType, private val searchClicked: (String) -> Unit) :
ListAdapter<String, SearchHistoryAdapter.SearchHistoryViewHolder>( ListAdapter<String, SearchHistoryAdapter.SearchHistoryViewHolder>(
DIFF_CALLBACK_INSTALLED DIFF_CALLBACK_INSTALLED
) { ) {
private var searchHistoryLiveData: SharedPreferenceStringSetLiveData? = null private var searchHistoryLiveData: SharedPreferenceClassLiveData<List<SearchHistory>>? = null
private var searchHistory: MutableSet<String>? = null private var searchHistory: MutableList<SearchHistory>? = null
private var historyType: PrefName = when (type.lowercase(Locale.ROOT)) { private var historyType: PrefName = when (type) {
"anime" -> PrefName.AnimeSearchHistory SearchType.ANIME -> PrefName.SortedAnimeSH
"manga" -> PrefName.MangaSearchHistory SearchType.MANGA -> PrefName.SortedMangaSH
else -> throw IllegalArgumentException("Invalid type") SearchType.CHARACTER -> PrefName.SortedCharacterSH
SearchType.STAFF -> PrefName.SortedStaffSH
SearchType.STUDIO -> PrefName.SortedStudioSH
SearchType.USER -> PrefName.SortedUserSH
} }
private fun MutableList<SearchHistory>?.sorted(): List<String>? =
this?.sortedByDescending { it.time }?.map { it.search }
init { init {
searchHistoryLiveData = searchHistoryLiveData =
PrefManager.getLiveVal(historyType, mutableSetOf<String>()).asLiveStringSet() PrefManager.getLiveVal(historyType, mutableListOf<SearchHistory>()).asLiveClass()
searchHistoryLiveData?.observeForever { searchHistoryLiveData?.observeForever { data ->
searchHistory = it.toMutableSet() searchHistory = data.toMutableList()
submitList(searchHistory?.toList()) submitList(searchHistory?.sorted())
} }
} }
fun remove(item: String) { fun remove(item: String) {
searchHistory?.remove(item) searchHistory?.let { list ->
list.removeAll { it.search == item }
}
PrefManager.setVal(historyType, searchHistory) PrefManager.setVal(historyType, searchHistory)
submitList(searchHistory?.toList()) submitList(searchHistory?.sorted())
} }
fun add(item: String) { fun add(item: String) {
if (searchHistory?.contains(item) == true || item.isBlank()) return val maxSize = 25
if (searchHistory?.any { it.search == item } == true || item.isBlank()) return
if (PrefManager.getVal(PrefName.Incognito)) return if (PrefManager.getVal(PrefName.Incognito)) return
searchHistory?.add(item) searchHistory?.add(SearchHistory(item, System.currentTimeMillis()))
submitList(searchHistory?.toList()) if ((searchHistory?.size ?: 0) > maxSize) {
searchHistory?.removeAt(
searchHistory?.sorted()?.lastIndex ?: 0
)
}
submitList(searchHistory?.sorted())
PrefManager.setVal(historyType, searchHistory) PrefManager.setVal(historyType, searchHistory)
} }
fun clearHistory() {
searchHistory?.clear()
PrefManager.setVal(historyType, searchHistory)
submitList(searchHistory?.sorted())
}
override fun onCreateViewHolder( override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
viewType: Int viewType: Int

View File

@@ -5,5 +5,8 @@ import java.io.Serializable
data class Studio( data class Studio(
val id: String, val id: String,
val name: String, val name: String,
val isFavourite: Boolean?,
val favourites: Int?,
val imageUrl: String?,
var yearMedia: MutableMap<String, ArrayList<Media>>? = null var yearMedia: MutableMap<String, ArrayList<Media>>? = null
) : Serializable ) : Serializable

View File

@@ -0,0 +1,65 @@
package ani.dantotsu.media
import android.app.Activity
import android.content.Intent
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
import androidx.core.util.Pair
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.copyToClipboard
import ani.dantotsu.databinding.ItemCharacterBinding
import ani.dantotsu.loadImage
import ani.dantotsu.setAnimation
import java.io.Serializable
class StudioAdapter(
private val studioList: MutableList<Studio>
) : RecyclerView.Adapter<StudioAdapter.StudioViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StudioViewHolder {
val binding =
ItemCharacterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return StudioViewHolder(binding)
}
override fun onBindViewHolder(holder: StudioViewHolder, position: Int) {
val binding = holder.binding
setAnimation(binding.root.context, holder.binding.root)
val studio = studioList.getOrNull(position) ?: return
binding.itemCompactRelation.isVisible = false
binding.itemCompactImage.loadImage(studio.imageUrl)
binding.itemCompactTitle.text = studio.name
}
override fun getItemCount(): Int = studioList.size
inner class StudioViewHolder(val binding: ItemCharacterBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
itemView.setOnClickListener {
val studio = studioList[bindingAdapterPosition]
ContextCompat.startActivity(
itemView.context,
Intent(
itemView.context,
StudioActivity::class.java
).putExtra("studio", studio as Serializable),
ActivityOptionsCompat.makeSceneTransitionAnimation(
itemView.context as Activity,
Pair.create(
binding.itemCompactImage,
ViewCompat.getTransitionName(binding.itemCompactImage)!!
),
).toBundle()
)
}
itemView.setOnLongClickListener {
copyToClipboard(
studioList[bindingAdapterPosition].name
); true
}
}
}
}

View File

@@ -1,6 +1,8 @@
package ani.dantotsu.media package ani.dantotsu.media
import android.content.Context import android.content.Context
import androidx.core.net.toFile
import androidx.core.net.toUri
import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.parsers.SubtitleType import ani.dantotsu.parsers.SubtitleType
@@ -21,28 +23,32 @@ class SubtitleDownloader {
suspend fun loadSubtitleType(url: String): SubtitleType = suspend fun loadSubtitleType(url: String): SubtitleType =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
return@withContext try { return@withContext try {
// Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it if (!url.startsWith("file")) {
val networkHelper = Injekt.get<NetworkHelper>() // Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it
val request = Request.Builder() val networkHelper = Injekt.get<NetworkHelper>()
.url(url) val request = Request.Builder()
.build() .url(url)
.build()
val response = networkHelper.client.newCall(request).execute() val response = networkHelper.client.newCall(request).execute()
// Check if response is successful // Check if response is successful
if (response.isSuccessful) { if (response.isSuccessful) {
val responseBody = response.body.string() val responseBody = response.body.string()
val subtitleType = when { val subtitleType = getType(responseBody)
responseBody.contains("[Script Info]") -> SubtitleType.ASS
responseBody.contains("WEBVTT") -> SubtitleType.VTT subtitleType
else -> SubtitleType.SRT } else {
SubtitleType.UNKNOWN
} }
subtitleType
} else { } else {
SubtitleType.UNKNOWN val uri = url.toUri()
val file = uri.toFile()
val fileBody = file.readText()
val subtitleType = getType(fileBody)
subtitleType
} }
} catch (e: Exception) { } catch (e: Exception) {
Logger.log(e) Logger.log(e)
@@ -50,6 +56,15 @@ class SubtitleDownloader {
} }
} }
private fun getType(content: String): SubtitleType {
return when {
content.contains("[Script Info]") -> SubtitleType.ASS
content.contains("WEBVTT") -> SubtitleType.VTT
content.contains("SRT") -> SubtitleType.SRT
else -> SubtitleType.UNKNOWN
}
}
//actually downloads lol //actually downloads lol
@Deprecated("handled externally") @Deprecated("handled externally")
suspend fun downloadSubtitle( suspend fun downloadSubtitle(
@@ -72,14 +87,14 @@ class SubtitleDownloader {
val client = Injekt.get<NetworkHelper>().client val client = Injekt.get<NetworkHelper>().client
val request = Request.Builder().url(url).build() val request = Request.Builder().url(url).build()
val reponse = client.newCall(request).execute() val response = client.newCall(request).execute()
if (!reponse.isSuccessful) { if (!response.isSuccessful) {
snackString("Failed to download subtitle") snackString("Failed to download subtitle")
return return
} }
reponse.body.byteStream().use { input -> response.body.byteStream().use { input ->
subtitleFile.openOutputStream(context, false).use { output -> subtitleFile.openOutputStream(context, false).use { output ->
if (output == null) throw Exception("Could not open output stream") if (output == null) throw Exception("Could not open output stream")
input.copyTo(output) input.copyTo(output)

View File

@@ -0,0 +1,143 @@
package ani.dantotsu.media
import android.annotation.SuppressLint
import android.graphics.drawable.Drawable
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.App.Companion.context
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.AnilistSearch.SearchType
import ani.dantotsu.connections.anilist.AnilistSearch.SearchType.Companion.toAnilistString
import ani.dantotsu.connections.anilist.SearchResults
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class SupportingSearchAdapter(private val activity: SearchActivity, private val type: SearchType) :
HeaderInterface() {
@SuppressLint("ClickableViewAccessibility")
override fun onBindViewHolder(holder: SearchHeaderViewHolder, position: Int) {
binding = holder.binding
searchHistoryAdapter = SearchHistoryAdapter(type) {
binding.searchBarText.setText(it)
binding.searchBarText.setSelection(it.length)
}
binding.searchHistoryList.layoutManager = LinearLayoutManager(binding.root.context)
binding.searchHistoryList.adapter = searchHistoryAdapter
val imm: InputMethodManager =
activity.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
if (activity.searchType == SearchType.MANGA || activity.searchType == SearchType.ANIME) {
throw IllegalArgumentException("Invalid search type (wrong adapter)")
}
binding.searchByImage.visibility = View.GONE
binding.searchResultGrid.visibility = View.GONE
binding.searchResultList.visibility = View.GONE
binding.searchFilter.visibility = View.GONE
binding.searchAdultCheck.visibility = View.GONE
binding.searchList.visibility = View.GONE
binding.searchChipRecycler.visibility = View.GONE
binding.searchBar.hint = activity.searchType.toAnilistString()
if (PrefManager.getVal(PrefName.Incognito)) {
val startIconDrawableRes = R.drawable.ic_incognito_24
val startIconDrawable: Drawable? =
context?.let { AppCompatResources.getDrawable(it, startIconDrawableRes) }
binding.searchBar.startIconDrawable = startIconDrawable
}
binding.searchBarText.removeTextChangedListener(textWatcher)
when (type) {
SearchType.CHARACTER -> {
binding.searchBarText.setText(activity.characterResult.search)
}
SearchType.STUDIO -> {
binding.searchBarText.setText(activity.studioResult.search)
}
SearchType.STAFF -> {
binding.searchBarText.setText(activity.staffResult.search)
}
SearchType.USER -> {
binding.searchBarText.setText(activity.userResult.search)
}
else -> throw IllegalArgumentException("Invalid search type")
}
binding.clearHistory.setOnClickListener {
it.startAnimation(fadeOutAnimation())
it.visibility = View.GONE
searchHistoryAdapter.clearHistory()
}
updateClearHistoryVisibility()
fun searchTitle() {
val searchText = binding.searchBarText.text.toString().takeIf { it.isNotEmpty() }
val result: SearchResults<*> = when (type) {
SearchType.CHARACTER -> activity.characterResult
SearchType.STUDIO -> activity.studioResult
SearchType.STAFF -> activity.staffResult
SearchType.USER -> activity.userResult
else -> throw IllegalArgumentException("Invalid search type")
}
result.search = searchText
activity.search()
}
textWatcher = object : TextWatcher {
override fun afterTextChanged(s: Editable) {}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
if (s.toString().isBlank()) {
activity.emptyMediaAdapter()
CoroutineScope(Dispatchers.IO).launch {
delay(200)
activity.runOnUiThread {
setHistoryVisibility(true)
}
}
} else {
setHistoryVisibility(false)
searchTitle()
}
}
}
binding.searchBarText.addTextChangedListener(textWatcher)
binding.searchBarText.setOnEditorActionListener { _, actionId, _ ->
return@setOnEditorActionListener when (actionId) {
EditorInfo.IME_ACTION_SEARCH -> {
searchTitle()
binding.searchBarText.clearFocus()
imm.hideSoftInputFromWindow(binding.searchBarText.windowToken, 0)
true
}
else -> false
}
}
binding.searchBar.setEndIconOnClickListener { searchTitle() }
search = Runnable { searchTitle() }
requestFocus = Runnable { binding.searchBarText.requestFocus() }
}
}

View File

@@ -26,4 +26,5 @@ data class Anime(
var slug: String? = null, var slug: String? = null,
var kitsuEpisodes: Map<String, Episode>? = null, var kitsuEpisodes: Map<String, Episode>? = null,
var fillerEpisodes: Map<String, Episode>? = null, var fillerEpisodes: Map<String, Episode>? = null,
var anifyEpisodes: Map<String, Episode>? = null,
) : Serializable ) : Serializable

View File

@@ -8,8 +8,8 @@ import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getString
import androidx.core.content.ContextCompat.startActivity import androidx.core.content.ContextCompat.startActivity
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
@@ -18,9 +18,10 @@ import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.FileUrl import ani.dantotsu.FileUrl
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.currActivity import ani.dantotsu.currActivity
import ani.dantotsu.currContext
import ani.dantotsu.databinding.DialogLayoutBinding import ani.dantotsu.databinding.DialogLayoutBinding
import ani.dantotsu.databinding.ItemAnimeWatchBinding
import ani.dantotsu.databinding.ItemChipBinding import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.databinding.ItemMediaSourceBinding
import ani.dantotsu.displayTimer import ani.dantotsu.displayTimer
import ani.dantotsu.isOnline import ani.dantotsu.isOnline
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
@@ -33,12 +34,15 @@ import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.others.webview.CookieCatcher import ani.dantotsu.others.webview.CookieCatcher
import ani.dantotsu.parsers.AnimeSources import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.DynamicAnimeParser import ani.dantotsu.parsers.DynamicAnimeParser
import ani.dantotsu.parsers.OfflineAnimeParser
import ani.dantotsu.parsers.WatchSources import ani.dantotsu.parsers.WatchSources
import ani.dantotsu.px import ani.dantotsu.px
import ani.dantotsu.settings.FAQActivity import ani.dantotsu.settings.FAQActivity
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.toast import ani.dantotsu.toast
import ani.dantotsu.util.customAlertDialog
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_SUBSCRIPTION_CHECK import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_SUBSCRIPTION_CHECK
@@ -54,16 +58,14 @@ class AnimeWatchAdapter(
) : RecyclerView.Adapter<AnimeWatchAdapter.ViewHolder>() { ) : RecyclerView.Adapter<AnimeWatchAdapter.ViewHolder>() {
private var autoSelect = true private var autoSelect = true
var subscribe: MediaDetailsActivity.PopImageButton? = null var subscribe: MediaDetailsActivity.PopImageButton? = null
private var _binding: ItemAnimeWatchBinding? = null private var _binding: ItemMediaSourceBinding? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val bind = ItemAnimeWatchBinding.inflate(LayoutInflater.from(parent.context), parent, false) val bind =
ItemMediaSourceBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(bind) return ViewHolder(bind)
} }
private var nestedDialog: AlertDialog? = null
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding = holder.binding val binding = holder.binding
_binding = binding _binding = binding
@@ -75,7 +77,7 @@ class AnimeWatchAdapter(
null null
) )
} }
//Youtube // Youtube
if (media.anime?.youtube != null && PrefManager.getVal(PrefName.ShowYtButton)) { if (media.anime?.youtube != null && PrefManager.getVal(PrefName.ShowYtButton)) {
binding.animeSourceYT.visibility = View.VISIBLE binding.animeSourceYT.visibility = View.VISIBLE
binding.animeSourceYT.setOnClickListener { binding.animeSourceYT.setOnClickListener {
@@ -89,7 +91,7 @@ class AnimeWatchAdapter(
R.string.subbed R.string.subbed
) )
//PreferDub // PreferDub
var changing = false var changing = false
binding.animeSourceDubbed.setOnCheckedChangeListener { _, isChecked -> binding.animeSourceDubbed.setOnCheckedChangeListener { _, isChecked ->
binding.animeSourceDubbedText.text = binding.animeSourceDubbedText.text =
@@ -99,8 +101,8 @@ class AnimeWatchAdapter(
if (!changing) fragment.onDubClicked(isChecked) if (!changing) fragment.onDubClicked(isChecked)
} }
//Wrong Title // Wrong Title
binding.animeSourceSearch.setOnClickListener { binding.mediaSourceSearch.setOnClickListener {
SourceSearchDialogFragment().show( SourceSearchDialogFragment().show(
fragment.requireActivity().supportFragmentManager, fragment.requireActivity().supportFragmentManager,
null null
@@ -108,37 +110,37 @@ class AnimeWatchAdapter(
} }
val offline = !isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode) val offline = !isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode)
binding.animeSourceNameContainer.isGone = offline binding.mediaSourceNameContainer.isGone = offline
binding.animeSourceSettings.isGone = offline binding.mediaSourceSettings.isGone = offline
binding.animeSourceSearch.isGone = offline binding.mediaSourceSearch.isGone = offline
binding.animeSourceTitle.isGone = offline binding.mediaSourceTitle.isGone = offline
//Source Selection // Source Selection
var source = var source =
media.selected!!.sourceIndex.let { if (it >= watchSources.names.size) 0 else it } media.selected!!.sourceIndex.let { if (it >= watchSources.names.size) 0 else it }
setLanguageList(media.selected!!.langIndex, source) setLanguageList(media.selected!!.langIndex, source)
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) { if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
binding.animeSource.setText(watchSources.names[source]) binding.mediaSource.setText(watchSources.names[source])
watchSources[source].apply { watchSources[source].apply {
this.selectDub = media.selected!!.preferDub this.selectDub = media.selected!!.preferDub
binding.animeSourceTitle.text = showUserText binding.mediaSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } } showUserTextListener = { MainScope().launch { binding.mediaSourceTitle.text = it } }
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately() binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
} }
} }
binding.animeSource.setAdapter( binding.mediaSource.setAdapter(
ArrayAdapter( ArrayAdapter(
fragment.requireContext(), fragment.requireContext(),
R.layout.item_dropdown, R.layout.item_dropdown,
watchSources.names watchSources.names
) )
) )
binding.animeSourceTitle.isSelected = true binding.mediaSourceTitle.isSelected = true
binding.animeSource.setOnItemClickListener { _, _, i, _ -> binding.mediaSource.setOnItemClickListener { _, _, i, _ ->
fragment.onSourceChange(i).apply { fragment.onSourceChange(i).apply {
binding.animeSourceTitle.text = showUserText binding.mediaSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } } showUserTextListener = { MainScope().launch { binding.mediaSourceTitle.text = it } }
changing = true changing = true
binding.animeSourceDubbed.isChecked = selectDub binding.animeSourceDubbed.isChecked = selectDub
changing = false changing = false
@@ -150,15 +152,15 @@ class AnimeWatchAdapter(
fragment.loadEpisodes(i, false) fragment.loadEpisodes(i, false)
} }
binding.animeSourceLanguage.setOnItemClickListener { _, _, i, _ -> binding.mediaSourceLanguage.setOnItemClickListener { _, _, i, _ ->
// Check if 'extension' and 'selected' properties exist and are accessible // Check if 'extension' and 'selected' properties exist and are accessible
(watchSources[source] as? DynamicAnimeParser)?.let { ext -> (watchSources[source] as? DynamicAnimeParser)?.let { ext ->
ext.sourceLanguage = i ext.sourceLanguage = i
fragment.onLangChange(i) fragment.onLangChange(i)
fragment.onSourceChange(media.selected!!.sourceIndex).apply { fragment.onSourceChange(media.selected!!.sourceIndex).apply {
binding.animeSourceTitle.text = showUserText binding.mediaSourceTitle.text = showUserText
showUserTextListener = showUserTextListener =
{ MainScope().launch { binding.animeSourceTitle.text = it } } { MainScope().launch { binding.mediaSourceTitle.text = it } }
changing = true changing = true
binding.animeSourceDubbed.isChecked = selectDub binding.animeSourceDubbed.isChecked = selectDub
changing = false changing = false
@@ -170,19 +172,19 @@ class AnimeWatchAdapter(
} ?: run { } } ?: run { }
} }
//settings // Settings
binding.animeSourceSettings.setOnClickListener { binding.mediaSourceSettings.setOnClickListener {
(watchSources[source] as? DynamicAnimeParser)?.let { ext -> (watchSources[source] as? DynamicAnimeParser)?.let { ext ->
fragment.openSettings(ext.extension) fragment.openSettings(ext.extension)
} }
} }
//Icons // Icons
//subscribe // Subscribe
subscribe = MediaDetailsActivity.PopImageButton( subscribe = MediaDetailsActivity.PopImageButton(
fragment.lifecycleScope, fragment.lifecycleScope,
binding.animeSourceSubscribe, binding.mediaSourceSubscribe,
R.drawable.ic_round_notifications_active_24, R.drawable.ic_round_notifications_active_24,
R.drawable.ic_round_notifications_none_24, R.drawable.ic_round_notifications_none_24,
R.color.bg_opp, R.color.bg_opp,
@@ -190,125 +192,164 @@ class AnimeWatchAdapter(
fragment.subscribed, fragment.subscribed,
true true
) { ) {
fragment.onNotificationPressed(it, binding.animeSource.text.toString()) fragment.onNotificationPressed(it, binding.mediaSource.text.toString())
} }
subscribeButton(false) subscribeButton(false)
binding.animeSourceSubscribe.setOnLongClickListener { binding.mediaSourceSubscribe.setOnLongClickListener {
openSettings(fragment.requireContext(), CHANNEL_SUBSCRIPTION_CHECK) openSettings(fragment.requireContext(), CHANNEL_SUBSCRIPTION_CHECK)
} }
//Nested Button // Nested Button
binding.animeNestedButton.setOnClickListener { binding.mediaNestedButton.setOnClickListener {
val dialogView = val dialogBinding = DialogLayoutBinding.inflate(fragment.layoutInflater)
LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null) dialogBinding.apply {
val dialogBinding = DialogLayoutBinding.bind(dialogView) var refresh = false
var refresh = false var run = false
var run = false var reversed = media.selected!!.recyclerReversed
var reversed = media.selected!!.recyclerReversed var style =
var style = media.selected!!.recyclerStyle ?: PrefManager.getVal(PrefName.AnimeDefaultView)
media.selected!!.recyclerStyle ?: PrefManager.getVal(PrefName.AnimeDefaultView)
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f mediaSourceTop.rotation = if (reversed) -90f else 90f
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down" sortText.text = if (reversed) "Down to Up" else "Up to Down"
dialogBinding.animeSourceTop.setOnClickListener { mediaSourceTop.setOnClickListener {
reversed = !reversed reversed = !reversed
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f mediaSourceTop.rotation = if (reversed) -90f else 90f
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down" sortText.text = if (reversed) "Down to Up" else "Up to Down"
run = true run = true
}
//Grids
var selected = when (style) {
0 -> dialogBinding.animeSourceList
1 -> dialogBinding.animeSourceGrid
2 -> dialogBinding.animeSourceCompact
else -> dialogBinding.animeSourceList
}
when (style) {
0 -> dialogBinding.layoutText.setText(R.string.list)
1 -> dialogBinding.layoutText.setText(R.string.grid)
2 -> dialogBinding.layoutText.setText(R.string.compact)
else -> dialogBinding.animeSourceList
}
selected.alpha = 1f
fun selected(it: ImageButton) {
selected.alpha = 0.33f
selected = it
selected.alpha = 1f
}
dialogBinding.animeSourceList.setOnClickListener {
selected(it as ImageButton)
style = 0
dialogBinding.layoutText.setText(R.string.list)
run = true
}
dialogBinding.animeSourceGrid.setOnClickListener {
selected(it as ImageButton)
style = 1
dialogBinding.layoutText.setText(R.string.grid)
run = true
}
dialogBinding.animeSourceCompact.setOnClickListener {
selected(it as ImageButton)
style = 2
dialogBinding.layoutText.setText(R.string.compact)
run = true
}
dialogBinding.animeWebviewContainer.setOnClickListener {
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
toast(R.string.webview_not_installed)
} }
//start CookieCatcher activity // Grids
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) { var selected = when (style) {
val sourceAHH = watchSources[source] as? DynamicAnimeParser 0 -> mediaSourceList
val sourceHttp = 1 -> mediaSourceGrid
sourceAHH?.extension?.sources?.firstOrNull() as? AnimeHttpSource 2 -> mediaSourceCompact
val url = sourceHttp?.baseUrl else -> mediaSourceList
url?.let { }
refresh = true when (style) {
val headersMap = try { 0 -> layoutText.setText(R.string.list)
sourceHttp.headers.toMultimap() 1 -> layoutText.setText(R.string.grid)
.mapValues { it.value.getOrNull(0) ?: "" } 2 -> layoutText.setText(R.string.compact)
} catch (e: Exception) { else -> mediaSourceList
emptyMap() }
selected.alpha = 1f
fun selected(it: ImageButton) {
selected.alpha = 0.33f
selected = it
selected.alpha = 1f
}
mediaSourceList.setOnClickListener {
selected(it as ImageButton)
style = 0
layoutText.setText(R.string.list)
run = true
}
mediaSourceGrid.setOnClickListener {
selected(it as ImageButton)
style = 1
layoutText.setText(R.string.grid)
run = true
}
mediaSourceCompact.setOnClickListener {
selected(it as ImageButton)
style = 2
layoutText.setText(R.string.compact)
run = true
}
mediaWebviewContainer.setOnClickListener {
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
toast(R.string.webview_not_installed)
}
// Start CookieCatcher activity
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
val sourceAHH = watchSources[source] as? DynamicAnimeParser
val sourceHttp =
sourceAHH?.extension?.sources?.firstOrNull() as? AnimeHttpSource
val url = sourceHttp?.baseUrl
url?.let {
refresh = true
val headersMap = try {
sourceHttp.headers.toMultimap()
.mapValues { it.value.getOrNull(0) ?: "" }
} catch (e: Exception) {
emptyMap()
}
val intent =
Intent(fragment.requireContext(), CookieCatcher::class.java)
.putExtra("url", url)
.putExtra("headers", headersMap as HashMap<String, String>)
startActivity(fragment.requireContext(), intent, null)
} }
val intent = Intent(fragment.requireContext(), CookieCatcher::class.java)
.putExtra("url", url)
.putExtra("headers", headersMap as HashMap<String, String>)
startActivity(fragment.requireContext(), intent, null)
} }
} }
resetProgress.setOnClickListener {
fragment.requireContext().customAlertDialog().apply {
setTitle(" Delete Progress for all episodes of ${media.nameRomaji}")
setMessage("This will delete all the locally stored progress for all episodes")
setPosButton(R.string.ok) {
val prefix = "${media.id}_"
val regex = Regex("^${prefix}\\d+$")
PrefManager.getAllCustomValsForMedia(prefix)
.keys
.filter { it.matches(regex) }
.onEach { key -> PrefManager.removeCustomVal(key) }
snackString("Deleted the progress of all Episodes for ${media.nameRomaji}")
}
setNegButton(R.string.no)
show()
}
}
resetProgressDef.text = getString(currContext()!!, R.string.clear_stored_episode)
// Hidden
mangaScanlatorContainer.visibility = View.GONE
animeDownloadContainer.visibility = View.GONE
fragment.requireContext().customAlertDialog().apply {
setTitle("Options")
setCustomView(dialogBinding.root)
setPosButton("OK") {
if (run) fragment.onIconPressed(style, reversed)
if (refresh) fragment.loadEpisodes(source, true)
}
setNegButton("Cancel") {
if (refresh) fragment.loadEpisodes(source, true)
}
show()
}
} }
//hidden
dialogBinding.animeScanlatorContainer.visibility = View.GONE
dialogBinding.animeDownloadContainer.visibility = View.GONE
nestedDialog = AlertDialog.Builder(fragment.requireContext(), R.style.MyPopup)
.setTitle("Options")
.setView(dialogView)
.setPositiveButton("OK") { _, _ ->
if (run) fragment.onIconPressed(style, reversed)
if (refresh) fragment.loadEpisodes(source, true)
}
.setNegativeButton("Cancel") { _, _ ->
if (refresh) fragment.loadEpisodes(source, true)
}
.setOnCancelListener {
if (refresh) fragment.loadEpisodes(source, true)
}
.create()
nestedDialog?.show()
} }
//Episode Handling // Episode Handling
handleEpisodes() handleEpisodes()
//clear progress
binding.sourceTitle.setOnLongClickListener {
fragment.requireContext().customAlertDialog().apply {
setTitle(" Delete Progress for all episodes of ${media.nameRomaji}")
setMessage("This will delete all the locally stored progress for all episodes")
setPosButton(R.string.ok) {
val prefix = "${media.id}_"
val regex = Regex("^${prefix}\\d+$")
PrefManager.getAllCustomValsForMedia(prefix)
.keys
.filter { it.matches(regex) }
.onEach { key -> PrefManager.removeCustomVal(key) }
snackString("Deleted the progress of all Episodes for ${media.nameRomaji}")
}
setNegButton(R.string.no)
show()
}
true
}
} }
fun subscribeButton(enabled: Boolean) { fun subscribeButton(enabled: Boolean) {
subscribe?.enabled(enabled) subscribe?.enabled(enabled)
} }
//Chips // Chips
fun updateChips(limit: Int, names: Array<String>, arr: Array<Int>, selected: Int = 0) { fun updateChips(limit: Int, names: Array<String>, arr: Array<Int>, selected: Int = 0) {
val binding = _binding val binding = _binding
if (binding != null) { if (binding != null) {
@@ -319,13 +360,13 @@ class AnimeWatchAdapter(
val chip = val chip =
ItemChipBinding.inflate( ItemChipBinding.inflate(
LayoutInflater.from(fragment.context), LayoutInflater.from(fragment.context),
binding.animeSourceChipGroup, binding.mediaSourceChipGroup,
false false
).root ).root
chip.isCheckable = true chip.isCheckable = true
fun selected() { fun selected() {
chip.isChecked = true chip.isChecked = true
binding.animeWatchChipScroll.smoothScrollTo( binding.mediaWatchChipScroll.smoothScrollTo(
(chip.left - screenWidth / 2) + (chip.width / 2), (chip.left - screenWidth / 2) + (chip.width / 2),
0 0
) )
@@ -344,14 +385,14 @@ class AnimeWatchAdapter(
selected() selected()
fragment.onChipClicked(position, limit * (position), last - 1) fragment.onChipClicked(position, limit * (position), last - 1)
} }
binding.animeSourceChipGroup.addView(chip) binding.mediaSourceChipGroup.addView(chip)
if (selected == position) { if (selected == position) {
selected() selected()
select = chip select = chip
} }
} }
if (select != null) if (select != null)
binding.animeWatchChipScroll.apply { binding.mediaWatchChipScroll.apply {
post { post {
scrollTo( scrollTo(
(select.left - screenWidth / 2) + (select.width / 2), (select.left - screenWidth / 2) + (select.width / 2),
@@ -363,7 +404,7 @@ class AnimeWatchAdapter(
} }
fun clearChips() { fun clearChips() {
_binding?.animeSourceChipGroup?.removeAllViews() _binding?.mediaSourceChipGroup?.removeAllViews()
} }
fun handleEpisodes() { fun handleEpisodes() {
@@ -379,15 +420,15 @@ class AnimeWatchAdapter(
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString() var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
if (episodes.contains(continueEp)) { if (episodes.contains(continueEp)) {
binding.animeSourceContinue.visibility = View.VISIBLE binding.sourceContinue.visibility = View.VISIBLE
handleProgress( handleProgress(
binding.itemEpisodeProgressCont, binding.itemMediaProgressCont,
binding.itemEpisodeProgress, binding.itemMediaProgress,
binding.itemEpisodeProgressEmpty, binding.itemMediaProgressEmpty,
media.id, media.id,
continueEp continueEp
) )
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight > PrefManager.getVal<Float>( if ((binding.itemMediaProgress.layoutParams as LinearLayout.LayoutParams).weight > PrefManager.getVal<Float>(
PrefName.WatchPercentage PrefName.WatchPercentage
) )
) { ) {
@@ -395,9 +436,9 @@ class AnimeWatchAdapter(
if (e != -1 && e + 1 < episodes.size) { if (e != -1 && e + 1 < episodes.size) {
continueEp = episodes[e + 1] continueEp = episodes[e + 1]
handleProgress( handleProgress(
binding.itemEpisodeProgressCont, binding.itemMediaProgressCont,
binding.itemEpisodeProgress, binding.itemMediaProgress,
binding.itemEpisodeProgressEmpty, binding.itemMediaProgressEmpty,
media.id, media.id,
continueEp continueEp
) )
@@ -407,51 +448,65 @@ class AnimeWatchAdapter(
val cleanedTitle = ep.title?.let { MediaNameAdapter.removeEpisodeNumber(it) } val cleanedTitle = ep.title?.let { MediaNameAdapter.removeEpisodeNumber(it) }
binding.itemEpisodeImage.loadImage( binding.itemMediaImage.loadImage(
ep.thumb ?: FileUrl[media.banner ?: media.cover], 0 ep.thumb ?: FileUrl[media.banner ?: media.cover], 0
) )
if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE
binding.animeSourceContinueText.text = binding.mediaSourceContinueText.text =
currActivity()!!.getString( currActivity()!!.getString(
R.string.continue_episode, ep.number, if (ep.filler) R.string.continue_episode, ep.number, if (ep.filler)
currActivity()!!.getString(R.string.filler_tag) currActivity()!!.getString(R.string.filler_tag)
else else
"", cleanedTitle "", cleanedTitle
) )
binding.animeSourceContinue.setOnClickListener { binding.sourceContinue.setOnClickListener {
fragment.onEpisodeClick(continueEp) fragment.onEpisodeClick(continueEp)
} }
if (fragment.continueEp) { if (fragment.continueEp) {
if ( if (
(binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams) (binding.itemMediaProgress.layoutParams as LinearLayout.LayoutParams)
.weight < PrefManager.getVal<Float>(PrefName.WatchPercentage) .weight < PrefManager.getVal<Float>(PrefName.WatchPercentage)
) { ) {
binding.animeSourceContinue.performClick() binding.sourceContinue.performClick()
fragment.continueEp = false fragment.continueEp = false
} }
} }
} else { } else {
binding.animeSourceContinue.visibility = View.GONE binding.sourceContinue.visibility = View.GONE
} }
binding.animeSourceProgressBar.visibility = View.GONE binding.sourceProgressBar.visibility = View.GONE
val sourceFound = media.anime.episodes!!.isNotEmpty() val sourceFound = media.anime.episodes!!.isNotEmpty()
binding.animeSourceNotFound.isGone = sourceFound val isDownloadedSource =
watchSources[media.selected!!.sourceIndex] is OfflineAnimeParser
if (isDownloadedSource) {
binding.sourceNotFound.text = if (sourceFound) {
currActivity()!!.getString(R.string.source_not_found)
} else {
currActivity()!!.getString(R.string.download_not_found)
}
} else {
binding.sourceNotFound.text =
currActivity()!!.getString(R.string.source_not_found)
}
binding.sourceNotFound.isGone = sourceFound
binding.faqbutton.isGone = sourceFound binding.faqbutton.isGone = sourceFound
if (!sourceFound && PrefManager.getVal(PrefName.SearchSources) && autoSelect) { if (!sourceFound && PrefManager.getVal(PrefName.SearchSources) && autoSelect) {
if (binding.animeSource.adapter.count > media.selected!!.sourceIndex + 1) { if (binding.mediaSource.adapter.count > media.selected!!.sourceIndex + 1) {
val nextIndex = media.selected!!.sourceIndex + 1 val nextIndex = media.selected!!.sourceIndex + 1
binding.animeSource.setText( binding.mediaSource.setText(
binding.animeSource.adapter binding.mediaSource.adapter
.getItem(nextIndex).toString(), false .getItem(nextIndex).toString(), false
) )
fragment.onSourceChange(nextIndex).apply { fragment.onSourceChange(nextIndex).apply {
binding.animeSourceTitle.text = showUserText binding.mediaSourceTitle.text = showUserText
showUserTextListener = showUserTextListener =
{ MainScope().launch { binding.animeSourceTitle.text = it } } { MainScope().launch { binding.mediaSourceTitle.text = it } }
binding.animeSourceDubbed.isChecked = selectDub binding.animeSourceDubbed.isChecked = selectDub
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately() binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
setLanguageList(0, nextIndex) setLanguageList(0, nextIndex)
@@ -460,13 +515,13 @@ class AnimeWatchAdapter(
fragment.loadEpisodes(nextIndex, false) fragment.loadEpisodes(nextIndex, false)
} }
} }
binding.animeSource.setOnClickListener { autoSelect = false } binding.mediaSource.setOnClickListener { autoSelect = false }
} else { } else {
binding.animeSourceContinue.visibility = View.GONE binding.sourceContinue.visibility = View.GONE
binding.animeSourceNotFound.visibility = View.GONE binding.sourceNotFound.visibility = View.GONE
binding.faqbutton.visibility = View.GONE binding.faqbutton.visibility = View.GONE
clearChips() clearChips()
binding.animeSourceProgressBar.visibility = View.VISIBLE binding.sourceProgressBar.visibility = View.VISIBLE
} }
} }
} }
@@ -480,9 +535,9 @@ class AnimeWatchAdapter(
ext.sourceLanguage = lang ext.sourceLanguage = lang
} }
try { try {
binding?.animeSourceLanguage?.setText(parser.extension.sources[lang].lang) binding?.mediaSourceLanguage?.setText(parser.extension.sources[lang].lang)
} catch (e: IndexOutOfBoundsException) { } catch (e: IndexOutOfBoundsException) {
binding?.animeSourceLanguage?.setText( binding?.mediaSourceLanguage?.setText(
parser.extension.sources.firstOrNull()?.lang ?: "Unknown" parser.extension.sources.firstOrNull()?.lang ?: "Unknown"
) )
} }
@@ -493,9 +548,9 @@ class AnimeWatchAdapter(
) )
val items = adapter.count val items = adapter.count
binding?.animeSourceLanguageContainer?.visibility = binding?.mediaSourceLanguageContainer?.visibility =
if (items > 1) View.VISIBLE else View.GONE if (items > 1) View.VISIBLE else View.GONE
binding?.animeSourceLanguage?.setAdapter(adapter) binding?.mediaSourceLanguage?.setAdapter(adapter)
} }
} }
@@ -503,7 +558,7 @@ class AnimeWatchAdapter(
override fun getItemCount(): Int = 1 override fun getItemCount(): Int = 1
inner class ViewHolder(val binding: ItemAnimeWatchBinding) : inner class ViewHolder(val binding: ItemMediaSourceBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
init { init {
displayTimer(media, binding.animeSourceContainer) displayTimer(media, binding.animeSourceContainer)

View File

@@ -1,7 +1,6 @@
package ani.dantotsu.media.anime package ani.dantotsu.media.anime
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -28,10 +27,9 @@ import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.FileUrl
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.addons.download.DownloadAddonManager import ani.dantotsu.addons.download.DownloadAddonManager
import ani.dantotsu.databinding.FragmentAnimeWatchBinding import ani.dantotsu.databinding.FragmentMediaSourceBinding
import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.compareName import ani.dantotsu.download.DownloadsManager.Companion.compareName
@@ -61,6 +59,7 @@ import ani.dantotsu.toast
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import ani.dantotsu.util.StoragePermissions.Companion.accessAlertDialog import ani.dantotsu.util.StoragePermissions.Companion.accessAlertDialog
import ani.dantotsu.util.StoragePermissions.Companion.hasDirAccess import ani.dantotsu.util.StoragePermissions.Companion.hasDirAccess
import ani.dantotsu.util.customAlertDialog
import com.anggrayudi.storage.file.extension import com.anggrayudi.storage.file.extension
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
@@ -78,7 +77,7 @@ import kotlin.math.max
import kotlin.math.roundToInt import kotlin.math.roundToInt
class AnimeWatchFragment : Fragment() { class AnimeWatchFragment : Fragment() {
private var _binding: FragmentAnimeWatchBinding? = null private var _binding: FragmentMediaSourceBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private val model: MediaDetailsViewModel by activityViewModels() private val model: MediaDetailsViewModel by activityViewModels()
@@ -105,7 +104,7 @@ class AnimeWatchFragment : Fragment() {
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
_binding = FragmentAnimeWatchBinding.inflate(inflater, container, false) _binding = FragmentMediaSourceBinding.inflate(inflater, container, false)
return _binding?.root return _binding?.root
} }
@@ -126,7 +125,7 @@ class AnimeWatchFragment : Fragment() {
) )
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight) binding.mediaSourceRecycler.updatePadding(bottom = binding.mediaSourceRecycler.paddingBottom + navBarHeight)
screenWidth = resources.displayMetrics.widthPixels.dp screenWidth = resources.displayMetrics.widthPixels.dp
var maxGridSize = (screenWidth / 100f).roundToInt() var maxGridSize = (screenWidth / 100f).roundToInt()
@@ -150,13 +149,13 @@ class AnimeWatchFragment : Fragment() {
} }
} }
binding.animeSourceRecycler.layoutManager = gridLayoutManager binding.mediaSourceRecycler.layoutManager = gridLayoutManager
binding.ScrollTop.setOnClickListener { binding.ScrollTop.setOnClickListener {
binding.animeSourceRecycler.scrollToPosition(10) binding.mediaSourceRecycler.scrollToPosition(10)
binding.animeSourceRecycler.smoothScrollToPosition(0) binding.mediaSourceRecycler.smoothScrollToPosition(0)
} }
binding.animeSourceRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { binding.mediaSourceRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy) super.onScrolled(recyclerView, dx, dy)
@@ -170,7 +169,7 @@ class AnimeWatchFragment : Fragment() {
} }
}) })
model.scrolledToTop.observe(viewLifecycleOwner) { model.scrolledToTop.observe(viewLifecycleOwner) {
if (it) binding.animeSourceRecycler.scrollToPosition(0) if (it) binding.mediaSourceRecycler.scrollToPosition(0)
} }
continueEp = model.continueMedia ?: false continueEp = model.continueMedia ?: false
@@ -203,7 +202,7 @@ class AnimeWatchFragment : Fragment() {
offlineMode = offlineMode offlineMode = offlineMode
) )
binding.animeSourceRecycler.adapter = binding.mediaSourceRecycler.adapter =
ConcatAdapter(headerAdapter, episodeAdapter) ConcatAdapter(headerAdapter, episodeAdapter)
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
@@ -212,10 +211,11 @@ class AnimeWatchFragment : Fragment() {
if (offline) { if (offline) {
media.selected!!.sourceIndex = model.watchSources!!.list.lastIndex media.selected!!.sourceIndex = model.watchSources!!.list.lastIndex
} else { } else {
awaitAll( val kitsuEpisodes = async { model.loadKitsuEpisodes(media) }
async { model.loadKitsuEpisodes(media) }, val anifyEpisodes = async { model.loadAnifyEpisodes(media.id) }
async { model.loadFillerEpisodes(media) } val fillerEpisodes = async { model.loadFillerEpisodes(media) }
)
awaitAll(kitsuEpisodes, anifyEpisodes, fillerEpisodes)
} }
model.loadEpisodes(media, media.selected!!.sourceIndex) model.loadEpisodes(media, media.selected!!.sourceIndex)
} }
@@ -230,6 +230,21 @@ class AnimeWatchFragment : Fragment() {
val episodes = loadedEpisodes[media.selected!!.sourceIndex] val episodes = loadedEpisodes[media.selected!!.sourceIndex]
if (episodes != null) { if (episodes != null) {
episodes.forEach { (i, episode) -> episodes.forEach { (i, episode) ->
if (media.anime?.anifyEpisodes != null) {
if (media.anime!!.anifyEpisodes!!.containsKey(i)) {
episode.desc =
media.anime!!.anifyEpisodes!![i]?.desc ?: episode.desc
episode.title = if (MediaNameAdapter.removeEpisodeNumberCompletely(
episode.title ?: ""
).isBlank()
) media.anime!!.anifyEpisodes!![i]?.title
?: episode.title else episode.title
?: media.anime!!.anifyEpisodes!![i]?.title ?: episode.title
episode.thumb =
media.anime!!.anifyEpisodes!![i]?.thumb ?: episode.thumb
}
}
if (media.anime?.fillerEpisodes != null) { if (media.anime?.fillerEpisodes != null) {
if (media.anime!!.fillerEpisodes!!.containsKey(i)) { if (media.anime!!.fillerEpisodes!!.containsKey(i)) {
episode.title = episode.title =
@@ -247,14 +262,14 @@ class AnimeWatchFragment : Fragment() {
) media.anime!!.kitsuEpisodes!![i]?.title ) media.anime!!.kitsuEpisodes!![i]?.title
?: episode.title else episode.title ?: episode.title else episode.title
?: media.anime!!.kitsuEpisodes!![i]?.title ?: episode.title ?: media.anime!!.kitsuEpisodes!![i]?.title ?: episode.title
episode.thumb = media.anime!!.kitsuEpisodes!![i]?.thumb episode.thumb =
?: FileUrl[media.cover] media.anime!!.kitsuEpisodes!![i]?.thumb ?: episode.thumb
} }
} }
} }
media.anime?.episodes = episodes media.anime?.episodes = episodes
//CHIP GROUP // CHIP GROUP
val total = episodes.size val total = episodes.size
val divisions = total.toDouble() / 10 val divisions = total.toDouble() / 10
start = 0 start = 0
@@ -295,6 +310,10 @@ class AnimeWatchFragment : Fragment() {
if (i != null) if (i != null)
media.anime?.fillerEpisodes = i media.anime?.fillerEpisodes = i
} }
model.getAnifyEpisodes().observe(viewLifecycleOwner) { i ->
if (i != null)
media.anime?.anifyEpisodes = i
}
} }
fun onSourceChange(i: Int): AnimeParser { fun onSourceChange(i: Int): AnimeParser {
@@ -380,34 +399,33 @@ class AnimeWatchFragment : Fragment() {
if (allSettings.size > 1) { if (allSettings.size > 1) {
val names = val names =
allSettings.map { LanguageMapper.getLanguageName(it.lang) }.toTypedArray() allSettings.map { LanguageMapper.getLanguageName(it.lang) }.toTypedArray()
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup) requireContext()
.setTitle("Select a Source") .customAlertDialog()
.setSingleChoiceItems(names, -1) { dialog, which -> .apply {
selectedSetting = allSettings[which] setTitle("Select a Source")
itemSelected = true singleChoiceItems(names) { which ->
dialog.dismiss() selectedSetting = allSettings[which]
itemSelected = true
// Move the fragment transaction here requireActivity().runOnUiThread {
requireActivity().runOnUiThread { val fragment =
val fragment = AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) {
AnimeSourcePreferencesFragment().getInstance(selectedSetting.id) { changeUIVisibility(true)
changeUIVisibility(true) loadEpisodes(media.selected!!.sourceIndex, true)
loadEpisodes(media.selected!!.sourceIndex, true) }
} parentFragmentManager.beginTransaction()
parentFragmentManager.beginTransaction() .setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down) .replace(R.id.fragmentExtensionsContainer, fragment)
.replace(R.id.fragmentExtensionsContainer, fragment) .addToBackStack(null)
.addToBackStack(null) .commit()
.commit() }
} }
} onDismiss {
.setOnDismissListener { if (!itemSelected) {
if (!itemSelected) { changeUIVisibility(true)
changeUIVisibility(true) }
} }
show()
} }
.show()
dialog.window?.setDimAmount(0.8f)
} else { } else {
// If there's only one setting, proceed with the fragment transaction // If there's only one setting, proceed with the fragment transaction
requireActivity().runOnUiThread { requireActivity().runOnUiThread {
@@ -416,11 +434,12 @@ class AnimeWatchFragment : Fragment() {
changeUIVisibility(true) changeUIVisibility(true)
loadEpisodes(media.selected!!.sourceIndex, true) loadEpisodes(media.selected!!.sourceIndex, true)
} }
parentFragmentManager.beginTransaction() parentFragmentManager.beginTransaction().apply {
.setCustomAnimations(R.anim.slide_up, R.anim.slide_down) setCustomAnimations(R.anim.slide_up, R.anim.slide_down)
.replace(R.id.fragmentExtensionsContainer, fragment) replace(R.id.fragmentExtensionsContainer, fragment)
.addToBackStack(null) addToBackStack(null)
.commit() commit()
}
} }
} }
@@ -619,7 +638,7 @@ class AnimeWatchFragment : Fragment() {
private fun reload() { private fun reload() {
val selected = model.loadSelected(media) val selected = model.loadSelected(media)
//Find latest episode for subscription // Find latest episode for subscription
selected.latest = selected.latest =
media.anime?.episodes?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f media.anime?.episodes?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f
selected.latest = selected.latest =
@@ -663,14 +682,14 @@ class AnimeWatchFragment : Fragment() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
binding.mediaInfoProgressBar.visibility = progress binding.mediaInfoProgressBar.visibility = progress
binding.animeSourceRecycler.layoutManager?.onRestoreInstanceState(state) binding.mediaSourceRecycler.layoutManager?.onRestoreInstanceState(state)
requireActivity().setNavigationTheme() requireActivity().setNavigationTheme()
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState() state = binding.mediaSourceRecycler.layoutManager?.onSaveInstanceState()
} }
companion object { companion object {

View File

@@ -1,6 +1,5 @@
package ani.dantotsu.media.anime package ani.dantotsu.media.anime
import android.app.AlertDialog
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@@ -13,7 +12,6 @@ import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R import ani.dantotsu.R
import ani.dantotsu.connections.updateProgress import ani.dantotsu.connections.updateProgress
import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemEpisodeCompactBinding import ani.dantotsu.databinding.ItemEpisodeCompactBinding
import ani.dantotsu.databinding.ItemEpisodeGridBinding import ani.dantotsu.databinding.ItemEpisodeGridBinding
import ani.dantotsu.databinding.ItemEpisodeListBinding import ani.dantotsu.databinding.ItemEpisodeListBinding
@@ -23,6 +21,7 @@ import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.MediaType import ani.dantotsu.media.MediaType
import ani.dantotsu.setAnimation import ani.dantotsu.setAnimation
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.util.customAlertDialog
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.GlideUrl
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@@ -106,8 +105,8 @@ class EpisodeAdapter(
val thumb = val thumb =
ep.thumb?.let { if (it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null } ep.thumb?.let { if (it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null }
Glide.with(binding.itemEpisodeImage).load(thumb ?: media.cover).override(400, 0) Glide.with(binding.itemMediaImage).load(thumb ?: media.cover).override(400, 0)
.into(binding.itemEpisodeImage) .into(binding.itemMediaImage)
binding.itemEpisodeNumber.text = ep.number binding.itemEpisodeNumber.text = ep.number
binding.itemEpisodeTitle.text = if (ep.number == title) "Episode $title" else title binding.itemEpisodeTitle.text = if (ep.number == title) "Episode $title" else title
@@ -140,9 +139,9 @@ class EpisodeAdapter(
} }
handleProgress( handleProgress(
binding.itemEpisodeProgressCont, binding.itemMediaProgressCont,
binding.itemEpisodeProgress, binding.itemMediaProgress,
binding.itemEpisodeProgressEmpty, binding.itemMediaProgressEmpty,
media.id, media.id,
ep.number ep.number
) )
@@ -154,8 +153,8 @@ class EpisodeAdapter(
val thumb = val thumb =
ep.thumb?.let { if (it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null } ep.thumb?.let { if (it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null }
Glide.with(binding.itemEpisodeImage).load(thumb ?: media.cover).override(400, 0) Glide.with(binding.itemMediaImage).load(thumb ?: media.cover).override(400, 0)
.into(binding.itemEpisodeImage) .into(binding.itemMediaImage)
binding.itemEpisodeNumber.text = ep.number binding.itemEpisodeNumber.text = ep.number
binding.itemEpisodeTitle.text = title binding.itemEpisodeTitle.text = title
@@ -183,9 +182,9 @@ class EpisodeAdapter(
binding.itemEpisodeViewed.visibility = View.GONE binding.itemEpisodeViewed.visibility = View.GONE
} }
handleProgress( handleProgress(
binding.itemEpisodeProgressCont, binding.itemMediaProgressCont,
binding.itemEpisodeProgress, binding.itemMediaProgress,
binding.itemEpisodeProgressEmpty, binding.itemMediaProgressEmpty,
media.id, media.id,
ep.number ep.number
) )
@@ -208,9 +207,9 @@ class EpisodeAdapter(
} }
} }
handleProgress( handleProgress(
binding.itemEpisodeProgressCont, binding.itemMediaProgressCont,
binding.itemEpisodeProgress, binding.itemMediaProgress,
binding.itemEpisodeProgressEmpty, binding.itemMediaProgressEmpty,
media.id, media.id,
ep.number ep.number
) )
@@ -318,16 +317,14 @@ class EpisodeAdapter(
fragment.onAnimeEpisodeStopDownloadClick(episodeNumber) fragment.onAnimeEpisodeStopDownloadClick(episodeNumber)
return@setOnClickListener return@setOnClickListener
} else if (downloadedEpisodes.contains(episodeNumber)) { } else if (downloadedEpisodes.contains(episodeNumber)) {
val builder = AlertDialog.Builder(currContext(), R.style.MyPopup) binding.root.context.customAlertDialog().apply {
builder.setTitle("Delete Episode") setTitle("Delete Episode")
builder.setMessage("Are you sure you want to delete Episode ${episodeNumber}?") setMessage("Are you sure you want to delete Episode $episodeNumber?")
builder.setPositiveButton("Yes") { _, _ -> setPosButton(R.string.yes) {
fragment.onAnimeEpisodeRemoveDownloadClick(episodeNumber) fragment.onAnimeEpisodeRemoveDownloadClick(episodeNumber)
} }
builder.setNegativeButton("No") { _, _ -> setNegButton(R.string.no)
} }.show()
val dialog = builder.show()
dialog.window?.setDimAmount(0.8f)
return@setOnClickListener return@setOnClickListener
} else { } else {
fragment.onAnimeEpisodeDownloadClick(episodeNumber) fragment.onAnimeEpisodeDownloadClick(episodeNumber)

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,6 @@ package ani.dantotsu.media.anime
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.AlertDialog
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.ComponentName import android.content.ComponentName
import android.content.DialogInterface import android.content.DialogInterface
@@ -444,15 +443,12 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
if (subtitles.isNotEmpty()) { if (subtitles.isNotEmpty()) {
val subtitleNames = subtitles.map { it.language } val subtitleNames = subtitles.map { it.language }
var subtitleToDownload: Subtitle? = null var subtitleToDownload: Subtitle? = null
val alertDialog = AlertDialog.Builder(context, R.style.MyPopup) requireActivity().customAlertDialog().apply {
.setTitle(R.string.download_subtitle) setTitle(R.string.download_subtitle)
.setSingleChoiceItems( singleChoiceItems(subtitleNames.toTypedArray()) { which ->
subtitleNames.toTypedArray(),
-1
) { _, which ->
subtitleToDownload = subtitles[which] subtitleToDownload = subtitles[which]
} }
.setPositiveButton(R.string.download) { dialog, _ -> setPosButton(R.string.download) {
scope.launch { scope.launch {
if (subtitleToDownload != null) { if (subtitleToDownload != null) {
SubtitleDownloader.downloadSubtitle( SubtitleDownloader.downloadSubtitle(
@@ -466,13 +462,9 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
) )
} }
} }
dialog.dismiss()
} }
.setNegativeButton(R.string.cancel) { dialog, _ -> setNegButton(R.string.cancel) {}
dialog.dismiss() }.show()
}
.show()
alertDialog.window?.setDimAmount(0.8f)
} else { } else {
snackString(R.string.no_subtitles_available) snackString(R.string.no_subtitles_available)
} }
@@ -490,7 +482,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
) )
} else { } else {
val downloadAddonManager: DownloadAddonManager = Injekt.get() val downloadAddonManager: DownloadAddonManager = Injekt.get()
if (!downloadAddonManager.isAvailable()){ if (!downloadAddonManager.isAvailable()) {
val context = currContext() ?: requireContext() val context = currContext() ?: requireContext()
context.customAlertDialog().apply { context.customAlertDialog().apply {
setTitle(R.string.download_addon_not_installed) setTitle(R.string.download_addon_not_installed)
@@ -571,70 +563,73 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
snackString(R.string.no_video_selected) snackString(R.string.no_video_selected)
} }
} }
fun checkAudioTracks() { fun checkAudioTracks() {
val audioTracks = extractor.audioTracks.map { it.lang } val audioTracks = extractor.audioTracks.map { it.lang }
if (audioTracks.isNotEmpty()) { if (audioTracks.isNotEmpty()) {
val audioNamesArray = audioTracks.toTypedArray() val audioNamesArray = audioTracks.toTypedArray()
val checkedItems = BooleanArray(audioNamesArray.size) { false } val checkedItems = BooleanArray(audioNamesArray.size) { false }
val alertDialog = AlertDialog.Builder(currContext, R.style.MyPopup)
.setTitle(R.string.download_audio_tracks) currContext.customAlertDialog().apply { // ToTest
.setMultiChoiceItems(audioNamesArray, checkedItems) { _, which, isChecked -> setTitle(R.string.download_audio_tracks)
val audioPair = Pair(extractor.audioTracks[which].url, extractor.audioTracks[which].lang) multiChoiceItems(audioNamesArray, checkedItems) {
if (isChecked) { it.forEachIndexed { index, isChecked ->
selectedAudioTracks.add(audioPair) val audioPair = Pair(
} else { extractor.audioTracks[index].url,
selectedAudioTracks.remove(audioPair) extractor.audioTracks[index].lang
)
if (isChecked) {
selectedAudioTracks.add(audioPair)
} else {
selectedAudioTracks.remove(audioPair)
}
} }
} }
.setPositiveButton(R.string.download) { _, _ -> setPosButton(R.string.download) {
dialog?.dismiss()
go() go()
} }
.setNegativeButton(R.string.skip) { dialog, _ -> setNegButton(R.string.skip) {
selectedAudioTracks = mutableListOf() selectedAudioTracks = mutableListOf()
go() go()
dialog.dismiss()
} }
.setNeutralButton(R.string.cancel) { dialog, _ -> setNeutralButton(R.string.cancel) {
selectedAudioTracks = mutableListOf() selectedAudioTracks = mutableListOf()
dialog.dismiss()
} }
.show() show()
alertDialog.window?.setDimAmount(0.8f) }
} else { } else {
go() go()
} }
} }
if (subtitles.isNotEmpty()) { if (subtitles.isNotEmpty()) { // ToTest
val subtitleNamesArray = subtitleNames.toTypedArray() val subtitleNamesArray = subtitleNames.toTypedArray()
val checkedItems = BooleanArray(subtitleNamesArray.size) { false } val checkedItems = BooleanArray(subtitleNamesArray.size) { false }
val alertDialog = AlertDialog.Builder(currContext, R.style.MyPopup) currContext.customAlertDialog().apply {
.setTitle(R.string.download_subtitle) setTitle(R.string.download_subtitle)
.setMultiChoiceItems(subtitleNamesArray, checkedItems) { _, which, isChecked -> multiChoiceItems(subtitleNamesArray, checkedItems) {
val subtitlePair = Pair(subtitles[which].file.url, subtitles[which].language) it.forEachIndexed { index, isChecked ->
if (isChecked) { val subtitlePair =
selectedSubtitles.add(subtitlePair) Pair(subtitles[index].file.url, subtitles[index].language)
} else { if (isChecked) {
selectedSubtitles.remove(subtitlePair) selectedSubtitles.add(subtitlePair)
} else {
selectedSubtitles.remove(subtitlePair)
}
} }
} }
.setPositiveButton(R.string.download) { _, _ -> setPosButton(R.string.download) {
dialog?.dismiss()
checkAudioTracks() checkAudioTracks()
} }
.setNegativeButton(R.string.skip) { dialog, _ -> setNegButton(R.string.skip) {
selectedSubtitles = mutableListOf() selectedSubtitles = mutableListOf()
checkAudioTracks() checkAudioTracks()
dialog.dismiss()
} }
.setNeutralButton(R.string.cancel) { dialog, _ -> setNeutralButton(R.string.cancel) {
selectedSubtitles = mutableListOf() selectedSubtitles = mutableListOf()
dialog.dismiss()
} }
.show() show()
alertDialog.window?.setDimAmount(0.8f) }
} else { } else {
checkAudioTracks() checkAudioTracks()
} }

View File

@@ -63,8 +63,12 @@ class TrackGroupDialogFragment(
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) { override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
val binding = holder.binding val binding = holder.binding
trackGroups[position].let { trackGroup -> trackGroups[position].let { trackGroup ->
if (overrideTrackNames?.getOrNull(position - (trackGroups.size - (overrideTrackNames?.size?:0))) != null) { if (overrideTrackNames?.getOrNull(
val pair = overrideTrackNames!![position - (trackGroups.size - overrideTrackNames!!.size)] position - (trackGroups.size - (overrideTrackNames?.size ?: 0))
) != null
) {
val pair =
overrideTrackNames!![position - (trackGroups.size - overrideTrackNames!!.size)]
binding.subtitleTitle.text = binding.subtitleTitle.text =
"[${pair.second}] ${pair.first}" "[${pair.second}] ${pair.first}"
} else when (val language = trackGroup.getTrackFormat(0).language?.lowercase()) { } else when (val language = trackGroup.getTrackFormat(0).language?.lowercase()) {

View File

@@ -15,12 +15,12 @@ import ani.dantotsu.databinding.ItemCommentsBinding
import ani.dantotsu.getAppString import ani.dantotsu.getAppString
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
import ani.dantotsu.openImage import ani.dantotsu.openImage
import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.profile.ProfileActivity import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.setAnimation import ani.dantotsu.setAnimation
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.util.ColorEditor.Companion.adjustColorForContrast import ani.dantotsu.util.ColorEditor.Companion.adjustColorForContrast
import ani.dantotsu.util.ColorEditor.Companion.getContrastRatio import ani.dantotsu.util.ColorEditor.Companion.getContrastRatio
import ani.dantotsu.util.customAlertDialog
import com.xwray.groupie.GroupieAdapter import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.Section import com.xwray.groupie.Section
import com.xwray.groupie.viewbinding.BindableItem import com.xwray.groupie.viewbinding.BindableItem
@@ -385,19 +385,14 @@ class CommentItem(
* @param callback the callback to call when the user clicks yes * @param callback the callback to call when the user clicks yes
*/ */
private fun dialogBuilder(title: String, message: String, callback: () -> Unit) { private fun dialogBuilder(title: String, message: String, callback: () -> Unit) {
val alertDialog = commentsFragment.activity.customAlertDialog().apply {
android.app.AlertDialog.Builder(commentsFragment.activity, R.style.MyPopup) setTitle(title)
.setTitle(title) setMessage(message)
.setMessage(message) setPosButton("Yes") {
.setPositiveButton("Yes") { dialog, _ -> callback()
callback() }
dialog.dismiss() setNegButton("No") {}
} }.show()
.setNegativeButton("No") { dialog, _ ->
dialog.dismiss()
}
val dialog = alertDialog.show()
dialog?.window?.setDimAmount(0.8f)
} }
private val usernameColors: Array<String> = arrayOf( private val usernameColors: Array<String> = arrayOf(

View File

@@ -2,7 +2,6 @@ package ani.dantotsu.media.comments
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context.INPUT_METHOD_SERVICE import android.content.Context.INPUT_METHOD_SERVICE
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Bundle import android.os.Bundle
@@ -12,7 +11,6 @@ import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.animation.doOnEnd import androidx.core.animation.doOnEnd
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
@@ -25,6 +23,7 @@ import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.comments.Comment import ani.dantotsu.connections.comments.Comment
import ani.dantotsu.connections.comments.CommentResponse import ani.dantotsu.connections.comments.CommentResponse
import ani.dantotsu.connections.comments.CommentsAPI import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.databinding.DialogEdittextBinding
import ani.dantotsu.databinding.FragmentCommentsBinding import ani.dantotsu.databinding.FragmentCommentsBinding
import ani.dantotsu.loadImage import ani.dantotsu.loadImage
import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsActivity
@@ -32,8 +31,8 @@ import ani.dantotsu.setBaseline
import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString import ani.dantotsu.snackString
import ani.dantotsu.toast
import ani.dantotsu.util.Logger import ani.dantotsu.util.Logger
import ani.dantotsu.util.customAlertDialog
import com.xwray.groupie.GroupieAdapter import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.Section import com.xwray.groupie.Section
import io.noties.markwon.editor.MarkwonEditor import io.noties.markwon.editor.MarkwonEditor
@@ -160,35 +159,48 @@ class CommentsFragment : Fragment() {
popup.inflate(R.menu.comments_sort_menu) popup.inflate(R.menu.comments_sort_menu)
popup.show() popup.show()
} }
binding.openRules.setOnClickListener {
activity.customAlertDialog().apply {
setTitle("Commenting Rules")
.setMessage(
"🚨 BREAK ANY RULE = YOU'RE GONE\n\n" +
"1. NO RACISM, DISCRIMINATION, OR HATE SPEECH\n" +
"2. NO SPAMMING OR SELF-PROMOTION\n" +
"3. ABSOLUTELY NO NSFW CONTENT\n" +
"4. ENGLISH ONLY NO EXCEPTIONS\n" +
"5. NO IMPERSONATION, HARASSMENT, OR ABUSE\n" +
"6. NO ILLEGAL CONTENT OR EXTREME DISRESPECT TOWARDS ANY FANDOM\n" +
"7. DO NOT REQUEST OR SHARE REPOSITORIES/EXTENSIONS\n" +
"8. SPOILERS ALLOWED ONLY WITH SPOILER TAGS AND A WARNING\n" +
"9. NO SEXUALIZING OR INAPPROPRIATE COMMENTS ABOUT MINOR CHARACTERS\n" +
"10. IF IT'S WRONG, DON'T POST IT!\n\n"
)
setNegButton("I Understand") {}
show()
}
}
binding.commentFilter.setOnClickListener { binding.commentFilter.setOnClickListener {
val alertDialog = AlertDialog.Builder(activity, R.style.MyPopup) activity.customAlertDialog().apply {
.setTitle("Enter a chapter/episode number tag") val customView = DialogEdittextBinding.inflate(layoutInflater)
.setView(R.layout.dialog_edittext) setTitle("Enter a chapter/episode number tag")
.setPositiveButton("OK") { dialog, _ -> setCustomView(customView.root)
val editText = setPosButton("OK") {
(dialog as AlertDialog).findViewById<EditText>(R.id.dialogEditText) val text = customView.dialogEditText.text.toString()
val text = editText?.text.toString()
filterTag = text.toIntOrNull() filterTag = text.toIntOrNull()
lifecycleScope.launch { lifecycleScope.launch {
loadAndDisplayComments() loadAndDisplayComments()
} }
dialog.dismiss()
} }
.setNeutralButton("Clear") { dialog, _ -> setNeutralButton("Clear") {
filterTag = null filterTag = null
lifecycleScope.launch { lifecycleScope.launch {
loadAndDisplayComments() loadAndDisplayComments()
} }
dialog.dismiss()
} }
.setNegativeButton("Cancel") { dialog, _ -> setNegButton("Cancel") { filterTag = null }
filterTag = null show()
dialog.dismiss() }
}
val dialog = alertDialog.show()
dialog?.window?.setDimAmount(0.8f)
} }
var isFetching = false var isFetching = false
@@ -303,13 +315,12 @@ class CommentsFragment : Fragment() {
activity.binding.commentLabel.setOnClickListener { activity.binding.commentLabel.setOnClickListener {
//alert dialog to enter a number, with a cancel and ok button //alert dialog to enter a number, with a cancel and ok button
val alertDialog = AlertDialog.Builder(activity, R.style.MyPopup) activity.customAlertDialog().apply {
.setTitle("Enter a chapter/episode number tag") val customView = DialogEdittextBinding.inflate(layoutInflater)
.setView(R.layout.dialog_edittext) setTitle("Enter a chapter/episode number tag")
.setPositiveButton("OK") { dialog, _ -> setCustomView(customView.root)
val editText = setPosButton("OK") {
(dialog as AlertDialog).findViewById<EditText>(R.id.dialogEditText) val text = customView.dialogEditText.text.toString()
val text = editText?.text.toString()
tag = text.toIntOrNull() tag = text.toIntOrNull()
if (tag == null) { if (tag == null) {
activity.binding.commentLabel.background = ResourcesCompat.getDrawable( activity.binding.commentLabel.background = ResourcesCompat.getDrawable(
@@ -324,28 +335,25 @@ class CommentsFragment : Fragment() {
null null
) )
} }
dialog.dismiss()
} }
.setNeutralButton("Clear") { dialog, _ -> setNeutralButton("Clear") {
tag = null tag = null
activity.binding.commentLabel.background = ResourcesCompat.getDrawable( activity.binding.commentLabel.background = ResourcesCompat.getDrawable(
resources, resources,
R.drawable.ic_label_off_24, R.drawable.ic_label_off_24,
null null
) )
dialog.dismiss()
} }
.setNegativeButton("Cancel") { dialog, _ -> setNegButton("Cancel") {
tag = null tag = null
activity.binding.commentLabel.background = ResourcesCompat.getDrawable( activity.binding.commentLabel.background = ResourcesCompat.getDrawable(
resources, resources,
R.drawable.ic_label_off_24, R.drawable.ic_label_off_24,
null null
) )
dialog.dismiss()
} }
val dialog = alertDialog.show() show()
dialog?.window?.setDimAmount(0.8f) }
} }
} }
@@ -363,11 +371,6 @@ class CommentsFragment : Fragment() {
} }
} }
@SuppressLint("NotifyDataSetChanged")
override fun onStart() {
super.onStart()
}
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
@@ -579,31 +582,28 @@ class CommentsFragment : Fragment() {
* Called when the user tries to comment for the first time * Called when the user tries to comment for the first time
*/ */
private fun showCommentRulesDialog() { private fun showCommentRulesDialog() {
val alertDialog = AlertDialog.Builder(activity, R.style.MyPopup) activity.customAlertDialog().apply {
.setTitle("Commenting Rules") setTitle("Commenting Rules")
.setMessage( .setMessage(
"I WILL BAN YOU WITHOUT HESITATION\n" + "🚨 BREAK ANY RULE = YOU'RE GONE\n\n" +
"1. No racism\n" + "1. NO RACISM, DISCRIMINATION, OR HATE SPEECH\n" +
"2. No hate speech\n" + "2. NO SPAMMING OR SELF-PROMOTION\n" +
"3. No spam\n" + "3. ABSOLUTELY NO NSFW CONTENT\n" +
"4. No NSFW content\n" + "4. ENGLISH ONLY NO EXCEPTIONS\n" +
"6. ENGLISH ONLY\n" + "5. NO IMPERSONATION, HARASSMENT, OR ABUSE\n" +
"7. No self promotion\n" + "6. NO ILLEGAL CONTENT OR EXTREME DISRESPECT TOWARDS ANY FANDOM\n" +
"8. No impersonation\n" + "7. DO NOT REQUEST OR SHARE REPOSITORIES/EXTENSIONS\n" +
"9. No harassment\n" + "8. SPOILERS ALLOWED ONLY WITH SPOILER TAGS AND A WARNING\n" +
"10. No illegal content\n" + "9. NO SEXUALIZING OR INAPPROPRIATE COMMENTS ABOUT MINOR CHARACTERS\n" +
"11. Anything you know you shouldn't comment\n" "10. IF IT'S WRONG, DON'T POST IT!\n\n"
) )
.setPositiveButton("I Understand") { dialog, _ -> setPosButton("I Understand") {
dialog.dismiss()
PrefManager.setVal(PrefName.FirstComment, false) PrefManager.setVal(PrefName.FirstComment, false)
processComment() processComment()
} }
.setNegativeButton("Cancel") { dialog, _ -> setNegButton(R.string.cancel)
dialog.dismiss() show()
} }
val dialog = alertDialog.show()
dialog?.window?.setDimAmount(0.8f)
} }
private fun processComment() { private fun processComment() {

View File

@@ -5,7 +5,7 @@ import java.io.Serializable
data class Manga( data class Manga(
var totalChapters: Int? = null, var totalChapters: Int? = null,
var selectedChapter: String? = null, var selectedChapter: MangaChapter? = null,
var chapters: MutableMap<String, MangaChapter>? = null, var chapters: MutableMap<String, MangaChapter>? = null,
var slug: String? = null, var slug: String? = null,
var author: Author? = null, var author: Author? = null,

View File

@@ -16,6 +16,7 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.io.FileNotFoundException
import java.io.FileOutputStream import java.io.FileOutputStream
data class ImageData( data class ImageData(
@@ -76,7 +77,7 @@ fun saveImage(
uri?.let { uri?.let {
contentResolver.openOutputStream(it)?.use { os -> contentResolver.openOutputStream(it)?.use { os ->
bitmap.compress(format, quality, os) bitmap.compress(format, quality, os)
} } ?: throw FileNotFoundException("Failed to open output stream for URI: $uri")
} }
} else { } else {
val directory = val directory =
@@ -86,12 +87,20 @@ fun saveImage(
} }
val file = File(directory, filename) val file = File(directory, filename)
// Check if the file already exists
if (file.exists()) {
println("File already exists: ${file.absolutePath}")
return
}
FileOutputStream(file).use { outputStream -> FileOutputStream(file).use { outputStream ->
bitmap.compress(format, quality, outputStream) bitmap.compress(format, quality, outputStream)
} }
} }
} catch (e: FileNotFoundException) {
println("File not found: ${e.message}")
} catch (e: Exception) { } catch (e: Exception) {
// Handle exception here
println("Exception while saving image: ${e.message}") println("Exception while saving image: ${e.message}")
} }
} }

View File

@@ -40,4 +40,6 @@ data class MangaChapter(
private val dualPages = mutableListOf<Pair<MangaImage, MangaImage?>>() private val dualPages = mutableListOf<Pair<MangaImage, MangaImage?>>()
fun dualPages(): List<Pair<MangaImage, MangaImage?>> = dualPages fun dualPages(): List<Pair<MangaImage, MangaImage?>> = dualPages
fun uniqueNumber(): String = "${number}-${scanlator ?: "Unknown"}"
} }

View File

@@ -1,6 +1,5 @@
package ani.dantotsu.media.manga package ani.dantotsu.media.manga
import android.app.AlertDialog
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@@ -63,7 +62,7 @@ class MangaChapterAdapter(
init { init {
itemView.setOnClickListener { itemView.setOnClickListener {
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size) if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size)
fragment.onMangaChapterClick(arr[bindingAdapterPosition].number) fragment.onMangaChapterClick(arr[bindingAdapterPosition])
} }
} }
} }
@@ -74,7 +73,7 @@ class MangaChapterAdapter(
fun startDownload(chapterNumber: String) { fun startDownload(chapterNumber: String) {
activeDownloads.add(chapterNumber) activeDownloads.add(chapterNumber)
// Find the position of the chapter and notify only that item // Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == chapterNumber } val position = arr.indexOfFirst { it.uniqueNumber() == chapterNumber }
if (position != -1) { if (position != -1) {
notifyItemChanged(position) notifyItemChanged(position)
} }
@@ -84,17 +83,17 @@ class MangaChapterAdapter(
activeDownloads.remove(chapterNumber) activeDownloads.remove(chapterNumber)
downloadedChapters.add(chapterNumber) downloadedChapters.add(chapterNumber)
// Find the position of the chapter and notify only that item // Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == chapterNumber } val position = arr.indexOfFirst { it.uniqueNumber() == chapterNumber }
if (position != -1) { if (position != -1) {
arr[position].progress = "Downloaded" arr[position].progress = "Downloaded"
notifyItemChanged(position) notifyItemChanged(position)
} }
} }
fun deleteDownload(chapterNumber: String) { fun deleteDownload(chapterNumber: MangaChapter) {
downloadedChapters.remove(chapterNumber) downloadedChapters.remove(chapterNumber.uniqueNumber())
// Find the position of the chapter and notify only that item // Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == chapterNumber } val position = arr.indexOfFirst { it.uniqueNumber() == chapterNumber.uniqueNumber() }
if (position != -1) { if (position != -1) {
arr[position].progress = "" arr[position].progress = ""
notifyItemChanged(position) notifyItemChanged(position)
@@ -105,7 +104,7 @@ class MangaChapterAdapter(
activeDownloads.remove(chapterNumber) activeDownloads.remove(chapterNumber)
downloadedChapters.remove(chapterNumber) downloadedChapters.remove(chapterNumber)
// Find the position of the chapter and notify only that item // Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == chapterNumber } val position = arr.indexOfFirst { it.uniqueNumber() == chapterNumber }
if (position != -1) { if (position != -1) {
arr[position].progress = "" arr[position].progress = ""
notifyItemChanged(position) notifyItemChanged(position)
@@ -114,7 +113,7 @@ class MangaChapterAdapter(
fun updateDownloadProgress(chapterNumber: String, progress: Int) { fun updateDownloadProgress(chapterNumber: String, progress: Int) {
// Find the position of the chapter and notify only that item // Find the position of the chapter and notify only that item
val position = arr.indexOfFirst { it.number == chapterNumber } val position = arr.indexOfFirst { it.uniqueNumber() == chapterNumber }
if (position != -1) { if (position != -1) {
arr[position].progress = "Downloading: ${progress}%" arr[position].progress = "Downloading: ${progress}%"
@@ -127,7 +126,8 @@ class MangaChapterAdapter(
if (position < 0 || position >= arr.size) return if (position < 0 || position >= arr.size) return
for (i in 0..<n) { for (i in 0..<n) {
if (position + i < arr.size) { if (position + i < arr.size) {
val chapterNumber = arr[position + i].number val chapter = arr[position + i]
val chapterNumber = chapter.uniqueNumber()
if (activeDownloads.contains(chapterNumber)) { if (activeDownloads.contains(chapterNumber)) {
//do nothing //do nothing
continue continue
@@ -135,8 +135,8 @@ class MangaChapterAdapter(
//do nothing //do nothing
continue continue
} else { } else {
fragment.onMangaChapterDownloadClick(chapterNumber) fragment.onMangaChapterDownloadClick(chapter)
startDownload(chapterNumber) startDownload(chapter.uniqueNumber())
} }
} }
} }
@@ -201,28 +201,29 @@ class MangaChapterAdapter(
init { init {
itemView.setOnClickListener { itemView.setOnClickListener {
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size) if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size)
fragment.onMangaChapterClick(arr[bindingAdapterPosition].number) fragment.onMangaChapterClick(arr[bindingAdapterPosition])
} }
binding.itemDownload.setOnClickListener { binding.itemDownload.setOnClickListener {
if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size) { if (0 <= bindingAdapterPosition && bindingAdapterPosition < arr.size) {
val chapterNumber = arr[bindingAdapterPosition].number val chapter = arr[bindingAdapterPosition]
val chapterNumber = chapter.uniqueNumber()
if (activeDownloads.contains(chapterNumber)) { if (activeDownloads.contains(chapterNumber)) {
fragment.onMangaChapterStopDownloadClick(chapterNumber) fragment.onMangaChapterStopDownloadClick(chapter)
return@setOnClickListener return@setOnClickListener
} else if (downloadedChapters.contains(chapterNumber)) { } else if (downloadedChapters.contains(chapterNumber)) {
it.context.customAlertDialog().apply { it.context.customAlertDialog().apply {
setTitle("Delete Chapter") setTitle("Delete Chapter")
setMessage("Are you sure you want to delete ${chapterNumber}?") setMessage("Are you sure you want to delete ${chapterNumber}?")
setPosButton(R.string.delete) { setPosButton(R.string.delete) {
fragment.onMangaChapterRemoveDownloadClick(chapterNumber) fragment.onMangaChapterRemoveDownloadClick(chapter)
} }
setNegButton(R.string.cancel) setNegButton(R.string.cancel)
show() show()
} }
return@setOnClickListener return@setOnClickListener
} else { } else {
fragment.onMangaChapterDownloadClick(chapterNumber) fragment.onMangaChapterDownloadClick(chapter)
startDownload(chapterNumber) startDownload(chapter.uniqueNumber())
} }
} }
} }
@@ -277,7 +278,7 @@ class MangaChapterAdapter(
is ChapterListViewHolder -> { is ChapterListViewHolder -> {
val binding = holder.binding val binding = holder.binding
val ep = arr[position] val ep = arr[position]
holder.bind(ep.number, ep.progress) holder.bind(ep.uniqueNumber(), ep.progress)
setAnimation(fragment.requireContext(), holder.binding.root) setAnimation(fragment.requireContext(), holder.binding.root)
binding.itemChapterNumber.text = ep.number binding.itemChapterNumber.text = ep.number

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