Compare commits

...

169 Commits

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

* can't have two of the same buttons smh

* It was a misinput

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

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

* Bump dependencies

* Bump gradle to 8.7

* Bump gradle properties

* Add missing null safety

* Fix unresolved color

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

* Add animation to dialog

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

* Add string

* Who did this?

* Add listener

* Made NextLib Conditional

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

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

* Add Decoders to Builder

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

* feat:Added button to view rules

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

* Update CommentsFragment.kt

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

* one more thing to check

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

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

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

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

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

* feat(storyReply): fixed all markdowns
2024-06-01 08:27:11 -05:00
rebelonion
fdc1b31c44 Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-06-01 08:25:12 -05:00
rebelonion
1b4c8704ea fix: buffer manga image names 2024-06-01 08:25:08 -05:00
aayush262
5473ac8238 fix: some missing thumbnails 2024-05-30 23:11:34 +05:30
aayush262
e52ea2628a fix: fillers missing 2024-05-29 23:26:12 +05:30
aayush262
0f8218482a Merge remote-tracking branch 'origin/dev' into dev 2024-05-29 22:01:37 +05:30
aayush262
8822ef6805 feat: more thumbnails, descriptions (thanks to @yupcm) 2024-05-29 21:59:57 +05:30
aayush262
6ce41b8fbb fix: half cut text story (thanks to shivam) 2024-05-29 01:59:13 +05:30
aayush262
11655bd38d fix: hmm 2024-05-28 01:13:32 +05:30
aayush262
ea75197120 feat: Delete,edit activity 2024-05-28 01:04:07 +05:30
aayush262
6878d12b5c feat: more thumbnails 2024-05-27 23:09:19 +05:30
rebelonion
5800dcf3e7 fix: some download optimizations 2024-05-27 07:08:47 -05:00
rebelonion
b30047804a feat: setting to hide red dot 2024-05-27 05:58:51 -05:00
rebelonion
0b32636c1b Merge branch 'dev' of https://github.com/rebelonion/Dantotsu into dev 2024-05-27 05:11:48 -05:00
rebelonion
43e560a893 fix: Synchronized 2024-05-27 05:11:39 -05:00
aayush262
b256f02f14 chore: optimized feed activity 2024-05-26 23:49:19 +05:30
aayush262
46c17dced1 fix: small bug fix 2024-05-26 23:48:40 +05:30
aayush262
72fe910c59 fix: story scrolling issue 2024-05-26 21:43:40 +05:30
aayush262
fb65cb601e feat: remove other tabs when opening page from notification 2024-05-26 21:43:32 +05:30
aayush262
5a78d68f67 feat: remove other tabs when opening page from notification 2024-05-26 21:43:25 +05:30
aayush262
f205463a51 feat: refresh reply dialog after new message 2024-05-26 21:43:19 +05:30
aayush262
2de8ffd367 feat: optimize activity page 2024-05-26 21:43:12 +05:30
aayush262
f3f0daf7e7 Merge remote-tracking branch 'origin/dev' into dev 2024-05-26 00:41:28 +05:30
aayush262
2b4c9bf7a9 feat: notifications page rework 2024-05-26 00:40:46 +05:30
Sadwhy
21e25fe7a7 Fix commit messages in discord and telegram upload (#403)
It will fix it in its next build.
2024-05-25 22:13:17 +05:30
rebelonion
7b36cd0d29 fix: disallow screenshots in crash activity 2024-05-25 10:11:24 -05:00
rebelonion
ce488ea536 feat: biometric | etc 2024-05-25 10:08:11 -05:00
rebelonion
7717974b9e fix: what does the fix say? 🦊 2024-05-25 08:37:16 -05:00
rebelonion
37949c7e8e fix: move some stuffs around 2024-05-24 14:51:25 -05:00
rebelonion
e7a60e07d8 fix: destroyed activity crash on slower phones 2024-05-24 14:03:33 -05:00
rebelonion
7bce053202 fix: notification setting formatting 2024-05-24 14:02:36 -05:00
rebelonion
a5304477c7 fix: move try inside withContext 2024-05-24 13:34:39 -05:00
rebelonion
945018653e fix: some network stuff 2024-05-24 13:28:26 -05:00
rebelonion
5e38b00c1f fix: separate status query 2024-05-24 12:39:03 -05:00
rebel onion
dec990c24c Update README.md 2024-05-23 13:47:28 -05:00
275 changed files with 14060 additions and 6468 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
on:
push:
branches:
- dev
branches-ignore:
- main
- l10n_dev_crowdin
- custom-download-location
paths-ignore:
- '**/README.md'
tags:
- "v*.*.*"
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
env:
CI: true
SKIP_BUILD: false
steps:
- name: Checkout repo
@@ -19,14 +27,12 @@ jobs:
with:
fetch-depth: 0
- name: Download last SHA artifact
uses: dawidd6/action-download-artifact@v3
uses: dawidd6/action-download-artifact@v6
with:
workflow: beta.yml
name: last-sha
path: .
continue-on-error: true
- name: Get Commits Since Last Run
@@ -39,7 +45,9 @@ jobs:
fi
echo "Commits since $LAST_SHA:"
# 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
COMMIT_LOGS="${COMMIT_LOGS//'%'/'%25'}"
COMMIT_LOGS="${COMMIT_LOGS//$'\n'/'%0A'}"
@@ -49,6 +57,10 @@ jobs:
# Debugging: Print the variable to check its content
echo "$COMMIT_LOGS"
echo "$COMMIT_LOGS" > commit_log.txt
# Extract branch name from github.ref
BRANCH=${{ github.ref }}
BRANCH=${BRANCH#refs/heads/}
echo "BRANCH=${BRANCH}" >> $GITHUB_ENV
shell: /usr/bin/bash -e {0}
env:
CI: true
@@ -65,53 +77,278 @@ jobs:
echo "Version $VERSION"
echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: List files in the directory
run: ls -l
- name: Setup JDK 17
if: ${{ env.SKIP_BUILD != 'true' }}
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 17
cache: gradle
- name: Decode Keystore File
run: echo "${{ secrets.KEYSTORE_FILE }}" | base64 -d > $GITHUB_WORKSPACE/key.keystore
- name: List files in the directory
run: ls -l
- name: Make gradlew executable
run: chmod +x ./gradlew
- name: Build with Gradle
run: ./gradlew assembleGoogleAlpha -Pandroid.injected.signing.store.file=$GITHUB_WORKSPACE/key.keystore -Pandroid.injected.signing.store.password=${{ secrets.KEYSTORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.KEY_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.KEY_PASSWORD }}
- name: Decode Keystore File
if: ${{ github.repository == 'rebelonion/Dantotsu' }}
run: echo "${{ secrets.KEYSTORE_FILE }}" | base64 -d > $GITHUB_WORKSPACE/key.keystore
- name: Make gradlew executable
if: ${{ env.SKIP_BUILD != 'true' }}
run: chmod +x ./gradlew
- name: Build with Gradle
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
if: ${{ env.SKIP_BUILD != 'true' }}
uses: actions/upload-artifact@v4
with:
name: Dantotsu
retention-days: 5
compression-level: 9
path: "app/build/outputs/apk/google/alpha/app-google-alpha.apk"
- name: Upload APK to Discord and Telegram
if: ${{ github.repository == 'rebelonion/Dantotsu' }}
shell: bash
run: |
#Discord
# 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/')
# Truncate commit messages if they are too long
max_length=1900 # Adjust this value as needed
if [ ${#developers} -gt $max_length ]; then
developers="${developers:0:$max_length}... (truncated)"
fi
if [ ${#commit_messages} -gt $max_length ]; then
commit_messages="${commit_messages:0:$max_length}... (truncated)"
fi
contentbody=$( jq -nc --arg msg "Alpha-Build: <@&1225347048321191996> **$VERSION**:" --arg commits "$commit_messages" '{"content": ($msg + "\n" + $commits)}' )
curl -F "payload_json=${contentbody}" -F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" ${{ secrets.DISCORD_WEBHOOK }}
#Telegram
curl -F "chat_id=${{ secrets.TELEGRAM_CHANNEL_ID }}" \
-F "document=@app/build/outputs/apk/google/alpha/app-google-universal-alpha.apk" \
-F "caption=Alpha-Build: ${VERSION}: ${commit_messages}" \
https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument
# Construct Discord payload
discord_data=$(jq -nc \
--arg field_value "$commit_messages" \
--arg author_value "$developers" \
--arg footer_text "Version $VERSION" \
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" \
--arg thumbnail_url "$thumbnail_url" \
--arg embed_color "$embed_color" \
'{
"content": "<@&1225347048321191996>",
"embeds": [
{
"title": "New Alpha-Build dropped",
"color": $embed_color,
"fields": [
{
"name": "Commits:",
"value": $field_value,
"inline": true
},
{
"name": "Developers:",
"value": $author_value,
"inline": false
}
],
"footer": {
"text": $footer_text
},
"timestamp": $timestamp,
"thumbnail": {
"url": $thumbnail_url
}
}
],
"attachments": []
}')
echo "Debug: Final Discord payload:"
echo "$discord_data"
# Send Discord message
curl -H "Content-Type: application/json" \
-d "$discord_data" \
${{ secrets.DISCORD_WEBHOOK }}
echo "You have only send an embed to discord due to SKIP_BUILD being set to true"
# Upload APK to Discord
if [ "$SKIP_BUILD" != "true" ]; then
curl -F "payload_json=${contentbody}" \
-F "dantotsu_debug=@app/build/outputs/apk/google/alpha/app-google-alpha.apk" \
${{ secrets.DISCORD_WEBHOOK }}
else
echo "Skipping APK upload to Discord due to SKIP_BUILD being set to true"
fi
# Format commit messages for Telegram
telegram_commit_messages=$(echo "$COMMIT_LOG" | sed 's/%0A/\n/g' | while read -r line; do
message=$(echo "$line" | sed -E 's/● (.*) ~(.*) \[֍\]\((.*)\)/● \1 ~\2 <a href="\3">֍<\/a>/')
message=$(echo "$message" | sed -E 's/\[#([0-9]+)\]\((https:\/\/github\.com\/[^)]+)\)/<a href="\2">#\1<\/a>/g')
echo "$message"
done)
telegram_commit_messages="<blockquote>${telegram_commit_messages}</blockquote>"
# Configuring dev info
echo "$developers" > dev_info.txt
echo "$developers"
# making the file executable
chmod +x workflowscripts/tel_parser.sed
./workflowscripts/tel_parser.sed dev_info.txt >> output.txt
dev_info_tel=$(< output.txt)
telegram_dev_info="<blockquote>${dev_info_tel}</blockquote>"
echo "$telegram_dev_info"
# Upload APK to Telegram
if [ "$SKIP_BUILD" != "true" ]; then
APK_PATH="app/build/outputs/apk/google/alpha/app-google-alpha.apk"
response=$(curl -sS -f -X POST \
"https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendDocument" \
-F "chat_id=-1002117798698" \
-F "message_thread_id=7044" \
-F "document=@$APK_PATH" \
-F "caption=New Alpha-Build dropped 🔥
Commits:
${telegram_commit_messages}
Dev:
${telegram_dev_info}
version: ${VERSION}" \
-F "parse_mode=HTML")
else
echo "skipping because skip build set to true"
fi
env:
COMMIT_LOG: ${{ env.COMMIT_LOG }}
VERSION: ${{ env.VERSION }}

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

8
.gitignore vendored
View File

@@ -2,6 +2,9 @@
.gradle/
build/
#kotlin
.kotlin/
# Local configuration file (sdk path, etc)
local.properties
@@ -33,4 +36,7 @@ output.json
scripts/
#crowdin
crowdin.yml
crowdin.yml
#vscode
.vscode

View File

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

View File

@@ -11,15 +11,14 @@ def gitCommitHash = providers.exec {
}.standardOutput.asText.get().trim()
android {
compileSdk 34
compileSdk 35
defaultConfig {
applicationId "ani.dantotsu"
minSdk 21
targetSdk 34
versionCode((System.currentTimeMillis() / 60000).toInteger())
versionName "3.1.0"
versionCode 300100000
targetSdk 35
versionName "3.2.2"
versionCode 300200200
signingConfig signingConfigs.debug
}
@@ -101,6 +100,8 @@ dependencies {
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.webkit:webkit:1.11.0'
implementation "com.anggrayudi:storage:1.5.5"
implementation "androidx.biometric:biometric:1.1.0"
// Glide
ext.glide_version = '4.16.0'
@@ -111,7 +112,7 @@ dependencies {
implementation 'jp.wasabeef:glide-transformations:4.3.0'
// Exoplayer
ext.exo_version = '1.3.1'
ext.exo_version = '1.5.0'
implementation "androidx.media3:media3-exoplayer:$exo_version"
implementation "androidx.media3:media3-ui:$exo_version"
implementation "androidx.media3:media3-exoplayer-hls:$exo_version"
@@ -121,6 +122,8 @@ dependencies {
// Media3 Casting
implementation "androidx.media3:media3-cast:$exo_version"
implementation "androidx.mediarouter:mediarouter:1.7.0"
// Media3 extension
implementation "com.github.anilbeesetti.nextlib:nextlib-media3ext:0.8.3"
// UI
implementation 'com.google.android.material:material:1.12.0'
@@ -131,7 +134,7 @@ dependencies {
implementation 'com.github.VipulOG:ebook-reader:0.1.6'
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
implementation 'com.github.eltos:simpledialogfragments:v3.7'
implementation 'com.github.AAChartModel:AAChartCore-Kotlin:7.2.1'
implementation 'com.github.AAChartModel:AAChartCore-Kotlin:7.2.3'
// Markwon
ext.markwon_version = '4.6.2'
@@ -157,7 +160,7 @@ dependencies {
implementation 'ru.beryukhov:flowreactivenetwork:1.0.4'
implementation 'ca.gosyer:voyager-navigator:1.0.0-rc07'
implementation 'com.squareup.logcat:logcat:0.1'
implementation 'com.github.inorichi.injekt:injekt-core:65b0440'
implementation 'uy.kohesive.injekt:injekt-core:1.16.+'
implementation 'com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.12'
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps'

View File

@@ -1,9 +1,40 @@
package ani.dantotsu.others
import androidx.fragment.app.FragmentActivity
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.text.SimpleDateFormat
import java.util.Locale
object AppUpdater {
suspend fun check(activity: FragmentActivity, post: Boolean = false) {
//no-op
// 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.buildMarkwon
import ani.dantotsu.client
import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.currContext
import ani.dantotsu.decodeBase64ToString
import ani.dantotsu.logError
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.settings.saving.PrefManager
@@ -37,26 +39,88 @@ import java.text.SimpleDateFormat
import java.util.Locale
object AppUpdater {
private val fallbackStableUrl: String
get() = "aHR0cHM6Ly9hcGkuZGFudG90c3UuYXBwL3VwZGF0ZXMvc3RhYmxl".decodeBase64ToString()
private val fallbackBetaUrl: String
get() = "aHR0cHM6Ly9hcGkuZGFudG90c3UuYXBwL3VwZGF0ZXMvYmV0YQ==".decodeBase64ToString()
@Serializable
data class FallbackResponse(
val version: String,
val changelog: String,
val downloadUrl: String? = null
)
private suspend fun fetchUpdateInfo(repo: String, isDebug: Boolean): Pair<String, String>? {
return try {
fetchFromGithub(repo, isDebug)
} catch (e: Exception) {
Logger.log("Github fetch failed, trying fallback: ${e.message}")
try {
fetchFromFallback(isDebug)
} catch (e: Exception) {
Logger.log("Fallback fetch failed: ${e.message}")
null
}
}
}
private suspend fun fetchFromGithub(repo: String, isDebug: Boolean): Pair<String, String> {
return if (isDebug) {
val res = client.get("https://api.github.com/repos/$repo/releases")
.parsed<JsonArray>().map {
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
}
val r = res.filter { it.prerelease }.filter { !it.tagName.contains("fdroid") }
.maxByOrNull {
it.timeStamp()
} ?: throw Exception("No Pre Release Found")
val v = r.tagName.substringAfter("v", "")
(r.body ?: "") to v.ifEmpty { throw Exception("Weird Version : ${r.tagName}") }
} else {
val res = client.get("https://raw.githubusercontent.com/$repo/main/stable.md").text
res to res.substringAfter("# ").substringBefore("\n")
}
}
private suspend fun fetchFromFallback(isDebug: Boolean): Pair<String, String> {
val url = if (isDebug) fallbackBetaUrl else fallbackStableUrl
val response = CommentsAPI.requestBuilder().get(url).parsed<FallbackResponse>()
return response.changelog to response.version
}
private suspend fun fetchApkUrl(repo: String, version: String, isDebug: Boolean): String? {
return try {
fetchApkUrlFromGithub(repo, version)
} catch (e: Exception) {
Logger.log("Github APK fetch failed, trying fallback: ${e.message}")
try {
fetchApkUrlFromFallback(version, isDebug)
} catch (e: Exception) {
Logger.log("Fallback APK fetch failed: ${e.message}")
null
}
}
}
private suspend fun fetchApkUrlFromGithub(repo: String, version: String): String? {
val apks = client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
.parsed<GithubResponse>().assets?.filter {
it.browserDownloadURL.endsWith(".apk")
}
return apks?.firstOrNull()?.browserDownloadURL
}
private suspend fun fetchApkUrlFromFallback(version: String, isDebug: Boolean): String? {
val url = if (isDebug) fallbackBetaUrl else fallbackStableUrl
return CommentsAPI.requestBuilder().get("$url/$version").parsed<FallbackResponse>().downloadUrl
}
suspend fun check(activity: FragmentActivity, post: Boolean = false) {
if (post) snackString(currContext()?.getString(R.string.checking_for_update))
val repo = activity.getString(R.string.repo)
tryWithSuspend {
val (md, version) = if (BuildConfig.DEBUG) {
val res = client.get("https://api.github.com/repos/$repo/releases")
.parsed<JsonArray>().map {
Mapper.json.decodeFromJsonElement<GithubResponse>(it)
}
val r = res.filter { it.prerelease }.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")
}
val (md, version) = fetchUpdateInfo(repo, BuildConfig.DEBUG) ?: return@tryWithSuspend
Logger.log("Git Version : $version")
val dontShow = PrefManager.getCustomVal("dont_ask_for_update_$version", false)
@@ -69,7 +133,11 @@ object AppUpdater {
)
addView(
TextView(activity).apply {
val markWon = buildMarkwon(activity, false)
val markWon = try {
buildMarkwon(activity, false)
} catch (e: IllegalArgumentException) {
return@runOnUiThread
}
markWon.setMarkdown(this, md)
}
)
@@ -85,17 +153,11 @@ object AppUpdater {
setPositiveButton(currContext()!!.getString(R.string.lets_go)) {
MainScope().launch(Dispatchers.IO) {
try {
val apks =
client.get("https://api.github.com/repos/$repo/releases/tags/v$version")
.parsed<GithubResponse>().assets?.filter {
it.browserDownloadURL.endsWith(
".apk"
)
}
val apkToDownload = apks?.first()
apkToDownload?.browserDownloadURL.apply {
if (this != null) activity.downloadUpdate(version, this)
else openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
val apkUrl = fetchApkUrl(repo, version, BuildConfig.DEBUG)
if (apkUrl != null) {
activity.downloadUpdate(version, apkUrl)
} else {
openLinkInBrowser("https://github.com/repos/$repo/releases/tag/v$version")
}
} catch (e: Exception) {
logError(e)
@@ -108,8 +170,7 @@ object AppUpdater {
}
show(activity.supportFragmentManager, "dialog")
}
}
else {
} else {
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
private fun Activity.downloadUpdate(version: String, url: String): Boolean {
private fun Activity.downloadUpdate(version: String, url: String) {
toast(getString(R.string.downloading_update, version))
val downloadManager = this.getSystemService<DownloadManager>()!!
@@ -163,7 +223,7 @@ object AppUpdater {
logError(e)
-1
}
if (id == -1L) return true
if (id == -1L) return
ContextCompat.registerReceiver(
this,
object : BroadcastReceiver() {
@@ -184,7 +244,6 @@ object AppUpdater {
}, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
ContextCompat.RECEIVER_EXPORTED
)
return true
}
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.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
@@ -112,10 +113,9 @@
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/epub+zip" />
<data android:mimeType="application/epub+zip"/>
<data android:mimeType="application/x-mobipocket-ebook" />
<data android:mimeType="application/vnd.amazon.ebook" />
<data android:mimeType="application/fb2+zip" />
@@ -131,10 +131,11 @@
</activity>
<activity android:name=".others.calc.CalcActivity"
android:parentActivityName=".MainActivity" />
<activity android:name=".settings.FAQActivity" />
<activity android:name=".settings.ReaderSettingsActivity" />
<activity android:name=".settings.AnilistSettingsActivity"/>
<activity android:name=".settings.UserInterfaceSettingsActivity" />
<activity android:name=".settings.PlayerSettingsActivity" />
<activity android:name=".settings.ReaderSettingsActivity" />
<activity android:name=".settings.FAQActivity" />
<activity
android:name=".settings.SettingsActivity"
android:parentActivityName=".MainActivity" />
@@ -155,7 +156,8 @@
android:parentActivityName=".MainActivity" />
<activity
android:name=".settings.SettingsExtensionsActivity"
android:parentActivityName=".MainActivity" />
android:parentActivityName=".MainActivity"
android:windowSoftInputMode="adjustPan"/>
<activity
android:name=".settings.SettingsAddonActivity"
android:parentActivityName=".MainActivity" />
@@ -194,14 +196,15 @@
android:label="Inbox Activity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".profile.activity.NotificationActivity"
android:name=".profile.notification.NotificationActivity"
android:label="Inbox Activity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".others.imagesearch.ImageSearchActivity"
android:parentActivityName=".MainActivity" />
<activity
android:name=".util.MarkdownCreatorActivity"/>
android:name=".util.ActivityMarkdownCreator"
android:windowSoftInputMode="adjustResize|stateVisible" />
<activity android:name=".parsers.ParserTestActivity" />
<activity
android:name=".media.ReviewActivity"
@@ -370,24 +373,29 @@
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.Main" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="content" />
<data android:scheme="file" />
<data android:mimeType="*/*" />
<data android:pathPattern=".*\\.ani" />
<data android:pathPattern=".*\\.sani" />
<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>
</activity>
<activity

View File

@@ -105,6 +105,14 @@ class App : MultiDexApplication() {
LogcatLogger.install(AndroidLogcatLogger(LogPriority.VERBOSE))
}
if (PrefManager.getVal<Int>(PrefName.CommentsEnabled) == 0) {
if (BuildConfig.FLAVOR.contains("fdroid")) {
PrefManager.setVal(PrefName.CommentsEnabled, 2)
} else {
PrefManager.setVal(PrefName.CommentsEnabled, 1)
}
}
CoroutineScope(Dispatchers.IO).launch {
animeExtensionManager = Injekt.get()
animeExtensionManager.findAvailableExtensions()
@@ -128,7 +136,9 @@ class App : MultiDexApplication() {
downloadAddonManager = Injekt.get()
torrentAddonManager.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 scheduler = TaskScheduler.create(this@App, useAlarmManager)

View File

@@ -68,7 +68,6 @@ import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.AttrRes
import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
@@ -92,12 +91,12 @@ import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.BuildConfig.APPLICATION_ID
import ani.dantotsu.connections.anilist.Genre
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.bakaupdates.MangaUpdates
import ani.dantotsu.connections.crashlytics.CrashlyticsInterface
import ani.dantotsu.databinding.ItemCountDownBinding
import ani.dantotsu.media.Media
import ani.dantotsu.media.MediaDetailsActivity
import ani.dantotsu.notifications.IncognitoNotificationClickReceiver
import ani.dantotsu.others.AlignTagHandler
import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.others.SpoilerPlugin
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.internal.PreferenceKeystore
import ani.dantotsu.settings.saving.internal.PreferenceKeystore.Companion.generateSalt
import ani.dantotsu.util.CountUpTimer
import ani.dantotsu.util.Logger
import com.bumptech.glide.Glide
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.RequestOptions
import com.bumptech.glide.request.target.Target
import com.bumptech.glide.request.target.ViewTarget
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.google.android.material.bottomnavigation.BottomNavigationView
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.glide.GlideImagesPlugin
import jp.wasabeef.glide.transformations.BlurTransformation
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import nl.joery.animatedbottombar.AnimatedBottomBar
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@@ -154,10 +148,13 @@ import java.io.FileOutputStream
import java.io.OutputStream
import java.lang.reflect.Field
import java.util.Calendar
import java.util.Locale
import java.util.TimeZone
import java.util.Timer
import java.util.TimerTask
import kotlin.collections.set
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import kotlin.math.log2
import kotlin.math.max
import kotlin.math.min
@@ -314,6 +311,7 @@ fun Activity.reloadActivity() {
Refresh.all()
finish()
startActivity(Intent(this, this::class.java))
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
initActivity(this)
}
@@ -855,6 +853,7 @@ fun savePrefsToDownloads(
)
}
@SuppressLint("StringFormatMatches")
fun savePrefs(serialized: String, path: String, title: String, context: Context): File? {
var file = File(path, "$title.ani")
var counter = 1
@@ -874,6 +873,7 @@ fun savePrefs(serialized: String, path: String, title: String, context: Context)
}
}
@SuppressLint("StringFormatMatches")
fun savePrefs(
serialized: String,
path: String,
@@ -921,6 +921,7 @@ fun shareImage(title: String, bitmap: Bitmap, context: Context) {
context.startActivity(Intent.createChooser(intent, "Share $title"))
}
@SuppressLint("StringFormatMatches")
fun saveImage(image: Bitmap, path: String, imageFileName: String): File? {
val imageFile = File(path, "$imageFileName.png")
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) {
when {
media.anime != null -> countDown(media, view)
media.format == "MANGA" || media.format == "ONE_SHOT" -> sinceWhen(media, view)
else -> {} // No timer yet
else -> {}
}
}
@@ -1447,6 +1411,8 @@ fun openOrCopyAnilistLink(link: String) {
} else {
copyToClipboard(link, true)
}
} else if (getYoutubeId(link).isNotEmpty()) {
openLinkInYouTube(link)
} else {
copyToClipboard(link, true)
}
@@ -1483,6 +1449,7 @@ fun buildMarkwon(
TagHandlerNoOp.create("h1", "h2", "h3", "h4", "h5", "h6", "hr", "pre", "a")
)
}
plugin.addHandler(AlignTagHandler())
})
.usePlugin(GlideImagesPlugin.create(object : GlideImagesPlugin.GlideStore {
@@ -1527,3 +1494,44 @@ fun buildMarkwon(
.build()
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.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Intent
import android.content.res.Configuration
import android.graphics.drawable.Animatable
@@ -51,7 +50,8 @@ import ani.dantotsu.others.CustomBottomDialog
import ani.dantotsu.others.calc.CalcActivity
import ani.dantotsu.profile.ProfileActivity
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.saving.PrefManager
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.PreferencePackager
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.AudioHelper
import ani.dantotsu.util.Logger
import ani.dantotsu.util.customAlertDialog
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputEditText
import eu.kanade.domain.source.service.SourcePreferences
import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin
@@ -116,58 +117,8 @@ class MainActivity : AppCompatActivity() {
}
}
val action = intent.action
val type = intent.type
if (Intent.ACTION_VIEW == action && type != null) {
val uri: Uri? = intent.data
try {
if (uri == null) {
throw Exception("Uri is null")
}
val jsonString =
contentResolver.openInputStream(uri)?.readBytes()
?: throw Exception("Error reading file")
val name =
DocumentFile.fromSingleUri(this, uri)?.name ?: "settings"
//.sani is encrypted, .ani is not
if (name.endsWith(".sani")) {
passwordAlertDialog { password ->
if (password != null) {
val salt = jsonString.copyOfRange(0, 16)
val encrypted = jsonString.copyOfRange(16, jsonString.size)
val decryptedJson = try {
PreferenceKeystore.decryptWithPassword(
password,
encrypted,
salt
)
} catch (e: Exception) {
toast("Incorrect password")
return@passwordAlertDialog
}
if (PreferencePackager.unpack(decryptedJson)) {
val intent = Intent(this, this.javaClass)
this.finish()
startActivity(intent)
}
} else {
toast("Password cannot be empty")
}
}
} else if (name.endsWith(".ani")) {
val decryptedJson = jsonString.toString(Charsets.UTF_8)
if (PreferencePackager.unpack(decryptedJson)) {
val intent = Intent(this, this.javaClass)
this.finish()
startActivity(intent)
}
} else {
toast("Invalid file type")
}
} catch (e: Exception) {
e.printStackTrace()
toast("Error importing settings")
}
if (Intent.ACTION_VIEW == intent.action) {
handleViewIntent(intent)
}
val bottomNavBar = findViewById<AnimatedBottomBar>(R.id.navbar)
@@ -287,7 +238,7 @@ class MainActivity : AppCompatActivity() {
.get() > 0 || preferences.mangaExtensionUpdatesCount().get() > 0
) {
snackString(R.string.extension_updates_available)
?.setDuration(Snackbar.LENGTH_LONG)
?.setDuration(Snackbar.LENGTH_SHORT)
?.setAction(R.string.review) {
startActivity(Intent(this, ExtensionsActivity::class.java))
}
@@ -365,7 +316,6 @@ class MainActivity : AppCompatActivity() {
} else if (fragmentToLoad == "NOTIFICATIONS" && activityId != -1) {
Logger.log("MainActivity, onCreate: $activityId")
val notificationIntent = Intent(this, NotificationActivity::class.java).apply {
putExtra("FRAGMENT_TO_LOAD", "NOTIFICATIONS")
putExtra("activityId", activityId)
}
launched = true
@@ -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>()
fun startTorrent() {
if (torrentManager.isAvailable() && PrefManager.getVal(PrefName.TorrentEnabled)) {
@@ -490,39 +443,102 @@ class MainActivity : AppCompatActivity() {
params.updateMargins(bottom = margin.toPx)
}
private fun handleViewIntent(intent: Intent) {
val uri: Uri? = intent.data
try {
if (uri == null) {
throw Exception("Uri is null")
}
if ((uri.scheme == "tachiyomi" || uri.scheme == "aniyomi" || uri.scheme == "novelyomi") && uri.host == "add-repo") {
val url = uri.getQueryParameter("url") ?: throw Exception("No url for repo import")
val (prefName, name) = when (uri.scheme) {
"tachiyomi" -> PrefName.MangaExtensionRepos to "Manga"
"aniyomi" -> PrefName.AnimeExtensionRepos to "Anime"
"novelyomi" -> PrefName.NovelExtensionRepos to "Novel"
else -> throw Exception("Invalid scheme")
}
val savedRepos: Set<String> = PrefManager.getVal(prefName)
val newRepos = savedRepos.toMutableSet()
AddRepositoryBottomSheet.addRepoWarning(this) {
newRepos.add(url)
PrefManager.setVal(prefName, newRepos)
toast("$name Extension Repo added")
}
return
}
if (intent.type == null) return
val jsonString =
contentResolver.openInputStream(uri)?.readBytes()
?: throw Exception("Error reading file")
val name =
DocumentFile.fromSingleUri(this, uri)?.name ?: "settings"
//.sani is encrypted, .ani is not
if (name.endsWith(".sani")) {
passwordAlertDialog { password ->
if (password != null) {
val salt = jsonString.copyOfRange(0, 16)
val encrypted = jsonString.copyOfRange(16, jsonString.size)
val decryptedJson = try {
PreferenceKeystore.decryptWithPassword(
password,
encrypted,
salt
)
} catch (e: Exception) {
toast("Incorrect password")
return@passwordAlertDialog
}
if (PreferencePackager.unpack(decryptedJson)) {
val newIntent = Intent(this, this.javaClass)
this.finish()
startActivity(newIntent)
}
} else {
toast("Password cannot be empty")
}
}
} else if (name.endsWith(".ani")) {
val decryptedJson = jsonString.toString(Charsets.UTF_8)
if (PreferencePackager.unpack(decryptedJson)) {
val newIntent = Intent(this, this.javaClass)
this.finish()
startActivity(newIntent)
}
} else {
toast("Invalid file type")
}
} catch (e: Exception) {
e.printStackTrace()
toast("Error importing settings")
}
}
private fun passwordAlertDialog(callback: (CharArray?) -> Unit) {
val password = CharArray(16).apply { fill('0') }
// Inflate the dialog layout
val dialogView = DialogUserAgentBinding.inflate(layoutInflater)
dialogView.userAgentTextBox.hint = "Password"
dialogView.subtitle.visibility = View.VISIBLE
dialogView.subtitle.text = getString(R.string.enter_password_to_decrypt_file)
val dialog = AlertDialog.Builder(this, R.style.MyPopup)
.setTitle("Enter Password")
.setView(dialogView.root)
.setPositiveButton("OK", null)
.setNegativeButton("Cancel") { dialog, _ ->
val dialogView = DialogUserAgentBinding.inflate(layoutInflater).apply {
userAgentTextBox.hint = "Password"
subtitle.visibility = View.VISIBLE
subtitle.text = getString(R.string.enter_password_to_decrypt_file)
}
customAlertDialog().apply {
setTitle("Enter Password")
setCustomView(dialogView.root)
setPosButton(R.string.yes) {
val editText = dialogView.userAgentTextBox
if (editText.text?.isNotBlank() == true) {
editText.text?.toString()?.trim()?.toCharArray(password)
callback(password)
} else {
toast("Password cannot be empty")
}
}
setNegButton(R.string.cancel) {
password.fill('0')
dialog.dismiss()
callback(null)
}
.create()
dialog.window?.setDimAmount(0.8f)
dialog.show()
// Override the positive button here
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
val editText = dialog.findViewById<TextInputEditText>(R.id.userAgentTextBox)
if (editText?.text?.isNotBlank() == true) {
editText.text?.toString()?.trim()?.toCharArray(password)
dialog.dismiss()
callback(password)
} else {
toast("Password cannot be empty")
}
show()
}
}

View File

@@ -26,9 +26,17 @@ interface DownloadAddonApiV2 {
statCallback: (Double) -> Unit
): 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

View File

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

View File

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

View File

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

View File

@@ -2,15 +2,25 @@ package ani.dantotsu.connections.anilist
import ani.dantotsu.R
import ani.dantotsu.currContext
import ani.dantotsu.media.Author
import ani.dantotsu.media.Character
import ani.dantotsu.media.Media
import ani.dantotsu.media.Studio
import ani.dantotsu.profile.User
import java.io.Serializable
data class SearchResults(
interface SearchResults<T> {
var search: String?
var page: Int
var results: MutableList<T>
var hasNextPage: Boolean
}
data class AniMangaSearchResults(
val type: String,
var isAdult: Boolean,
var onList: Boolean? = null,
var perPage: Int? = null,
var search: String? = null,
var countryOfOrigin: String? = null,
var sort: String? = null,
var genres: MutableList<String>? = null,
@@ -23,10 +33,11 @@ data class SearchResults(
var seasonYear: Int? = null,
var startYear: Int? = null,
var season: String? = null,
var page: Int = 1,
var results: MutableList<Media>,
var hasNextPage: Boolean,
) : Serializable {
override var search: String? = null,
override var page: Int = 1,
override var results: MutableList<Media>,
override var hasNextPage: Boolean,
) : SearchResults<Media>, Serializable {
fun toChipList(): List<SearchChip> {
val list = mutableListOf<SearchChip>()
sort?.let {
@@ -108,4 +119,33 @@ data class SearchResults(
val type: String,
val text: String
)
}
}
data class CharacterSearchResults(
override var search: String?,
override var page: Int = 1,
override var results: MutableList<Character>,
override var hasNextPage: Boolean,
) : SearchResults<Character>, Serializable
data class StudioSearchResults(
override var search: String?,
override var page: Int = 1,
override var results: MutableList<Studio>,
override var hasNextPage: Boolean,
) : SearchResults<Studio>, Serializable
data class StaffSearchResults(
override var search: String?,
override var page: Int = 1,
override var results: MutableList<Author>,
override var hasNextPage: Boolean,
) : SearchResults<Author>, Serializable
data class UserSearchResults(
override var search: String?,
override var page: Int = 1,
override var results: MutableList<User>,
override var hasNextPage: Boolean,
) : SearchResults<User>, Serializable

View File

@@ -15,6 +15,8 @@ import ani.dantotsu.snackString
import ani.dantotsu.toast
import ani.dantotsu.util.Logger
import java.util.Calendar
import java.util.Locale
import kotlin.math.abs
object Anilist {
val query: AnilistQueries = AnilistQueries()
@@ -22,7 +24,7 @@ object Anilist {
var token: String? = null
var username: String? = null
var adult: Boolean = false
var userid: Int? = null
var avatar: String? = null
var bg: String? = null
@@ -36,6 +38,17 @@ object Anilist {
var rateLimitReset: Long = 0
var initialized = false
var adult: Boolean = false
var titleLanguage: String? = null
var staffNameLanguage: String? = null
var airingNotifications: Boolean = false
var restrictMessagesToFollowing: Boolean = false
var scoreFormat: String? = null
var rowOrder: String? = null
var activityMergeTime: Int? = null
var timezone: String? = null
var animeCustomLists: List<String>? = null
var mangaCustomLists: List<String>? = null
val sortBy = listOf(
"SCORE_DESC",
@@ -96,6 +109,86 @@ object Anilist {
"Original Creator", "Story & Art", "Story"
)
val timeZone = listOf(
"(GMT-11:00) Pago Pago",
"(GMT-10:00) Hawaii Time",
"(GMT-09:00) Alaska Time",
"(GMT-08:00) Pacific Time",
"(GMT-07:00) Mountain Time",
"(GMT-06:00) Central Time",
"(GMT-05:00) Eastern Time",
"(GMT-04:00) Atlantic Time - Halifax",
"(GMT-03:00) Sao Paulo",
"(GMT-02:00) Mid-Atlantic",
"(GMT-01:00) Azores",
"(GMT+00:00) London",
"(GMT+01:00) Berlin",
"(GMT+02:00) Helsinki",
"(GMT+03:00) Istanbul",
"(GMT+04:00) Dubai",
"(GMT+04:30) Kabul",
"(GMT+05:00) Maldives",
"(GMT+05:30) India Standard Time",
"(GMT+05:45) Kathmandu",
"(GMT+06:00) Dhaka",
"(GMT+06:30) Cocos",
"(GMT+07:00) Bangkok",
"(GMT+08:00) Hong Kong",
"(GMT+08:30) Pyongyang",
"(GMT+09:00) Tokyo",
"(GMT+09:30) Central Time - Darwin",
"(GMT+10:00) Eastern Time - Brisbane",
"(GMT+10:30) Central Time - Adelaide",
"(GMT+11:00) Eastern Time - Melbourne, Sydney",
"(GMT+12:00) Nauru",
"(GMT+13:00) Auckland",
"(GMT+14:00) Kiritimati",
)
val titleLang = listOf(
"English (Attack on Titan)",
"Romaji (Shingeki no Kyojin)",
"Native (進撃の巨人)"
)
val staffNameLang = listOf(
"Romaji, Western Order (Killua Zoldyck)",
"Romaji (Zoldyck Killua)",
"Native (キルア=ゾルディック)"
)
val scoreFormats = listOf(
"100 Point (55/100)",
"10 Point Decimal (5.5/10)",
"10 Point (5/10)",
"5 Star (3/5)",
"3 Point Smiley :)"
)
val rowOrderMap = mapOf(
"Score" to "score",
"Title" to "title",
"Last Updated" to "updatedAt",
"Last Added" to "id"
)
val activityMergeTimeMap = mapOf(
"Never" to 0,
"30 mins" to 30,
"69 mins" to 69,
"1 hour" to 60,
"2 hours" to 120,
"3 hours" to 180,
"6 hours" to 360,
"12 hours" to 720,
"1 day" to 1440,
"2 days" to 2880,
"3 days" to 4320,
"1 week" to 10080,
"2 weeks" to 20160,
"Always" to 29160
)
private val cal: Calendar = Calendar.getInstance()
private val currentYear = cal.get(Calendar.YEAR)
private val currentSeason: Int = when (cal.get(Calendar.MONTH)) {
@@ -106,6 +199,33 @@ object Anilist {
else -> 0
}
fun getDisplayTimezone(apiTimezone: String, context: Context): String {
val noTimezone = context.getString(R.string.selected_no_time_zone)
val parts = apiTimezone.split(":")
if (parts.size != 2) return noTimezone
val hours = parts[0].toIntOrNull() ?: 0
val minutes = parts[1].toIntOrNull() ?: 0
val sign = if (hours >= 0) "+" else "-"
val formattedHours = String.format(Locale.US, "%02d", abs(hours))
val formattedMinutes = String.format(Locale.US, "%02d", minutes)
val searchString = "(GMT$sign$formattedHours:$formattedMinutes)"
return timeZone.find { it.contains(searchString) } ?: noTimezone
}
fun getApiTimezone(displayTimezone: String): String {
val regex = """\(GMT([+-])(\d{2}):(\d{2})\)""".toRegex()
val matchResult = regex.find(displayTimezone)
return if (matchResult != null) {
val (sign, hours, minutes) = matchResult.destructured
val formattedSign = if (sign == "+") "" else "-"
"$formattedSign$hours:$minutes"
} else {
"00:00"
}
}
private fun getSeason(next: Boolean): Pair<String, Int> {
var newSeason = if (next) currentSeason + 1 else currentSeason - 1
var newYear = currentYear

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -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
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 commentsEnabled = PrefManager.getVal<Int>(PrefName.CommentsEnabled) == 1
private val ADDRESS: String get() = if (commentsEnabled) API_ADDRESS else LOCAL_HOST
var authToken: String? = null
var userId: String? = null
var isBanned: Boolean = false
@@ -371,8 +374,8 @@ object CommentsAPI {
}
private fun errorMessage(reason: String) {
Logger.log(reason)
if (isOnline) snackString(reason)
if (commentsEnabled) Logger.log(reason)
if (isOnline && commentsEnabled) snackString(reason)
}
fun logout() {
@@ -408,7 +411,7 @@ object CommentsAPI {
return map
}
private fun requestBuilder(client: OkHttpClient = Injekt.get<NetworkHelper>().client): Requests {
fun requestBuilder(client: OkHttpClient = Injekt.get<NetworkHelper>().client): Requests {
return Requests(
client,
headerBuilder()

View File

@@ -70,7 +70,7 @@ object Discord {
const val application_Id = "1163925779692912771"
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 =
"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

@@ -1,24 +1,19 @@
package ani.dantotsu.connections.discord
import ani.dantotsu.connections.discord.Discord.token
import ani.dantotsu.connections.discord.serializers.Activity
import ani.dantotsu.connections.discord.serializers.Presence
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit.SECONDS
import kotlin.coroutines.CoroutineContext
import ani.dantotsu.client as app
@Suppress("MemberVisibilityCanBePrivate")
open class RPC(val token: String, val coroutineContext: CoroutineContext) {
private val json = Json {
encodeDefaults = true
allowStructuredMapKeys = true
ignoreUnknownKeys = true
}
enum class Type {
PLAYING, STREAMING, LISTENING, WATCHING, COMPETING
}
@@ -27,7 +22,7 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
companion object {
data class RPCData(
val applicationId: String? = null,
val applicationId: String,
val type: Type? = null,
val activityName: String? = null,
val details: String? = null,
@@ -40,24 +35,24 @@ open class RPC(val token: String, val coroutineContext: CoroutineContext) {
val buttons: MutableList<Link> = mutableListOf()
)
@Serializable
data class KizzyApi(val id: String)
val api = "https://kizzy-api.vercel.app/image?url="
private suspend fun String.discordUrl(): String? {
if (startsWith("mp:")) return this
val json = app.get("$api$this").parsedSafe<KizzyApi>()
return json?.id
}
suspend fun createPresence(data: RPCData): String {
val json = Json {
encodeDefaults = true
allowStructuredMapKeys = true
ignoreUnknownKeys = true
}
return json.encodeToString(Presence.Response(
3,
val client = OkHttpClient.Builder()
.connectTimeout(10, SECONDS)
.readTimeout(10, SECONDS)
.writeTimeout(10, SECONDS)
.build()
val assetApi = RPCExternalAsset(data.applicationId, token!!, client, json)
suspend fun String.discordUrl() = assetApi.getDiscordUri(this)
return json.encodeToString(
Presence.Response(
3,
Presence(
activities = listOf(
Activity(

View File

@@ -0,0 +1,61 @@
// this code was kanged from the greatest mind of this era, aka shivam brahmkshatriya
// please subscribe to my only fans here: https://github.com/brahmkshatriya
package ani.dantotsu.connections.discord
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.Call
import okhttp3.Callback
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okio.IOException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
class RPCExternalAsset(
applicationId: String,
private val token: String,
private val client: OkHttpClient,
private val json: Json
) {
@Serializable
data class ExternalAsset(
val url: String? = null,
@SerialName("external_asset_path")
val externalAssetPath: String? = null
)
private val api = "https://discord.com/api/v9/applications/$applicationId/external-assets"
suspend fun getDiscordUri(imageUrl: String): String? {
if (imageUrl.startsWith("mp:")) return imageUrl
val request = Request.Builder().url(api).header("Authorization", token)
.post("{\"urls\":[\"$imageUrl\"]}".toRequestBody("application/json".toMediaType()))
.build()
return runCatching {
val res = client.newCall(request).await()
json.decodeFromString<List<ExternalAsset>>(res.body.string())
.firstOrNull()?.externalAssetPath?.let { "mp:$it" }
}.getOrNull()
}
private suspend inline fun Call.await(): Response {
return suspendCoroutine {
enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
it.resumeWithException(e)
}
override fun onResponse(call: Call, response: Response) {
it.resume(response)
}
})
}
}
}

View File

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

View File

@@ -28,6 +28,7 @@ class Contributors {
"rebelonion" -> "Owner & Maintainer"
"sneazy-ibo" -> "Contributor & Comment Moderator"
"WaiWhat" -> "Icon Designer"
"itsmechinmoy" -> "Discord and Telegram Admin/Helper, Comment Moderator & Translator"
else -> "Contributor"
}
developers = developers.plus(
@@ -89,9 +90,15 @@ class Contributors {
"Comment Moderator and Arabic Translator",
"https://anilist.co/user/6049773"
),
Developer(
"Dawnusedyeet",
"https://s4.anilist.co/file/anilistcdn/user/avatar/large/b6237399-RHFvRHriXjwS.png",
"Contributor",
"https://anilist.co/user/Dawnusedyeet/"
),
Developer(
"hastsu",
"https://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",
"https://anilist.co/user/6183359"
),
@@ -111,4 +118,4 @@ class Contributors {
@SerialName("html_url")
val htmlUrl: String
)
}
}

View File

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

View File

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

View File

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

View File

@@ -48,6 +48,7 @@ import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.util.Logger
import ani.dantotsu.util.customAlertDialog
import com.anggrayudi.storage.file.openInputStream
import com.google.android.material.card.MaterialCardView
import com.google.android.material.imageview.ShapeableImageView
@@ -202,25 +203,24 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
val type: MediaType = MediaType.ANIME
// Alert dialog to confirm deletion
val builder =
androidx.appcompat.app.AlertDialog.Builder(requireContext(), R.style.MyPopup)
builder.setTitle("Delete ${item.title}?")
builder.setMessage("Are you sure you want to delete ${item.title}?")
builder.setPositiveButton("Yes") { _, _ ->
downloadManager.removeMedia(item.title, type)
val mediaIds =
PrefManager.getAnimeDownloadPreferences().all?.filter { it.key.contains(item.title) }?.values
?: emptySet()
if (mediaIds.isEmpty()) {
snackString("No media found") // if this happens, terrible things have happened
requireContext().customAlertDialog().apply {
setTitle("Delete ${item.title}?")
setMessage("Are you sure you want to delete ${item.title}?")
setPosButton(R.string.yes) {
downloadManager.removeMedia(item.title, type)
val mediaIds =
PrefManager.getAnimeDownloadPreferences().all?.filter { it.key.contains(item.title) }?.values
?: emptySet()
if (mediaIds.isEmpty()) {
snackString("No media found") // if this happens, terrible things have happened
}
getDownloads()
}
getDownloads()
setNegButton(R.string.no) {
// Do nothing
}
show()
}
builder.setNegativeButton("No") { _, _ ->
// Do nothing
}
val dialog = builder.show()
dialog.window?.setDimAmount(0.8f)
true
}
}
@@ -288,10 +288,12 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
}
downloadsJob = Job()
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>()
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 offlineAnimeModel = loadOfflineAnimeModel(download)
if (offlineAnimeModel.title == "unknown") offlineAnimeModel.title = title
@@ -319,17 +321,20 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
)
val gson = GsonBuilder()
.registerTypeAdapter(SChapter::class.java, InstanceCreator<SChapter> {
SChapterImpl() // Provide an instance of SChapterImpl
SChapterImpl()
})
.registerTypeAdapter(SAnime::class.java, InstanceCreator<SAnime> {
SAnimeImpl() // Provide an instance of SAnimeImpl
SAnimeImpl()
})
.registerTypeAdapter(SEpisode::class.java, InstanceCreator<SEpisode> {
SEpisodeImpl() // Provide an instance of SEpisodeImpl
SEpisodeImpl()
})
.create()
val media = 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 =
media.openInputStream(context ?: currContext()!!)?.bufferedReader().use {
it?.readText()
@@ -394,6 +399,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
bannerUri
)
} catch (e: Exception) {
Logger.log(e)
return try {
loadOfflineAnimeModelCompat(downloadedType)
} catch (e: Exception) {
@@ -401,7 +407,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener {
Logger.log(e)
Injekt.get<CrashlyticsInterface>().logException(e)
OfflineAnimeModel(
"unknown",
downloadedType.titleName,
"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.snackString
import ani.dantotsu.util.Logger
import ani.dantotsu.util.NumberConverter.Companion.ofLength
import com.anggrayudi.storage.file.deleteRecursively
import com.anggrayudi.storage.file.forceDelete
import com.anggrayudi.storage.file.openOutputStream
@@ -134,15 +135,15 @@ class MangaDownloaderService : Service() {
mutex.withLock {
downloadJobs[task.chapter] = job
}
job.join() // Wait for the job to complete before continuing to the next task
job.join()
mutex.withLock {
downloadJobs.remove(task.chapter)
}
updateNotification() // Update the notification after each task is completed
updateNotification()
}
if (MangaServiceDataSingleton.downloadQueue.isEmpty()) {
withContext(Dispatchers.Main) {
stopSelf() // Stop the service when the queue is empty
stopSelf()
}
}
}
@@ -181,7 +182,7 @@ class MangaDownloaderService : Service() {
suspend fun download(task: DownloadTask) {
try {
withContext(Dispatchers.Main) {
withContext(Dispatchers.IO) {
val notifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(
this@MangaDownloaderService,
@@ -194,18 +195,27 @@ class MangaDownloaderService : Service() {
val deferredMap = mutableMapOf<Int, Deferred<Bitmap?>>()
builder.setContentText("Downloading ${task.title} - ${task.chapter}")
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
withContext(Dispatchers.Main) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
}
getSubDirectory(
val baseOutputDir = getSubDirectory(
this@MangaDownloaderService,
MediaType.MANGA,
false,
task.title
) ?: throw Exception("Base output directory not found")
val outputDir = getSubDirectory(
this@MangaDownloaderService,
MediaType.MANGA,
false,
task.title,
task.chapter
)?.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
for ((index, image) in task.imageData.withIndex()) {
if (deferredMap.size >= task.simultaneousDownloads) {
@@ -222,64 +232,76 @@ class MangaDownloaderService : Service() {
image.page,
image.source
)
if (bitmap == null) {
snackString("${task.chapter} - Retrying to download page ${index.ofLength(3)}, attempt ${retryCount + 1}.")
}
retryCount++
}
if (bitmap != null) {
saveToDisk("$index.jpg", bitmap, task.title, task.chapter)
if (bitmap == null) {
outputDir.deleteRecursively(this@MangaDownloaderService, false)
throw Exception("${task.chapter} - Unable to download all pages after $retryCount attempts. Try again.")
}
saveToDisk("${index.ofLength(3)}.jpg", outputDir, bitmap)
farthest++
builder.setProgress(task.imageData.size, farthest, false)
broadcastDownloadProgress(
task.chapter,
task.uniqueName,
farthest * 100 / task.imageData.size
)
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
withContext(Dispatchers.Main) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
}
bitmap
}
}
// Wait for any remaining deferred to complete
deferredMap.values.awaitAll()
builder.setContentText("${task.title} - ${task.chapter} Download complete")
.setProgress(0, 0, false)
notificationManager.notify(NOTIFICATION_ID, builder.build())
withContext(Dispatchers.Main) {
builder.setContentText("${task.title} - ${task.chapter} Download complete")
.setProgress(0, 0, false)
if (notifi) {
notificationManager.notify(NOTIFICATION_ID, builder.build())
}
}
saveMediaInfo(task)
saveMediaInfo(task, baseOutputDir)
downloadsManager.addDownload(
DownloadedType(
task.title,
task.chapter,
MediaType.MANGA
MediaType.MANGA,
scanlator = task.scanlator,
)
)
broadcastDownloadFinished(task.chapter)
broadcastDownloadFinished(task.uniqueName)
snackString("${task.title} - ${task.chapter} Download finished")
}
} catch (e: Exception) {
Logger.log("Exception while downloading file: ${e.message}")
snackString("Exception while downloading file: ${e.message}")
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 {
// 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)
// Create a file reference within that directory for the image
val file =
directory.createFile("image/jpeg", fileName) ?: throw Exception("File not created")
// Use a FileOutputStream to write the bitmap to the file
file.openOutputStream(this, false).use { outputStream ->
if (outputStream == null) throw Exception("Output stream is null")
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
@@ -292,11 +314,8 @@ class MangaDownloaderService : Service() {
}
@OptIn(DelicateCoroutinesApi::class)
private fun saveMediaInfo(task: DownloadTask) {
private fun saveMediaInfo(task: DownloadTask, directory: DocumentFile) {
launchIO {
val directory =
getSubDirectory(this@MangaDownloaderService, MediaType.MANGA, false, task.title)
?: throw Exception("Directory not found")
directory.findFile("media.json")?.forceDelete(this@MangaDownloaderService)
val file = directory.createFile("application/json", "media.json")
?: throw Exception("File not created")
@@ -411,11 +430,15 @@ class MangaDownloaderService : Service() {
data class DownloadTask(
val title: String,
val chapter: String,
val scanlator: String,
val imageData: List<ImageData>,
val sourceMedia: Media? = null,
val retries: Int = 2,
val simultaneousDownloads: Int = 2,
)
) {
val uniqueName: String
get() = "$chapter-$scanlator"
}
companion object {
private const val NOTIFICATION_ID = 1103

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,6 @@ import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@@ -21,7 +20,6 @@ import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.MediaPageTransformer
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemAnimePageBinding
import ani.dantotsu.databinding.LayoutTrendingBinding
import ani.dantotsu.getAppString
@@ -83,13 +81,21 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
updateAvatar()
trendingBinding.searchBar.hint = "ANIME"
trendingBinding.searchBar.hint = binding.root.context.getString(R.string.search)
trendingBinding.searchBarText.setOnClickListener {
ContextCompat.startActivity(
it.context,
Intent(it.context, SearchActivity::class.java).putExtra("type", "ANIME"),
null
)
val context = binding.root.context
if (PrefManager.getVal(PrefName.AniMangaSearchDirect) && Anilist.token != null) {
ContextCompat.startActivity(
context,
Intent(context, SearchActivity::class.java).putExtra("type", "ANIME"),
null
)
} else {
SearchBottomSheet.newInstance().show(
(context as AppCompatActivity).supportFragmentManager,
"search"
)
}
}
trendingBinding.userAvatar.setSafeOnClickListener {
@@ -111,8 +117,8 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
trendingBinding.searchBar.performClick()
}
trendingBinding.notificationCount.visibility =
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
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
recyclerView.adapter = adaptor
recyclerView.layoutManager =
@@ -268,8 +282,9 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
LinearLayoutManager.HORIZONTAL,
false
)
MediaListViewActivity.passedMedia = media.toCollection(ArrayList())
more.setOnClickListener {
MediaListViewActivity.passedMedia = media.toCollection(ArrayList())
ContextCompat.startActivity(
it.context, Intent(it.context, MediaListViewActivity::class.java)
.putExtra("title", string),
@@ -294,8 +309,8 @@ class AnimePageAdapter : RecyclerView.Adapter<AnimePageAdapter.AnimePageViewHold
fun updateNotificationCount() {
if (this::binding.isInitialized) {
trendingBinding.notificationCount.visibility =
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
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.statusBarHeight
import ani.dantotsu.util.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.math.max
@@ -92,6 +92,7 @@ class HomeFragment : Fragment() {
)
binding.homeUserDataProgressBar.visibility = View.GONE
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
binding.homeAnimeList.setOnClickListener {
@@ -132,6 +133,12 @@ class HomeFragment : Fragment() {
"dialog"
)
}
binding.searchImageContainer.setSafeOnClickListener {
SearchBottomSheet.newInstance().show(
(it.context as androidx.appcompat.app.AppCompatActivity).supportFragmentManager,
"search"
)
}
binding.homeUserAvatarContainer.setOnLongClickListener {
it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
ContextCompat.startActivity(
@@ -456,51 +463,58 @@ class HomeFragment : Fragment() {
var running = false
val live = Refresh.activity.getOrPut(1) { MutableLiveData(true) }
live.observe(viewLifecycleOwner)
{
if (!running && it) {
live.observe(viewLifecycleOwner) { shouldRefresh ->
if (!running && shouldRefresh) {
running = true
scope.launch {
withContext(Dispatchers.IO) {
//Get userData First
// Get user data first
Anilist.userid =
PrefManager.getNullableVal<String>(PrefName.AnilistUserId, null)
?.toIntOrNull()
if (Anilist.userid == null) {
getUserId(requireContext()) {
load()
}
} else {
CoroutineScope(Dispatchers.IO).launch {
withContext(Dispatchers.Main) {
getUserId(requireContext()) {
load()
}
}
} else {
getUserId(requireContext()) {
load()
}
}
model.loaded = true
CoroutineScope(Dispatchers.IO).launch {
model.setListImages()
}
var empty = true
val homeLayoutShow: List<Boolean> =
PrefManager.getVal(PrefName.HomeLayout)
model.initHomePage()
(array.indices).forEach { i ->
model.setListImages()
}
var empty = true
val homeLayoutShow: List<Boolean> = PrefManager.getVal(PrefName.HomeLayout)
withContext(Dispatchers.Main) {
homeLayoutShow.indices.forEach { i ->
if (homeLayoutShow.elementAt(i)) {
empty = false
} else withContext(Dispatchers.Main) {
} else {
containers[i].visibility = View.GONE
}
}
model.empty.postValue(empty)
}
val initHomePage = async(Dispatchers.IO) { model.initHomePage() }
val initUserStatus = async(Dispatchers.IO) { model.initUserStatus() }
initHomePage.await()
initUserStatus.await()
withContext(Dispatchers.Main) {
model.empty.postValue(empty)
binding.homeHiddenItemsContainer.visibility = View.GONE
}
live.postValue(false)
_binding?.homeRefresh?.isRefreshing = false
running = false
}
binding.homeHiddenItemsContainer.visibility = View.GONE
}
}
}
@@ -508,6 +522,7 @@ class HomeFragment : Fragment() {
if (!model.loaded) Refresh.activity[1]!!.postValue(true)
if (_binding != null) {
binding.homeNotificationCount.isVisible = Anilist.unreadNotificationCount > 0
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
binding.homeNotificationCount.text = Anilist.unreadNotificationCount.toString()
}
super.onResume()

View File

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

View File

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

View File

@@ -80,14 +80,23 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
updateAvatar()
trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
trendingBinding.notificationCount.text = Anilist.unreadNotificationCount.toString()
trendingBinding.searchBar.hint = "MANGA"
trendingBinding.searchBar.hint = binding.root.context.getString(R.string.search)
trendingBinding.searchBarText.setOnClickListener {
ContextCompat.startActivity(
it.context,
Intent(it.context, SearchActivity::class.java).putExtra("type", "MANGA"),
null
)
val context = binding.root.context
if (PrefManager.getVal(PrefName.AniMangaSearchDirect) && Anilist.token != null) {
ContextCompat.startActivity(
context,
Intent(context, SearchActivity::class.java).putExtra("type", "MANGA"),
null
)
} else {
SearchBottomSheet.newInstance().show(
(context as AppCompatActivity).supportFragmentManager,
"search"
)
}
}
trendingBinding.userAvatar.setSafeOnClickListener {
@@ -257,10 +266,10 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
adaptor: MediaAdaptor,
recyclerView: RecyclerView,
progress: View,
title: View ,
more: View ,
title: View,
more: View,
string: String,
media : MutableList<Media>
media: MutableList<Media>
) {
progress.visibility = View.GONE
recyclerView.adapter = adaptor
@@ -296,8 +305,8 @@ class MangaPageAdapter : RecyclerView.Adapter<MangaPageAdapter.MangaPageViewHold
fun updateNotificationCount() {
if (this::binding.isInitialized) {
trendingBinding.notificationCount.visibility =
if (Anilist.unreadNotificationCount > 0) View.VISIBLE else View.GONE
trendingBinding.notificationCount.isVisible = Anilist.unreadNotificationCount > 0
&& PrefManager.getVal<Boolean>(PrefName.ShowNotificationRedDot) == true
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) {
paint.color = if (int < booleanList.size && booleanList[int]) {
Color.GRAY
Color.GRAY
} else {
if (isUser) secondColor else primaryColor
}
@@ -58,7 +58,7 @@ class CircleView(context: Context, attrs: AttributeSet?) : View(context, attrs)
} else {
val effectiveAngle = totalAngle / parts
for (i in 0 until parts) {
val startAngle = i * (effectiveAngle + gapAngle) -90f
val startAngle = i * (effectiveAngle + gapAngle) - 90f
path.reset()
path.addArc(
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.booleanList = list
this.isUser = isUser

View File

@@ -9,13 +9,14 @@ import androidx.core.view.updateLayoutParams
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.api.Activity
import ani.dantotsu.databinding.ActivityStatusBinding
import ani.dantotsu.initActivity
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.home.status.listener.StoriesCallback
import ani.dantotsu.initActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.profile.User
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.Logger
class StatusActivity : AppCompatActivity(), StoriesCallback {
private lateinit var activity: ArrayList<User>
@@ -44,12 +45,20 @@ class StatusActivity : AppCompatActivity(), StoriesCallback {
val key = "activities"
val watchedActivity = PrefManager.getCustomVal<Set<Int>>(key, setOf())
val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity )
val startIndex = if ( startFrom > 0) startFrom else 0
binding.stories.setStoriesList(activity[position].activity, this, startIndex + 1)
if (activity.getOrNull(position) != null) {
val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity)
val startIndex = if (startFrom > 0) startFrom else 0
binding.stories.setStoriesList(
activityList = activity[position].activity,
startIndex = startIndex + 1
)
} else {
Logger.log("index out of bounds for position $position of size ${activity.size}")
finish()
}
}
private fun findFirstNonMatch(watchedActivity: Set<Int>, activity: List<Activity>): Int {
for (activityItem in activity) {
if (activityItem.id !in watchedActivity) {
@@ -58,13 +67,16 @@ class StatusActivity : AppCompatActivity(), StoriesCallback {
}
return -1
}
override fun onPause() {
super.onPause()
binding.stories.pause()
}
override fun onResume() {
super.onResume()
binding.stories.resume()
if (hasWindowFocus())
binding.stories.resume()
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
@@ -75,15 +87,16 @@ class StatusActivity : AppCompatActivity(), StoriesCallback {
binding.stories.pause()
}
}
override fun onStoriesEnd() {
position += 1
if (position < activity.size) {
val key = "activities"
val watchedActivity = PrefManager.getCustomVal<Set<Int>>(key, setOf())
val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity )
val startIndex= if ( startFrom > 0) startFrom else 0
val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity)
val startIndex = if (startFrom > 0) startFrom else 0
binding.stories.startAnimation(slideOutLeft)
binding.stories.setStoriesList(activity[position].activity, this, startIndex + 1)
binding.stories.setStoriesList(activity[position].activity, startIndex + 1)
binding.stories.startAnimation(slideInRight)
} else {
finish()
@@ -92,18 +105,19 @@ class StatusActivity : AppCompatActivity(), StoriesCallback {
override fun onStoriesStart() {
position -= 1
if (position >= 0) {
if (position >= 0 && activity[position].activity.isNotEmpty()) {
val key = "activities"
val watchedActivity = PrefManager.getCustomVal<Set<Int>>(key, setOf())
val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity )
val startIndex = if ( startFrom > 0) startFrom else 0
val startFrom = findFirstNonMatch(watchedActivity, activity[position].activity)
val startIndex = if (startFrom > 0) startFrom else 0
binding.stories.startAnimation(slideOutRight)
binding.stories.setStoriesList(activity[position].activity, this, startIndex + 1)
binding.stories.setStoriesList(activity[position].activity, startIndex + 1)
binding.stories.startAnimation(slideInLeft)
} else {
finish()
}
}
companion object {
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.UsersDialogFragment
import ani.dantotsu.profile.activity.ActivityItemBuilder
import ani.dantotsu.profile.activity.RepliesBottomDialog
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
@@ -48,7 +49,6 @@ import kotlin.math.abs
class Stories @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr), View.OnTouchListener {
private lateinit var activity: FragmentActivity
private lateinit var binding: FragmentStatusBinding
private lateinit var activityList: List<Activity>
private lateinit var storiesListener: StoriesCallback
@@ -74,16 +74,14 @@ class Stories @JvmOverloads constructor(
if (context is StoriesCallback) storiesListener = context as StoriesCallback
binding.leftTouchPanel.setOnTouchListener(this)
binding.rightTouchPanel.setOnTouchListener(this)
binding.touchPanel.setOnTouchListener(this)
}
fun setStoriesList(
activityList: List<Activity>, activity: FragmentActivity, startIndex: Int = 1
activityList: List<Activity>, startIndex: Int = 1
) {
this.activityList = activityList
this.activity = activity
this.storyIndex = startIndex
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() {
Logger.log("rightPanelTouch: $storyIndex")
if (storyIndex == activityList.size) {
@@ -359,6 +313,7 @@ class Stories @JvmOverloads constructor(
timer.resume()
}
@SuppressLint("ClickableViewAccessibility")
private fun loadStory(story: Activity) {
val key = "activities"
val set = PrefManager.getCustomVal<Set<Int>>(key, setOf()).plus((story.id))
@@ -374,6 +329,15 @@ class Stories @JvmOverloads constructor(
null
)
}
binding.textActivity.setOnTouchListener { v, event ->
onTouchView(v, event, true)
v.onTouchEvent(event)
}
binding.textActivityContainer.setOnTouchListener { v, event ->
onTouchView(v, event, true)
v.onTouchEvent(event)
}
fun visible(isList: Boolean) {
binding.textActivity.isVisible = !isList
binding.textActivityContainer.isVisible = !isList
@@ -397,15 +361,17 @@ class Stories @JvmOverloads constructor(
}
}
} ${story.progress ?: story.media?.title?.userPreferred} " +
if (
story.status?.contains("completed") == false &&
!story.status.contains("plans") &&
!story.status.contains("repeating")
) {
"of ${story.media?.title?.userPreferred}"
} else {
""
}
if (
story.status?.contains("completed") == false &&
!story.status.contains("plans") &&
!story.status.contains("repeating") &&
!story.status.contains("paused") &&
!story.status.contains("dropped")
) {
"of ${story.media?.title?.userPreferred}"
} else {
""
}
binding.infoText.text = text
val bannerAnimations: Boolean = PrefManager.getVal(PrefName.BannerAnimations)
blurImage(
@@ -421,7 +387,7 @@ class Stories @JvmOverloads constructor(
story.media?.id
),
ActivityOptionsCompat.makeSceneTransitionAnimation(
activity,
(it.context as FragmentActivity),
binding.coverImage,
ViewCompat.getTransitionName(binding.coverImage)!!
).toBundle()
@@ -455,22 +421,21 @@ class Stories @JvmOverloads constructor(
}
val likeColor = ContextCompat.getColor(context, R.color.yt_red)
val notLikeColor = ContextCompat.getColor(context, R.color.bg_opp)
binding.replyCount.text = story.replyCount.toString()
binding.activityReplies.setColorFilter(ContextCompat.getColor(context, R.color.bg_opp))
binding.activityRepliesContainer.setOnClickListener {
RepliesBottomDialog.newInstance(story.id)
.show(activity.supportFragmentManager, "replies")
.show((it.context as FragmentActivity).supportFragmentManager, "replies")
}
binding.activityLike.setColorFilter(if (story.isLiked == true) likeColor else notLikeColor)
binding.replyCount.text = story.replyCount.toString()
binding.activityLikeCount.text = story.likeCount.toString()
binding.activityReplies.setColorFilter(ContextCompat.getColor(context, R.color.bg_opp))
binding.activityLikeContainer.setOnClickListener {
like()
}
binding.activityLikeContainer.setOnLongClickListener {
val context = activity
UsersDialogFragment().apply {
userList(userList)
show(context.supportFragmentManager, "dialog")
show((it.context as FragmentActivity).supportFragmentManager, "dialog")
}
true
}
@@ -484,7 +449,7 @@ class Stories @JvmOverloads constructor(
val notLikeColor = ContextCompat.getColor(context, R.color.bg_opp)
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
scope.launch {
val res = Anilist.query.toggleLike(story.id, "ACTIVITY")
val res = Anilist.mutation.toggleLike(story.id, "ACTIVITY")
withContext(Dispatchers.Main) {
if (res != null) {
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
import android.content.Context
import android.content.Intent
import android.view.LayoutInflater
import android.view.ViewGroup
@@ -15,6 +14,8 @@ import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.profile.User
import ani.dantotsu.setAnimation
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.snackString
import ani.dantotsu.util.ActivityMarkdownCreator
class UserStatusAdapter(private val user: ArrayList<User>) :
RecyclerView.Adapter<UserStatusAdapter.UsersViewHolder>() {
@@ -23,6 +24,10 @@ class UserStatusAdapter(private val user: ArrayList<User>) :
RecyclerView.ViewHolder(binding.root) {
init {
itemView.setOnClickListener {
if (user[bindingAdapterPosition].activity.isEmpty()) {
snackString("No activity")
return@setOnClickListener
}
StatusActivity.user = user
ContextCompat.startActivity(
itemView.context,
@@ -34,14 +39,23 @@ class UserStatusAdapter(private val user: ArrayList<User>) :
)
}
itemView.setOnLongClickListener {
ContextCompat.startActivity(
itemView.context,
Intent(
if (user[bindingAdapterPosition].id == Anilist.userid) {
ContextCompat.startActivity(
itemView.context,
ProfileActivity::class.java
).putExtra("userId", user[bindingAdapterPosition].id),
null
)
Intent(itemView.context, ActivityMarkdownCreator::class.java)
.putExtra("type", "activity"),
null
)
} else {
ContextCompat.startActivity(
itemView.context,
Intent(
itemView.context,
ProfileActivity::class.java
).putExtra("userId", user[bindingAdapterPosition].id),
null
)
}
true
}
}
@@ -62,10 +76,15 @@ class UserStatusAdapter(private val user: ArrayList<User>) :
setAnimation(b.root.context, b.root)
val user = user[position]
b.profileUserAvatar.loadImage(user.pfp)
b.profileUserName.text = if (Anilist.userid == user.id) getAppString(R.string.your_story) else user.name
b.profileUserName.text =
if (Anilist.userid == user.id) getAppString(R.string.your_story) else user.name
val watchedActivity = PrefManager.getCustomVal<Set<Int>>("activities", setOf())
val booleanList = user.activity.map { watchedActivity.contains(it.id) }
b.profileUserStatusIndicator.setParts(user.activity.size, booleanList, user.id == Anilist.userid)
b.profileUserStatusIndicator.setParts(
user.activity.size,
booleanList,
user.id == Anilist.userid
)
}

View File

@@ -7,6 +7,12 @@ data class Author(
var name: String?,
var image: 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 character: ArrayList<Character>? = null
var character: ArrayList<Character>? = null,
var isFav: Boolean = false
) : Serializable

View File

@@ -1,11 +1,13 @@
package ani.dantotsu.media
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils.clamp
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.lifecycle.MutableLiveData
@@ -16,57 +18,127 @@ import androidx.recyclerview.widget.LinearLayoutManager
import ani.dantotsu.EmptyAdapter
import ani.dantotsu.R
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.loadImage
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.px
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import com.google.android.material.appbar.AppBarLayout
import io.noties.markwon.Markwon
import io.noties.markwon.SoftBreakAddsNewLinePlugin
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.math.abs
class AuthorActivity : AppCompatActivity() {
private lateinit var binding: ActivityAuthorBinding
class AuthorActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedListener {
private lateinit var binding: ActivityCharacterBinding
private val scope = lifecycleScope
private val model: OtherDetailsViewModel by viewModels()
private var author: Author? = null
private lateinit var author: Author
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?) {
super.onCreate(savedInstanceState)
ThemeManager(this).applyTheme()
binding = ActivityAuthorBinding.inflate(layoutInflater)
binding = ActivityCharacterBinding.inflate(layoutInflater)
setContentView(binding.root)
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 }
binding.studioRecycler.updatePadding(bottom = 64f.px + navBarHeight)
binding.studioTitle.isSelected = true
banner.updateLayoutParams { height += statusBarHeight }
binding.characterClose.updateLayoutParams<ViewGroup.MarginLayoutParams> { topMargin += statusBarHeight }
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.studioTitle.text = author?.name
binding.studioClose.setOnClickListener {
binding.characterClose.setOnClickListener {
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) {
if (it != null) {
author = it
loaded = true
binding.studioProgressBar.visibility = View.GONE
binding.studioRecycler.visibility = View.VISIBLE
if (author!!.yearMedia.isNullOrEmpty()) {
binding.studioRecycler.visibility = View.GONE
binding.characterProgress.visibility = View.GONE
binding.characterRecyclerView.visibility = View.VISIBLE
if (author.yearMedia.isNullOrEmpty()) {
binding.characterRecyclerView.visibility = View.GONE
}
val titlePosition = arrayListOf<Int>()
val concatAdapter = ConcatAdapter()
val map = author!!.yearMedia ?: return@observe
val map = author.yearMedia ?: return@observe
val keys = map.keys.toTypedArray()
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) {
val medias = map[keys[i]]!!
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(EmptyAdapter(empty))
}
binding.studioRecycler.adapter = concatAdapter
binding.studioRecycler.layoutManager = gridLayoutManager
binding.characterRecyclerView.adapter = concatAdapter
binding.characterRecyclerView.layoutManager = gridLayoutManager
binding.charactersRecycler.visibility = View.VISIBLE
binding.charactersText.visibility = View.VISIBLE
binding.charactersRecycler.adapter =
CharacterAdapter(author!!.character ?: arrayListOf())
binding.charactersRecycler.layoutManager =
binding.authorCharactersRecycler.visibility = View.VISIBLE
binding.AuthorCharactersText.visibility = View.VISIBLE
binding.authorCharactersRecycler.adapter =
CharacterAdapter(author.character ?: arrayListOf())
binding.authorCharactersRecycler.layoutManager =
LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
if (author!!.character.isNullOrEmpty()) {
binding.charactersRecycler.visibility = View.GONE
binding.charactersText.visibility = View.GONE
if (author.character.isNullOrEmpty()) {
binding.authorCharactersRecycler.visibility = View.GONE
binding.AuthorCharactersText.visibility = View.GONE
}
}
}
@@ -109,14 +185,28 @@ class AuthorActivity : AppCompatActivity() {
live.observe(this) {
if (it) {
scope.launch {
if (author != null)
withContext(Dispatchers.IO) { model.loadAuthor(author!!) }
withContext(Dispatchers.IO) { model.loadAuthor(author) }
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() {
if (Refresh.activity.containsKey(this.hashCode())) {
Refresh.activity.remove(this.hashCode())
@@ -125,7 +215,31 @@ class AuthorActivity : AppCompatActivity() {
}
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()
}
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
class AuthorAdapter(
private val authorList: ArrayList<Author>,
private val authorList: MutableList<Author>,
) : RecyclerView.Adapter<AuthorAdapter.AuthorViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AuthorViewHolder {
val binding =
@@ -26,7 +26,7 @@ class AuthorAdapter(
override fun onBindViewHolder(holder: AuthorViewHolder, position: Int) {
val binding = holder.binding
setAnimation(binding.root.context, holder.binding.root)
val author = authorList[position]
val author = authorList.getOrNull(position) ?: return
binding.itemCompactRelation.text = author.role
binding.itemCompactImage.loadImage(author.image)
binding.itemCompactTitle.text = author.name

View File

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

View File

@@ -9,13 +9,14 @@ import androidx.core.content.ContextCompat
import androidx.core.util.Pair
import androidx.core.view.ViewCompat
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 CharacterAdapter(
private val characterList: ArrayList<Character>
private val characterList: MutableList<Character>
) : RecyclerView.Adapter<CharacterAdapter.CharacterViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CharacterViewHolder {
val binding =
@@ -26,9 +27,8 @@ class CharacterAdapter(
override fun onBindViewHolder(holder: CharacterViewHolder, position: Int) {
val binding = holder.binding
setAnimation(binding.root.context, holder.binding.root)
val character = characterList[position]
val whitespace = "${character.role} "
character.voiceActor
val character = characterList.getOrNull(position) ?: return
val whitespace = "${if (character.role.lowercase() == "null") "" else character.role} "
binding.itemCompactRelation.text = whitespace
binding.itemCompactImage.loadImage(character.image)
binding.itemCompactTitle.text = character.name
@@ -55,6 +55,11 @@ class CharacterAdapter(
).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.math.MathUtils.clamp
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.lifecycle.MutableLiveData
@@ -45,6 +46,11 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
private lateinit var character: Character
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?) {
super.onCreate(savedInstanceState)
@@ -71,6 +77,11 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
binding.characterClose.setOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
binding.authorCharactersRecycler.isVisible = false
binding.AuthorCharactersText.isVisible = false
binding.authorCharacterDesc.isVisible = false
character = intent.getSerialized("character") ?: return
binding.characterTitle.text = character.name
banner.loadImage(character.banner)
@@ -158,11 +169,6 @@ class CharacterDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChang
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) {
if (mMaxScrollSize == 0) mMaxScrollSize = appBar.totalScrollRange
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.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.buildMarkwon
import ani.dantotsu.currActivity
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) :
RecyclerView.Adapter<CharacterDetailsAdapter.GenreViewHolder>() {
@@ -24,7 +22,9 @@ class CharacterDetailsAdapter(private val character: Character, private val acti
override fun onBindViewHolder(holder: GenreViewHolder, position: Int) {
val binding = holder.binding
val desc =
(if (character.age != "null") "${currActivity()!!.getString(R.string.age)} ${character.age}" else "") +
(if (character.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() != "")
"${currActivity()!!.getString(R.string.birthday)} ${character.dateOfBirth.toString()}" else "") +
(if (character.gender != "null")
@@ -41,8 +41,7 @@ class CharacterDetailsAdapter(private val character: Character, private val acti
} else "") + "\n" + character.description
binding.characterDesc.isTextSelectable
val markWon = Markwon.builder(activity).usePlugin(SoftBreakAddsNewLinePlugin.create())
.usePlugin(SpoilerPlugin()).build()
val markWon = buildMarkwon(activity)
markWon.setMarkdown(binding.characterDesc, desc.replace("~!", "||").replace("!~", "||"))
binding.voiceActorRecycler.adapter = AuthorAdapter(character.voiceActor ?: arrayListOf())
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
import android.graphics.Bitmap
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.api.FuzzyDate
import ani.dantotsu.connections.anilist.api.MediaEdge
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.Query
import ani.dantotsu.connections.mal.MAL
import ani.dantotsu.media.anime.Anime
import ani.dantotsu.media.manga.Manga
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 ani.dantotsu.connections.anilist.api.Media as ApiMedia
@@ -76,7 +84,7 @@ data class Media(
var nameMAL: String? = null,
var shareLink: String? = null,
var selected: Selected? = null,
var streamingEpisodes: List<MediaStreamingEpisode>? = null,
var idKitsu: String? = null,
var cameFromContinue: Boolean = false
@@ -129,6 +137,37 @@ data class Media(
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(
id = 0,
name = "No media found",

View File

@@ -251,10 +251,12 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
fun total() {
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) {
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}") } }
append(
if (media.anime != null) getString(R.string.episodes_out_of) else getString(
@@ -293,7 +295,7 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
binding.mediaTotal.visibility = View.VISIBLE
binding.mediaAddToList.text = userStatus
} else {
binding.mediaAddToList.setText(R.string.add)
binding.mediaAddToList.setText(R.string.add_list)
}
total()
binding.mediaAddToList.setOnClickListener {
@@ -372,7 +374,9 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
navBar.createTab(R.drawable.ic_round_comment_24, R.string.comments, R.id.comment)
navBar.addTab(infoTab)
navBar.addTab(watchTab)
navBar.addTab(commentTab)
if (PrefManager.getVal<Int>(PrefName.CommentsEnabled) == 1) {
navBar.addTab(commentTab)
}
if (model.continueMedia == null && media.cameFromContinue) {
model.continueMedia = PrefManager.getVal(PrefName.ContinueMedia)
selected = 1
@@ -424,7 +428,8 @@ class MediaDetailsActivity : AppCompatActivity(), AppBarLayout.OnOffsetChangedLi
}
override fun onResume() {
navBar.selectTabAt(selected)
if (::navBar.isInitialized)
navBar.selectTabAt(selected)
super.onResume()
}

View File

@@ -13,6 +13,7 @@ import ani.dantotsu.media.anime.Episode
import ani.dantotsu.media.anime.SelectorDialogFragment
import ani.dantotsu.media.manga.MangaChapter
import ani.dantotsu.others.AniSkip
import ani.dantotsu.others.Anify
import ani.dantotsu.others.Jikan
import ani.dantotsu.others.Kitsu
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>> =
MutableLiveData<Map<String, Episode>>(null)

View File

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

View File

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

View File

@@ -63,36 +63,24 @@ class MediaListDialogSmallFragment : BottomSheetDialogFragment() {
binding.mediaListContainer.updateLayoutParams<ViewGroup.MarginLayoutParams> { bottomMargin += navBarHeight }
val scope = viewLifecycleOwner.lifecycleScope
binding.mediaListDelete.setOnClickListener {
var id = media.userListId
viewLifecycleOwner.lifecycleScope.launch {
withContext(Dispatchers.IO) {
if (id != null) {
try {
Anilist.mutation.deleteList(id!!)
MAL.query.deleteList(media.anime != null, media.idMAL)
} catch (e: Exception) {
withContext(Dispatchers.Main) {
snackString(getString(R.string.delete_fail_reason, e.message))
}
return@withContext
}
} else {
val profile = Anilist.query.userMediaDetails(media)
profile.userListId?.let { listId ->
id = listId
Anilist.mutation.deleteList(listId)
MAL.query.deleteList(media.anime != null, media.idMAL)
}
}
}
withContext(Dispatchers.Main) {
if (id != null) {
scope.launch {
media.deleteFromList(scope, onSuccess = {
Refresh.all()
snackString(getString(R.string.deleted_from_list))
dismissAllowingStateLoss()
} else {
}, onError = { e ->
withContext(Dispatchers.Main) {
snackString(
getString(
R.string.delete_fail_reason, e.message
)
)
}
}, onNotFound = {
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.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import java.util.ArrayList
class MediaListViewActivity: AppCompatActivity() {
class MediaListViewActivity : AppCompatActivity() {
private lateinit var binding: ActivityMediaListViewBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -52,7 +51,8 @@ class MediaListViewActivity: AppCompatActivity() {
binding.listAppBar.setBackgroundColor(primaryColor)
binding.listTitle.setTextColor(primaryTextColor)
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
val view = PrefManager.getCustomVal("mediaView", 0)
var mediaView: View = when (view) {

View File

@@ -44,7 +44,10 @@ class MediaSocialAdapter(
profileUserName.text = user.name
profileInfo.apply {
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 ?: ""
}
visibility = View.VISIBLE
@@ -63,10 +66,12 @@ class MediaSocialAdapter(
profileCompactProgressContainer.visibility = View.VISIBLE
profileUserAvatar.setOnClickListener {
ContextCompat.startActivity(root.context,
ContextCompat.startActivity(
root.context,
Intent(root.context, ProfileActivity::class.java)
.putExtra("userId", user.id),
null)
null
)
}
profileUserAvatarContainer.setOnLongClickListener {
ImageViewDialog.newInstance(

View File

@@ -26,25 +26,50 @@ class OtherDetailsViewModel : ViewModel() {
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)
fun getCalendar(): LiveData<Map<String, MutableList<Media>>> = calendar
suspend fun loadCalendar() {
val curr = System.currentTimeMillis() / 1000
val res = Anilist.query.recentlyUpdated(curr - 86400, curr + (86400 * 6))
val df = DateFormat.getDateInstance(DateFormat.FULL)
val map = mutableMapOf<String, MutableList<Media>>()
val idMap = mutableMapOf<String, MutableList<Int>>()
res?.forEach {
val v = it.relation?.split(",")?.map { i -> i.toLong() }!!
val dateInfo = df.format(Date(v[1] * 1000))
val list = map.getOrPut(dateInfo) { mutableListOf() }
val idList = idMap.getOrPut(dateInfo) { mutableListOf() }
it.relation = "Episode ${v[0]}"
if (!idList.contains(it.id)) {
idList.add(it.id)
list.add(it)
suspend fun loadCalendar(showOnlyLibrary: Boolean = false) {
if (cachedAllCalendarData == null || cachedLibraryCalendarData == null) {
val curr = System.currentTimeMillis() / 1000
val res = Anilist.query.recentlyUpdated(curr - 86400, curr + (86400 * 6))
val df = DateFormat.getDateInstance(DateFormat.FULL)
val allMap = mutableMapOf<String, MutableList<Media>>()
val libraryMap = mutableMapOf<String, MutableList<Media>>()
val idMap = mutableMapOf<String, MutableList<Int>>()
val userId = Anilist.userid ?: 0
val userLibrary = Anilist.query.getMediaLists(true, userId)
val libraryMediaIds = userLibrary.flatMap { it.value }.map { it.id }
res.forEach {
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.content.Intent
import android.os.Bundle
import android.text.SpannableString
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
@@ -21,7 +20,7 @@ import ani.dantotsu.initActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.statusBarHeight
import ani.dantotsu.themes.ThemeManager
import ani.dantotsu.util.MarkdownCreatorActivity
import ani.dantotsu.util.ActivityMarkdownCreator
import com.xwray.groupie.GroupieAdapter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -59,7 +58,7 @@ class ReviewActivity : AppCompatActivity() {
binding.followFilterButton.setOnClickListener {
ContextCompat.startActivity(
this,
Intent(this, MarkdownCreatorActivity::class.java)
Intent(this, ActivityMarkdownCreator::class.java)
.putExtra("type", "review"),
null
)

View File

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

View File

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

View File

@@ -13,12 +13,18 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.connections.anilist.AniMangaSearchResults
import ani.dantotsu.connections.anilist.Anilist
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.initActivity
import ani.dantotsu.navBarHeight
import ani.dantotsu.profile.UsersAdapter
import ani.dantotsu.px
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
@@ -35,14 +41,25 @@ class SearchActivity : AppCompatActivity() {
val model: AnilistSearch by viewModels()
var style: Int = 0
lateinit var searchType: SearchType
private var screenWidth: Float = 0f
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 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)
override fun onCreate(savedInstanceState: Bundle?) {
@@ -59,39 +76,117 @@ class SearchActivity : AppCompatActivity() {
bottom = navBarHeight + 80f.px
)
style = PrefManager.getVal(PrefName.SearchStyle)
var listOnly: Boolean? = intent.getBooleanExtra("listOnly", false)
if (!listOnly!!) listOnly = null
val notSet = model.notSet
if (model.notSet) {
model.notSet = false
model.searchResults = SearchResults(
intent.getStringExtra("type") ?: "ANIME",
isAdult = if (Anilist.adult) intent.getBooleanExtra("hentai", false) else false,
onList = listOnly,
search = intent.getStringExtra("query"),
genres = intent.getStringExtra("genre")?.let { mutableListOf(it) },
tags = intent.getStringExtra("tag")?.let { mutableListOf(it) },
sort = intent.getStringExtra("sortBy"),
status = intent.getStringExtra("status"),
source = intent.getStringExtra("source"),
countryOfOrigin = intent.getStringExtra("country"),
season = intent.getStringExtra("season"),
seasonYear = 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
)
searchType = SearchType.fromString(intent.getStringExtra("type") ?: "ANIME")
when (searchType) {
SearchType.ANIME, SearchType.MANGA -> {
style = PrefManager.getVal(PrefName.SearchStyle)
var listOnly: Boolean? = intent.getBooleanExtra("listOnly", false)
if (!listOnly!!) listOnly = null
if (model.notSet) {
model.notSet = false
model.aniMangaSearchResults = AniMangaSearchResults(
intent.getStringExtra("type") ?: "ANIME",
isAdult = if (Anilist.adult) intent.getBooleanExtra(
"hentai",
false
) else false,
onList = listOnly,
search = intent.getStringExtra("query"),
genres = intent.getStringExtra("genre")?.let { mutableListOf(it) },
tags = intent.getStringExtra("tag")?.let { mutableListOf(it) },
sort = intent.getStringExtra("sortBy"),
status = intent.getStringExtra("status"),
source = intent.getStringExtra("source"),
countryOfOrigin = intent.getStringExtra("country"),
season = intent.getStringExtra("season"),
seasonYear = 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)
mediaAdaptor = MediaAdaptor(style, model.searchResults.results, this, matchParent = true)
headerAdaptor = SearchAdapter(this, model.searchResults.type)
headerAdaptor = if (searchType == SearchType.ANIME || searchType == SearchType.MANGA) {
SearchAdapter(this, searchType)
} else {
SupportingSearchAdapter(this, searchType)
}
val gridSize = (screenWidth / 120f).toInt()
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.adapter = concatAdapter
@@ -117,9 +232,9 @@ class SearchActivity : AppCompatActivity() {
RecyclerView.OnScrollListener() {
override fun onScrolled(v: RecyclerView, dx: Int, dy: Int) {
if (!v.canScrollVertically(1)) {
if (model.searchResults.hasNextPage && model.searchResults.results.isNotEmpty() && !loading) {
if (model.hasNextPage(searchType) && model.resultsIsNotEmpty(searchType) && !loading) {
scope.launch(Dispatchers.IO) {
model.loadNextPage(model.searchResults)
model.loadNextPage(searchType)
}
}
}
@@ -127,34 +242,110 @@ class SearchActivity : AppCompatActivity() {
}
})
model.getSearch().observe(this) {
if (it != null) {
model.searchResults.apply {
onList = it.onList
isAdult = it.isAdult
perPage = it.perPage
search = it.search
sort = it.sort
genres = it.genres
excludedGenres = it.excludedGenres
excludedTags = it.excludedTags
tags = it.tags
season = it.season
startYear = it.startYear
seasonYear = it.seasonYear
status = it.status
source = it.source
format = it.format
countryOfOrigin = it.countryOfOrigin
page = it.page
hasNextPage = it.hasNextPage
when (searchType) {
SearchType.ANIME, SearchType.MANGA -> {
model.getSearch<AniMangaSearchResults>(searchType).observe(this) {
if (it != null) {
model.aniMangaSearchResults.apply {
onList = it.onList
isAdult = it.isAdult
perPage = it.perPage
search = it.search
sort = it.sort
genres = it.genres
excludedGenres = it.excludedGenres
excludedTags = it.excludedTags
tags = it.tags
season = it.season
startYear = it.startYear
seasonYear = it.seasonYear
status = it.status
source = it.source
format = it.format
countryOfOrigin = it.countryOfOrigin
page = it.page
hasNextPage = it.hasNextPage
}
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
model.searchResults.results.addAll(it.results)
mediaAdaptor.notifyItemRangeInserted(prev, it.results.size)
SearchType.CHARACTER -> {
model.getSearch<CharacterSearchResults>(searchType).observe(this) {
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() {
searchTimer.cancel()
searchTimer.purge()
mediaAdaptor.notifyItemRangeRemoved(0, model.searchResults.results.size)
model.searchResults.results.clear()
when (searchType) {
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
}
@@ -188,10 +406,30 @@ class SearchActivity : AppCompatActivity() {
private var loading = false
fun search() {
headerAdaptor.setHistoryVisibility(false)
val size = model.searchResults.results.size
model.searchResults.results.clear()
val size = model.size(searchType)
model.clearResults(searchType)
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
@@ -202,7 +440,7 @@ class SearchActivity : AppCompatActivity() {
override fun run() {
scope.launch(Dispatchers.IO) {
loading = true
model.loadSearch(result)
model.loadSearch(searchType)
loading = false
}
}
@@ -213,8 +451,10 @@ class SearchActivity : AppCompatActivity() {
@SuppressLint("NotifyDataSetChanged")
fun recycler() {
mediaAdaptor.type = style
mediaAdaptor.notifyDataSetChanged()
if (searchType == SearchType.ANIME || searchType == SearchType.MANGA) {
mediaAdaptor.type = style
mediaAdaptor.notifyDataSetChanged()
}
}
var state: Parcelable? = null

View File

@@ -9,8 +9,6 @@ import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.PopupMenu
@@ -22,8 +20,8 @@ import androidx.recyclerview.widget.RecyclerView.HORIZONTAL
import ani.dantotsu.App.Companion.context
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.Anilist
import ani.dantotsu.connections.anilist.AnilistSearch.SearchType
import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.databinding.ItemSearchHeaderBinding
import ani.dantotsu.openLinkInBrowser
import ani.dantotsu.others.imagesearch.ImageSearchActivity
import ani.dantotsu.settings.saving.PrefManager
@@ -36,18 +34,11 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class SearchAdapter(private val activity: SearchActivity, private val type: String) :
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
class SearchAdapter(private val activity: SearchActivity, private val type: SearchType) :
HeaderInterface() {
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[1] -> R.drawable.ic_round_filter_peak_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)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SearchHeaderViewHolder {
val binding =
ItemSearchHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return SearchHeaderViewHolder(binding)
}
@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
@@ -79,6 +65,10 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
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)")
}
when (activity.style) {
0 -> {
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)) {
val startIconDrawableRes = R.drawable.ic_incognito_24
val startIconDrawable: Drawable? =
@@ -99,11 +89,11 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
binding.searchBar.startIconDrawable = startIconDrawable
}
var adult = activity.result.isAdult
var listOnly = activity.result.onList
var adult = activity.aniMangaResult.isAdult
var listOnly = activity.aniMangaResult.onList
binding.searchBarText.removeTextChangedListener(textWatcher)
binding.searchBarText.setText(activity.result.search)
binding.searchBarText.setText(activity.aniMangaResult.search)
binding.searchAdultCheck.isChecked = adult
binding.searchList.isChecked = listOnly == true
@@ -124,49 +114,49 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
popupMenu.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.sort_by_score -> {
activity.result.sort = Anilist.sortBy[0]
activity.aniMangaResult.sort = Anilist.sortBy[0]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_popular -> {
activity.result.sort = Anilist.sortBy[1]
activity.aniMangaResult.sort = Anilist.sortBy[1]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_trending -> {
activity.result.sort = Anilist.sortBy[2]
activity.aniMangaResult.sort = Anilist.sortBy[2]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_recent -> {
activity.result.sort = Anilist.sortBy[3]
activity.aniMangaResult.sort = Anilist.sortBy[3]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_a_z -> {
activity.result.sort = Anilist.sortBy[4]
activity.aniMangaResult.sort = Anilist.sortBy[4]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_z_a -> {
activity.result.sort = Anilist.sortBy[5]
activity.aniMangaResult.sort = Anilist.sortBy[5]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
}
R.id.sort_by_pure_pain -> {
activity.result.sort = Anilist.sortBy[6]
activity.aniMangaResult.sort = Anilist.sortBy[6]
activity.updateChips.invoke()
activity.search()
updateFilterTextViewDrawable()
@@ -177,14 +167,20 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
popupMenu.show()
true
}
if (activity.result.type != "ANIME") {
if (activity.aniMangaResult.type != "ANIME") {
binding.searchByImage.visibility = View.GONE
}
binding.searchByImage.setOnClickListener {
activity.startActivity(Intent(activity, ImageSearchActivity::class.java))
}
binding.clearHistory.setOnClickListener {
it.startAnimation(fadeOutAnimation())
it.visibility = View.GONE
searchHistoryAdapter.clearHistory()
}
updateClearHistoryVisibility()
fun searchTitle() {
activity.result.apply {
activity.aniMangaResult.apply {
search =
if (binding.searchBarText.text.toString() != "") binding.searchBarText.text.toString() else null
onList = listOnly
@@ -286,61 +282,12 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
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(
val activity: SearchActivity,
private val searchAdapter: SearchAdapter
) :
RecyclerView.Adapter<SearchChipAdapter.SearchChipViewHolder>() {
private var chips = activity.result.toChipList()
private var chips = activity.aniMangaResult.toChipList()
inner class SearchChipViewHolder(val binding: ItemChipBinding) :
RecyclerView.ViewHolder(binding.root)
@@ -357,7 +304,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
holder.binding.root.apply {
text = chip.text.replace("_", " ")
setOnClickListener {
activity.result.removeChip(chip)
activity.aniMangaResult.removeChip(chip)
update()
activity.search()
searchAdapter.updateFilterTextViewDrawable()
@@ -367,7 +314,7 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
@SuppressLint("NotifyDataSetChanged")
fun update() {
chips = activity.result.toChipList()
chips = activity.aniMangaResult.toChipList()
notifyDataSetChanged()
searchAdapter.updateFilterTextViewDrawable()
}
@@ -375,4 +322,3 @@ class SearchAdapter(private val activity: SearchActivity, private val type: Stri
override fun getItemCount(): Int = chips.size
}
}

View File

@@ -57,7 +57,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
}
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[1] -> R.drawable.ic_round_filter_peak_24
Anilist.sortBy[2] -> R.drawable.ic_round_star_graph_24
@@ -71,10 +71,10 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
}
private fun resetSearchFilter() {
activity.result.sort = null
activity.aniMangaResult.sort = null
binding.sortByFilter.setImageResource(R.drawable.ic_round_filter_alt_24)
startBounceZoomAnimation(binding.sortByFilter)
activity.result.countryOfOrigin = null
activity.aniMangaResult.countryOfOrigin = null
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_search_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
@@ -98,10 +98,10 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
activity = requireActivity() as SearchActivity
selectedGenres = activity.result.genres ?: mutableListOf()
exGenres = activity.result.excludedGenres ?: mutableListOf()
selectedTags = activity.result.tags ?: mutableListOf()
exTags = activity.result.excludedTags ?: mutableListOf()
selectedGenres = activity.aniMangaResult.genres ?: mutableListOf()
exGenres = activity.aniMangaResult.excludedGenres ?: mutableListOf()
selectedTags = activity.aniMangaResult.tags ?: mutableListOf()
exTags = activity.aniMangaResult.excludedTags ?: mutableListOf()
setSortByFilterImage()
binding.resetSearchFilter.setOnClickListener {
@@ -126,7 +126,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
resetSearchFilter()
CoroutineScope(Dispatchers.Main).launch {
activity.result.apply {
activity.aniMangaResult.apply {
status =
binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null }
source =
@@ -135,7 +135,7 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
season = binding.searchSeason.text.toString().ifBlank { null }
startYear = binding.searchYear.text.toString().toIntOrNull()
seasonYear = binding.searchYear.text.toString().toIntOrNull()
sort = activity.result.sort
sort = activity.aniMangaResult.sort
genres = selectedGenres
tags = selectedTags
excludedGenres = exGenres
@@ -155,43 +155,43 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
popupMenu.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
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)
startBounceZoomAnimation()
}
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)
startBounceZoomAnimation()
}
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)
startBounceZoomAnimation()
}
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)
startBounceZoomAnimation()
}
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)
startBounceZoomAnimation()
}
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)
startBounceZoomAnimation()
}
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)
startBounceZoomAnimation()
}
@@ -212,25 +212,25 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
}
R.id.country_china -> {
activity.result.countryOfOrigin = "CN"
activity.aniMangaResult.countryOfOrigin = "CN"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_china_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
R.id.country_south_korea -> {
activity.result.countryOfOrigin = "KR"
activity.aniMangaResult.countryOfOrigin = "KR"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_south_korea_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
R.id.country_japan -> {
activity.result.countryOfOrigin = "JP"
activity.aniMangaResult.countryOfOrigin = "JP"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_japan_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
R.id.country_taiwan -> {
activity.result.countryOfOrigin = "TW"
activity.aniMangaResult.countryOfOrigin = "TW"
binding.countryFilter.setImageResource(R.drawable.ic_round_globe_taiwan_googlefonts)
startBounceZoomAnimation(binding.countryFilter)
}
@@ -241,18 +241,18 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
}
binding.searchFilterApply.setOnClickListener {
activity.result.apply {
activity.aniMangaResult.apply {
status = binding.searchStatus.text.toString().replace(" ", "_").ifBlank { null }
source = binding.searchSource.text.toString().replace(" ", "_").ifBlank { null }
format = binding.searchFormat.text.toString().ifBlank { null }
season = binding.searchSeason.text.toString().ifBlank { null }
if (activity.result.type == "ANIME") {
if (activity.aniMangaResult.type == "ANIME") {
seasonYear = binding.searchYear.text.toString().toIntOrNull()
} else {
startYear = binding.searchYear.text.toString().toIntOrNull()
}
sort = activity.result.sort
countryOfOrigin = activity.result.countryOfOrigin
sort = activity.aniMangaResult.sort
countryOfOrigin = activity.aniMangaResult.countryOfOrigin
genres = selectedGenres
tags = selectedTags
excludedGenres = exGenres
@@ -266,8 +266,8 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
dismiss()
}
val format =
if (activity.result.type == "ANIME") Anilist.animeStatus else Anilist.mangaStatus
binding.searchStatus.setText(activity.result.status?.replace("_", " "))
if (activity.aniMangaResult.type == "ANIME") Anilist.animeStatus else Anilist.mangaStatus
binding.searchStatus.setText(activity.aniMangaResult.status?.replace("_", " "))
binding.searchStatus.setAdapter(
ArrayAdapter(
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(
ArrayAdapter(
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(
ArrayAdapter(
binding.root.context,
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") {
binding.searchYear.setText(activity.result.seasonYear?.toString())
if (activity.aniMangaResult.type == "ANIME") {
binding.searchYear.setText(activity.aniMangaResult.seasonYear?.toString())
} else {
binding.searchYear.setText(activity.result.startYear?.toString())
binding.searchYear.setText(activity.aniMangaResult.startYear?.toString())
}
binding.searchYear.setAdapter(
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 {
binding.searchSeason.setText(activity.result.season)
binding.searchSeason.setText(activity.aniMangaResult.season)
binding.searchSeason.setAdapter(
ArrayAdapter(
binding.root.context,
@@ -346,7 +346,9 @@ class SearchFilterBottomDialog : BottomSheetDialogFragment() {
binding.searchGenresGrid.isChecked = false
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()
chip.isChecked = selectedTags.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.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.connections.anilist.AnilistSearch.SearchType
import ani.dantotsu.databinding.ItemSearchHistoryBinding
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.SharedPreferenceStringSetLiveData
import java.util.Locale
import ani.dantotsu.settings.saving.SharedPreferenceClassLiveData
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>(
DIFF_CALLBACK_INSTALLED
) {
private var searchHistoryLiveData: SharedPreferenceStringSetLiveData? = null
private var searchHistory: MutableSet<String>? = null
private var historyType: PrefName = when (type.lowercase(Locale.ROOT)) {
"anime" -> PrefName.AnimeSearchHistory
"manga" -> PrefName.MangaSearchHistory
else -> throw IllegalArgumentException("Invalid type")
private var searchHistoryLiveData: SharedPreferenceClassLiveData<List<SearchHistory>>? = null
private var searchHistory: MutableList<SearchHistory>? = null
private var historyType: PrefName = when (type) {
SearchType.ANIME -> PrefName.SortedAnimeSH
SearchType.MANGA -> PrefName.SortedMangaSH
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 {
searchHistoryLiveData =
PrefManager.getLiveVal(historyType, mutableSetOf<String>()).asLiveStringSet()
searchHistoryLiveData?.observeForever {
searchHistory = it.toMutableSet()
submitList(searchHistory?.toList())
PrefManager.getLiveVal(historyType, mutableListOf<SearchHistory>()).asLiveClass()
searchHistoryLiveData?.observeForever { data ->
searchHistory = data.toMutableList()
submitList(searchHistory?.sorted())
}
}
fun remove(item: String) {
searchHistory?.remove(item)
searchHistory?.let { list ->
list.removeAll { it.search == item }
}
PrefManager.setVal(historyType, searchHistory)
submitList(searchHistory?.toList())
submitList(searchHistory?.sorted())
}
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
searchHistory?.add(item)
submitList(searchHistory?.toList())
searchHistory?.add(SearchHistory(item, System.currentTimeMillis()))
if ((searchHistory?.size ?: 0) > maxSize) {
searchHistory?.removeAt(
searchHistory?.sorted()?.lastIndex ?: 0
)
}
submitList(searchHistory?.sorted())
PrefManager.setVal(historyType, searchHistory)
}
fun clearHistory() {
searchHistory?.clear()
PrefManager.setVal(historyType, searchHistory)
submitList(searchHistory?.sorted())
}
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int

View File

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

@@ -72,14 +72,14 @@ class SubtitleDownloader {
val client = Injekt.get<NetworkHelper>().client
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")
return
}
reponse.body.byteStream().use { input ->
response.body.byteStream().use { input ->
subtitleFile.openOutputStream(context, false).use { output ->
if (output == null) throw Exception("Could not open output stream")
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 kitsuEpisodes: Map<String, Episode>? = null,
var fillerEpisodes: Map<String, Episode>? = null,
var anifyEpisodes: Map<String, Episode>? = null,
) : Serializable

View File

@@ -8,8 +8,8 @@ import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageButton
import android.widget.LinearLayout
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getString
import androidx.core.content.ContextCompat.startActivity
import androidx.core.view.isGone
import androidx.core.view.isVisible
@@ -18,9 +18,10 @@ import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.FileUrl
import ani.dantotsu.R
import ani.dantotsu.currActivity
import ani.dantotsu.currContext
import ani.dantotsu.databinding.DialogLayoutBinding
import ani.dantotsu.databinding.ItemAnimeWatchBinding
import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.databinding.ItemMediaSourceBinding
import ani.dantotsu.displayTimer
import ani.dantotsu.isOnline
import ani.dantotsu.loadImage
@@ -33,12 +34,15 @@ import ani.dantotsu.others.LanguageMapper
import ani.dantotsu.others.webview.CookieCatcher
import ani.dantotsu.parsers.AnimeSources
import ani.dantotsu.parsers.DynamicAnimeParser
import ani.dantotsu.parsers.OfflineAnimeParser
import ani.dantotsu.parsers.WatchSources
import ani.dantotsu.px
import ani.dantotsu.settings.FAQActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.toast
import ani.dantotsu.util.customAlertDialog
import com.google.android.material.chip.Chip
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_SUBSCRIPTION_CHECK
@@ -54,16 +58,14 @@ class AnimeWatchAdapter(
) : RecyclerView.Adapter<AnimeWatchAdapter.ViewHolder>() {
private var autoSelect = true
var subscribe: MediaDetailsActivity.PopImageButton? = null
private var _binding: ItemAnimeWatchBinding? = null
private var _binding: ItemMediaSourceBinding? = null
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)
}
private var nestedDialog: AlertDialog? = null
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding = holder.binding
_binding = binding
@@ -75,7 +77,7 @@ class AnimeWatchAdapter(
null
)
}
//Youtube
// Youtube
if (media.anime?.youtube != null && PrefManager.getVal(PrefName.ShowYtButton)) {
binding.animeSourceYT.visibility = View.VISIBLE
binding.animeSourceYT.setOnClickListener {
@@ -89,7 +91,7 @@ class AnimeWatchAdapter(
R.string.subbed
)
//PreferDub
// PreferDub
var changing = false
binding.animeSourceDubbed.setOnCheckedChangeListener { _, isChecked ->
binding.animeSourceDubbedText.text =
@@ -99,8 +101,8 @@ class AnimeWatchAdapter(
if (!changing) fragment.onDubClicked(isChecked)
}
//Wrong Title
binding.animeSourceSearch.setOnClickListener {
// Wrong Title
binding.mediaSourceSearch.setOnClickListener {
SourceSearchDialogFragment().show(
fragment.requireActivity().supportFragmentManager,
null
@@ -108,37 +110,37 @@ class AnimeWatchAdapter(
}
val offline = !isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode)
binding.animeSourceNameContainer.isGone = offline
binding.animeSourceSettings.isGone = offline
binding.animeSourceSearch.isGone = offline
binding.animeSourceTitle.isGone = offline
binding.mediaSourceNameContainer.isGone = offline
binding.mediaSourceSettings.isGone = offline
binding.mediaSourceSearch.isGone = offline
binding.mediaSourceTitle.isGone = offline
//Source Selection
// Source Selection
var source =
media.selected!!.sourceIndex.let { if (it >= watchSources.names.size) 0 else it }
setLanguageList(media.selected!!.langIndex, source)
if (watchSources.names.isNotEmpty() && source in 0 until watchSources.names.size) {
binding.animeSource.setText(watchSources.names[source])
binding.mediaSource.setText(watchSources.names[source])
watchSources[source].apply {
this.selectDub = media.selected!!.preferDub
binding.animeSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
binding.mediaSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.mediaSourceTitle.text = it } }
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
}
}
binding.animeSource.setAdapter(
binding.mediaSource.setAdapter(
ArrayAdapter(
fragment.requireContext(),
R.layout.item_dropdown,
watchSources.names
)
)
binding.animeSourceTitle.isSelected = true
binding.animeSource.setOnItemClickListener { _, _, i, _ ->
binding.mediaSourceTitle.isSelected = true
binding.mediaSource.setOnItemClickListener { _, _, i, _ ->
fragment.onSourceChange(i).apply {
binding.animeSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
binding.mediaSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.mediaSourceTitle.text = it } }
changing = true
binding.animeSourceDubbed.isChecked = selectDub
changing = false
@@ -150,15 +152,15 @@ class AnimeWatchAdapter(
fragment.loadEpisodes(i, false)
}
binding.animeSourceLanguage.setOnItemClickListener { _, _, i, _ ->
binding.mediaSourceLanguage.setOnItemClickListener { _, _, i, _ ->
// Check if 'extension' and 'selected' properties exist and are accessible
(watchSources[source] as? DynamicAnimeParser)?.let { ext ->
ext.sourceLanguage = i
fragment.onLangChange(i)
fragment.onSourceChange(media.selected!!.sourceIndex).apply {
binding.animeSourceTitle.text = showUserText
binding.mediaSourceTitle.text = showUserText
showUserTextListener =
{ MainScope().launch { binding.animeSourceTitle.text = it } }
{ MainScope().launch { binding.mediaSourceTitle.text = it } }
changing = true
binding.animeSourceDubbed.isChecked = selectDub
changing = false
@@ -170,19 +172,19 @@ class AnimeWatchAdapter(
} ?: run { }
}
//settings
binding.animeSourceSettings.setOnClickListener {
// Settings
binding.mediaSourceSettings.setOnClickListener {
(watchSources[source] as? DynamicAnimeParser)?.let { ext ->
fragment.openSettings(ext.extension)
}
}
//Icons
// Icons
//subscribe
// Subscribe
subscribe = MediaDetailsActivity.PopImageButton(
fragment.lifecycleScope,
binding.animeSourceSubscribe,
binding.mediaSourceSubscribe,
R.drawable.ic_round_notifications_active_24,
R.drawable.ic_round_notifications_none_24,
R.color.bg_opp,
@@ -190,125 +192,164 @@ class AnimeWatchAdapter(
fragment.subscribed,
true
) {
fragment.onNotificationPressed(it, binding.animeSource.text.toString())
fragment.onNotificationPressed(it, binding.mediaSource.text.toString())
}
subscribeButton(false)
binding.animeSourceSubscribe.setOnLongClickListener {
binding.mediaSourceSubscribe.setOnLongClickListener {
openSettings(fragment.requireContext(), CHANNEL_SUBSCRIPTION_CHECK)
}
//Nested Button
binding.animeNestedButton.setOnClickListener {
val dialogView =
LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null)
val dialogBinding = DialogLayoutBinding.bind(dialogView)
var refresh = false
var run = false
var reversed = media.selected!!.recyclerReversed
var style =
media.selected!!.recyclerStyle ?: PrefManager.getVal(PrefName.AnimeDefaultView)
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
dialogBinding.animeSourceTop.setOnClickListener {
reversed = !reversed
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
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)
// Nested Button
binding.mediaNestedButton.setOnClickListener {
val dialogBinding = DialogLayoutBinding.inflate(fragment.layoutInflater)
dialogBinding.apply {
var refresh = false
var run = false
var reversed = media.selected!!.recyclerReversed
var style =
media.selected!!.recyclerStyle ?: PrefManager.getVal(PrefName.AnimeDefaultView)
mediaSourceTop.rotation = if (reversed) -90f else 90f
sortText.text = if (reversed) "Down to Up" else "Up to Down"
mediaSourceTop.setOnClickListener {
reversed = !reversed
mediaSourceTop.rotation = if (reversed) -90f else 90f
sortText.text = if (reversed) "Down to Up" else "Up to Down"
run = true
}
//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()
// Grids
var selected = when (style) {
0 -> mediaSourceList
1 -> mediaSourceGrid
2 -> mediaSourceCompact
else -> mediaSourceList
}
when (style) {
0 -> layoutText.setText(R.string.list)
1 -> layoutText.setText(R.string.grid)
2 -> layoutText.setText(R.string.compact)
else -> mediaSourceList
}
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()
//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) {
subscribe?.enabled(enabled)
}
//Chips
// Chips
fun updateChips(limit: Int, names: Array<String>, arr: Array<Int>, selected: Int = 0) {
val binding = _binding
if (binding != null) {
@@ -319,13 +360,13 @@ class AnimeWatchAdapter(
val chip =
ItemChipBinding.inflate(
LayoutInflater.from(fragment.context),
binding.animeSourceChipGroup,
binding.mediaSourceChipGroup,
false
).root
chip.isCheckable = true
fun selected() {
chip.isChecked = true
binding.animeWatchChipScroll.smoothScrollTo(
binding.mediaWatchChipScroll.smoothScrollTo(
(chip.left - screenWidth / 2) + (chip.width / 2),
0
)
@@ -344,14 +385,14 @@ class AnimeWatchAdapter(
selected()
fragment.onChipClicked(position, limit * (position), last - 1)
}
binding.animeSourceChipGroup.addView(chip)
binding.mediaSourceChipGroup.addView(chip)
if (selected == position) {
selected()
select = chip
}
}
if (select != null)
binding.animeWatchChipScroll.apply {
binding.mediaWatchChipScroll.apply {
post {
scrollTo(
(select.left - screenWidth / 2) + (select.width / 2),
@@ -363,7 +404,7 @@ class AnimeWatchAdapter(
}
fun clearChips() {
_binding?.animeSourceChipGroup?.removeAllViews()
_binding?.mediaSourceChipGroup?.removeAllViews()
}
fun handleEpisodes() {
@@ -379,15 +420,15 @@ class AnimeWatchAdapter(
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
if (episodes.contains(continueEp)) {
binding.animeSourceContinue.visibility = View.VISIBLE
binding.sourceContinue.visibility = View.VISIBLE
handleProgress(
binding.itemEpisodeProgressCont,
binding.itemEpisodeProgress,
binding.itemEpisodeProgressEmpty,
binding.itemMediaProgressCont,
binding.itemMediaProgress,
binding.itemMediaProgressEmpty,
media.id,
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
)
) {
@@ -395,9 +436,9 @@ class AnimeWatchAdapter(
if (e != -1 && e + 1 < episodes.size) {
continueEp = episodes[e + 1]
handleProgress(
binding.itemEpisodeProgressCont,
binding.itemEpisodeProgress,
binding.itemEpisodeProgressEmpty,
binding.itemMediaProgressCont,
binding.itemMediaProgress,
binding.itemMediaProgressEmpty,
media.id,
continueEp
)
@@ -407,51 +448,65 @@ class AnimeWatchAdapter(
val cleanedTitle = ep.title?.let { MediaNameAdapter.removeEpisodeNumber(it) }
binding.itemEpisodeImage.loadImage(
binding.itemMediaImage.loadImage(
ep.thumb ?: FileUrl[media.banner ?: media.cover], 0
)
if (ep.filler) binding.itemEpisodeFillerView.visibility = View.VISIBLE
binding.animeSourceContinueText.text =
binding.mediaSourceContinueText.text =
currActivity()!!.getString(
R.string.continue_episode, ep.number, if (ep.filler)
currActivity()!!.getString(R.string.filler_tag)
else
"", cleanedTitle
)
binding.animeSourceContinue.setOnClickListener {
binding.sourceContinue.setOnClickListener {
fragment.onEpisodeClick(continueEp)
}
if (fragment.continueEp) {
if (
(binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams)
(binding.itemMediaProgress.layoutParams as LinearLayout.LayoutParams)
.weight < PrefManager.getVal<Float>(PrefName.WatchPercentage)
) {
binding.animeSourceContinue.performClick()
binding.sourceContinue.performClick()
fragment.continueEp = false
}
}
} 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()
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
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
binding.animeSource.setText(
binding.animeSource.adapter
binding.mediaSource.setText(
binding.mediaSource.adapter
.getItem(nextIndex).toString(), false
)
fragment.onSourceChange(nextIndex).apply {
binding.animeSourceTitle.text = showUserText
binding.mediaSourceTitle.text = showUserText
showUserTextListener =
{ MainScope().launch { binding.animeSourceTitle.text = it } }
{ MainScope().launch { binding.mediaSourceTitle.text = it } }
binding.animeSourceDubbed.isChecked = selectDub
binding.animeSourceDubbedCont.isVisible = isDubAvailableSeparately()
setLanguageList(0, nextIndex)
@@ -460,13 +515,13 @@ class AnimeWatchAdapter(
fragment.loadEpisodes(nextIndex, false)
}
}
binding.animeSource.setOnClickListener { autoSelect = false }
binding.mediaSource.setOnClickListener { autoSelect = false }
} else {
binding.animeSourceContinue.visibility = View.GONE
binding.animeSourceNotFound.visibility = View.GONE
binding.sourceContinue.visibility = View.GONE
binding.sourceNotFound.visibility = View.GONE
binding.faqbutton.visibility = View.GONE
clearChips()
binding.animeSourceProgressBar.visibility = View.VISIBLE
binding.sourceProgressBar.visibility = View.VISIBLE
}
}
}
@@ -480,9 +535,9 @@ class AnimeWatchAdapter(
ext.sourceLanguage = lang
}
try {
binding?.animeSourceLanguage?.setText(parser.extension.sources[lang].lang)
binding?.mediaSourceLanguage?.setText(parser.extension.sources[lang].lang)
} catch (e: IndexOutOfBoundsException) {
binding?.animeSourceLanguage?.setText(
binding?.mediaSourceLanguage?.setText(
parser.extension.sources.firstOrNull()?.lang ?: "Unknown"
)
}
@@ -493,9 +548,9 @@ class AnimeWatchAdapter(
)
val items = adapter.count
binding?.animeSourceLanguageContainer?.visibility =
binding?.mediaSourceLanguageContainer?.visibility =
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
inner class ViewHolder(val binding: ItemAnimeWatchBinding) :
inner class ViewHolder(val binding: ItemMediaSourceBinding) :
RecyclerView.ViewHolder(binding.root) {
init {
displayTimer(media, binding.animeSourceContainer)

View File

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

View File

@@ -1,6 +1,5 @@
package ani.dantotsu.media.anime
import android.app.AlertDialog
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@@ -13,7 +12,6 @@ import androidx.media3.common.util.UnstableApi
import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.connections.updateProgress
import ani.dantotsu.currContext
import ani.dantotsu.databinding.ItemEpisodeCompactBinding
import ani.dantotsu.databinding.ItemEpisodeGridBinding
import ani.dantotsu.databinding.ItemEpisodeListBinding
@@ -23,6 +21,7 @@ import ani.dantotsu.media.MediaNameAdapter
import ani.dantotsu.media.MediaType
import ani.dantotsu.setAnimation
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.util.customAlertDialog
import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl
import kotlinx.coroutines.delay
@@ -106,8 +105,8 @@ class EpisodeAdapter(
val thumb =
ep.thumb?.let { if (it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null }
Glide.with(binding.itemEpisodeImage).load(thumb ?: media.cover).override(400, 0)
.into(binding.itemEpisodeImage)
Glide.with(binding.itemMediaImage).load(thumb ?: media.cover).override(400, 0)
.into(binding.itemMediaImage)
binding.itemEpisodeNumber.text = ep.number
binding.itemEpisodeTitle.text = if (ep.number == title) "Episode $title" else title
@@ -140,9 +139,9 @@ class EpisodeAdapter(
}
handleProgress(
binding.itemEpisodeProgressCont,
binding.itemEpisodeProgress,
binding.itemEpisodeProgressEmpty,
binding.itemMediaProgressCont,
binding.itemMediaProgress,
binding.itemMediaProgressEmpty,
media.id,
ep.number
)
@@ -154,8 +153,8 @@ class EpisodeAdapter(
val thumb =
ep.thumb?.let { if (it.url.isNotEmpty()) GlideUrl(it.url) { it.headers } else null }
Glide.with(binding.itemEpisodeImage).load(thumb ?: media.cover).override(400, 0)
.into(binding.itemEpisodeImage)
Glide.with(binding.itemMediaImage).load(thumb ?: media.cover).override(400, 0)
.into(binding.itemMediaImage)
binding.itemEpisodeNumber.text = ep.number
binding.itemEpisodeTitle.text = title
@@ -183,9 +182,9 @@ class EpisodeAdapter(
binding.itemEpisodeViewed.visibility = View.GONE
}
handleProgress(
binding.itemEpisodeProgressCont,
binding.itemEpisodeProgress,
binding.itemEpisodeProgressEmpty,
binding.itemMediaProgressCont,
binding.itemMediaProgress,
binding.itemMediaProgressEmpty,
media.id,
ep.number
)
@@ -208,9 +207,9 @@ class EpisodeAdapter(
}
}
handleProgress(
binding.itemEpisodeProgressCont,
binding.itemEpisodeProgress,
binding.itemEpisodeProgressEmpty,
binding.itemMediaProgressCont,
binding.itemMediaProgress,
binding.itemMediaProgressEmpty,
media.id,
ep.number
)
@@ -318,16 +317,14 @@ class EpisodeAdapter(
fragment.onAnimeEpisodeStopDownloadClick(episodeNumber)
return@setOnClickListener
} else if (downloadedEpisodes.contains(episodeNumber)) {
val builder = AlertDialog.Builder(currContext(), R.style.MyPopup)
builder.setTitle("Delete Episode")
builder.setMessage("Are you sure you want to delete Episode ${episodeNumber}?")
builder.setPositiveButton("Yes") { _, _ ->
fragment.onAnimeEpisodeRemoveDownloadClick(episodeNumber)
}
builder.setNegativeButton("No") { _, _ ->
}
val dialog = builder.show()
dialog.window?.setDimAmount(0.8f)
binding.root.context.customAlertDialog().apply {
setTitle("Delete Episode")
setMessage("Are you sure you want to delete Episode $episodeNumber?")
setPosButton(R.string.yes) {
fragment.onAnimeEpisodeRemoveDownloadClick(episodeNumber)
}
setNegButton(R.string.no)
}.show()
return@setOnClickListener
} else {
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.app.Activity
import android.app.AlertDialog
import android.content.ActivityNotFoundException
import android.content.ComponentName
import android.content.DialogInterface
@@ -444,15 +443,12 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
if (subtitles.isNotEmpty()) {
val subtitleNames = subtitles.map { it.language }
var subtitleToDownload: Subtitle? = null
val alertDialog = AlertDialog.Builder(context, R.style.MyPopup)
.setTitle(R.string.download_subtitle)
.setSingleChoiceItems(
subtitleNames.toTypedArray(),
-1
) { _, which ->
requireActivity().customAlertDialog().apply {
setTitle(R.string.download_subtitle)
singleChoiceItems(subtitleNames.toTypedArray()) { which ->
subtitleToDownload = subtitles[which]
}
.setPositiveButton(R.string.download) { dialog, _ ->
setPosButton(R.string.download) {
scope.launch {
if (subtitleToDownload != null) {
SubtitleDownloader.downloadSubtitle(
@@ -466,13 +462,9 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
)
}
}
dialog.dismiss()
}
.setNegativeButton(R.string.cancel) { dialog, _ ->
dialog.dismiss()
}
.show()
alertDialog.window?.setDimAmount(0.8f)
setNegButton(R.string.cancel) {}
}.show()
} else {
snackString(R.string.no_subtitles_available)
}
@@ -490,7 +482,7 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
)
} else {
val downloadAddonManager: DownloadAddonManager = Injekt.get()
if (!downloadAddonManager.isAvailable()){
if (!downloadAddonManager.isAvailable()) {
val context = currContext() ?: requireContext()
context.customAlertDialog().apply {
setTitle(R.string.download_addon_not_installed)
@@ -571,70 +563,73 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
snackString(R.string.no_video_selected)
}
}
fun checkAudioTracks() {
val audioTracks = extractor.audioTracks.map { it.lang }
if (audioTracks.isNotEmpty()) {
val audioNamesArray = audioTracks.toTypedArray()
val checkedItems = BooleanArray(audioNamesArray.size) { false }
val alertDialog = AlertDialog.Builder(currContext, R.style.MyPopup)
.setTitle(R.string.download_audio_tracks)
.setMultiChoiceItems(audioNamesArray, checkedItems) { _, which, isChecked ->
val audioPair = Pair(extractor.audioTracks[which].url, extractor.audioTracks[which].lang)
if (isChecked) {
selectedAudioTracks.add(audioPair)
} else {
selectedAudioTracks.remove(audioPair)
currContext.customAlertDialog().apply { // ToTest
setTitle(R.string.download_audio_tracks)
multiChoiceItems(audioNamesArray, checkedItems) {
it.forEachIndexed { index, isChecked ->
val audioPair = Pair(
extractor.audioTracks[index].url,
extractor.audioTracks[index].lang
)
if (isChecked) {
selectedAudioTracks.add(audioPair)
} else {
selectedAudioTracks.remove(audioPair)
}
}
}
.setPositiveButton(R.string.download) { _, _ ->
dialog?.dismiss()
setPosButton(R.string.download) {
go()
}
.setNegativeButton(R.string.skip) { dialog, _ ->
setNegButton(R.string.skip) {
selectedAudioTracks = mutableListOf()
go()
dialog.dismiss()
}
.setNeutralButton(R.string.cancel) { dialog, _ ->
setNeutralButton(R.string.cancel) {
selectedAudioTracks = mutableListOf()
dialog.dismiss()
}
.show()
alertDialog.window?.setDimAmount(0.8f)
show()
}
} else {
go()
}
}
if (subtitles.isNotEmpty()) {
if (subtitles.isNotEmpty()) { // ToTest
val subtitleNamesArray = subtitleNames.toTypedArray()
val checkedItems = BooleanArray(subtitleNamesArray.size) { false }
val alertDialog = AlertDialog.Builder(currContext, R.style.MyPopup)
.setTitle(R.string.download_subtitle)
.setMultiChoiceItems(subtitleNamesArray, checkedItems) { _, which, isChecked ->
val subtitlePair = Pair(subtitles[which].file.url, subtitles[which].language)
if (isChecked) {
selectedSubtitles.add(subtitlePair)
} else {
selectedSubtitles.remove(subtitlePair)
currContext.customAlertDialog().apply {
setTitle(R.string.download_subtitle)
multiChoiceItems(subtitleNamesArray, checkedItems) {
it.forEachIndexed { index, isChecked ->
val subtitlePair =
Pair(subtitles[index].file.url, subtitles[index].language)
if (isChecked) {
selectedSubtitles.add(subtitlePair)
} else {
selectedSubtitles.remove(subtitlePair)
}
}
}
.setPositiveButton(R.string.download) { _, _ ->
dialog?.dismiss()
setPosButton(R.string.download) {
checkAudioTracks()
}
.setNegativeButton(R.string.skip) { dialog, _ ->
setNegButton(R.string.skip) {
selectedSubtitles = mutableListOf()
checkAudioTracks()
dialog.dismiss()
}
.setNeutralButton(R.string.cancel) { dialog, _ ->
setNeutralButton(R.string.cancel) {
selectedSubtitles = mutableListOf()
dialog.dismiss()
}
.show()
alertDialog.window?.setDimAmount(0.8f)
show()
}
} else {
checkAudioTracks()
}

View File

@@ -63,8 +63,12 @@ class TrackGroupDialogFragment(
override fun onBindViewHolder(holder: StreamViewHolder, position: Int) {
val binding = holder.binding
trackGroups[position].let { trackGroup ->
if (overrideTrackNames?.getOrNull(position - (trackGroups.size - (overrideTrackNames?.size?:0))) != null) {
val pair = overrideTrackNames!![position - (trackGroups.size - overrideTrackNames!!.size)]
if (overrideTrackNames?.getOrNull(
position - (trackGroups.size - (overrideTrackNames?.size ?: 0))
) != null
) {
val pair =
overrideTrackNames!![position - (trackGroups.size - overrideTrackNames!!.size)]
binding.subtitleTitle.text =
"[${pair.second}] ${pair.first}"
} 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.loadImage
import ani.dantotsu.openImage
import ani.dantotsu.others.ImageViewDialog
import ani.dantotsu.profile.ProfileActivity
import ani.dantotsu.setAnimation
import ani.dantotsu.snackString
import ani.dantotsu.util.ColorEditor.Companion.adjustColorForContrast
import ani.dantotsu.util.ColorEditor.Companion.getContrastRatio
import ani.dantotsu.util.customAlertDialog
import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.Section
import com.xwray.groupie.viewbinding.BindableItem
@@ -385,19 +385,14 @@ class CommentItem(
* @param callback the callback to call when the user clicks yes
*/
private fun dialogBuilder(title: String, message: String, callback: () -> Unit) {
val alertDialog =
android.app.AlertDialog.Builder(commentsFragment.activity, R.style.MyPopup)
.setTitle(title)
.setMessage(message)
.setPositiveButton("Yes") { dialog, _ ->
callback()
dialog.dismiss()
}
.setNegativeButton("No") { dialog, _ ->
dialog.dismiss()
}
val dialog = alertDialog.show()
dialog?.window?.setDimAmount(0.8f)
commentsFragment.activity.customAlertDialog().apply {
setTitle(title)
setMessage(message)
setPosButton("Yes") {
callback()
}
setNegButton("No") {}
}.show()
}
private val usernameColors: Array<String> = arrayOf(

View File

@@ -2,7 +2,6 @@ package ani.dantotsu.media.comments
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context.INPUT_METHOD_SERVICE
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
@@ -12,7 +11,6 @@ import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import androidx.appcompat.widget.PopupMenu
import androidx.core.animation.doOnEnd
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.CommentResponse
import ani.dantotsu.connections.comments.CommentsAPI
import ani.dantotsu.databinding.DialogEdittextBinding
import ani.dantotsu.databinding.FragmentCommentsBinding
import ani.dantotsu.loadImage
import ani.dantotsu.media.MediaDetailsActivity
@@ -32,8 +31,8 @@ import ani.dantotsu.setBaseline
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.toast
import ani.dantotsu.util.Logger
import ani.dantotsu.util.customAlertDialog
import com.xwray.groupie.GroupieAdapter
import com.xwray.groupie.Section
import io.noties.markwon.editor.MarkwonEditor
@@ -160,35 +159,48 @@ class CommentsFragment : Fragment() {
popup.inflate(R.menu.comments_sort_menu)
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 {
val alertDialog = AlertDialog.Builder(activity, R.style.MyPopup)
.setTitle("Enter a chapter/episode number tag")
.setView(R.layout.dialog_edittext)
.setPositiveButton("OK") { dialog, _ ->
val editText =
(dialog as AlertDialog).findViewById<EditText>(R.id.dialogEditText)
val text = editText?.text.toString()
activity.customAlertDialog().apply {
val customView = DialogEdittextBinding.inflate(layoutInflater)
setTitle("Enter a chapter/episode number tag")
setCustomView(customView.root)
setPosButton("OK") {
val text = customView.dialogEditText.text.toString()
filterTag = text.toIntOrNull()
lifecycleScope.launch {
loadAndDisplayComments()
}
dialog.dismiss()
}
.setNeutralButton("Clear") { dialog, _ ->
setNeutralButton("Clear") {
filterTag = null
lifecycleScope.launch {
loadAndDisplayComments()
}
dialog.dismiss()
}
.setNegativeButton("Cancel") { dialog, _ ->
filterTag = null
dialog.dismiss()
}
val dialog = alertDialog.show()
dialog?.window?.setDimAmount(0.8f)
setNegButton("Cancel") { filterTag = null }
show()
}
}
var isFetching = false
@@ -303,13 +315,12 @@ class CommentsFragment : Fragment() {
activity.binding.commentLabel.setOnClickListener {
//alert dialog to enter a number, with a cancel and ok button
val alertDialog = AlertDialog.Builder(activity, R.style.MyPopup)
.setTitle("Enter a chapter/episode number tag")
.setView(R.layout.dialog_edittext)
.setPositiveButton("OK") { dialog, _ ->
val editText =
(dialog as AlertDialog).findViewById<EditText>(R.id.dialogEditText)
val text = editText?.text.toString()
activity.customAlertDialog().apply {
val customView = DialogEdittextBinding.inflate(layoutInflater)
setTitle("Enter a chapter/episode number tag")
setCustomView(customView.root)
setPosButton("OK") {
val text = customView.dialogEditText.text.toString()
tag = text.toIntOrNull()
if (tag == null) {
activity.binding.commentLabel.background = ResourcesCompat.getDrawable(
@@ -324,28 +335,25 @@ class CommentsFragment : Fragment() {
null
)
}
dialog.dismiss()
}
.setNeutralButton("Clear") { dialog, _ ->
setNeutralButton("Clear") {
tag = null
activity.binding.commentLabel.background = ResourcesCompat.getDrawable(
resources,
R.drawable.ic_label_off_24,
null
)
dialog.dismiss()
}
.setNegativeButton("Cancel") { dialog, _ ->
setNegButton("Cancel") {
tag = null
activity.binding.commentLabel.background = ResourcesCompat.getDrawable(
resources,
R.drawable.ic_label_off_24,
null
)
dialog.dismiss()
}
val dialog = alertDialog.show()
dialog?.window?.setDimAmount(0.8f)
show()
}
}
}
@@ -363,11 +371,6 @@ class CommentsFragment : Fragment() {
}
}
@SuppressLint("NotifyDataSetChanged")
override fun onStart() {
super.onStart()
}
@SuppressLint("NotifyDataSetChanged")
override fun onResume() {
super.onResume()
@@ -579,31 +582,28 @@ class CommentsFragment : Fragment() {
* Called when the user tries to comment for the first time
*/
private fun showCommentRulesDialog() {
val alertDialog = AlertDialog.Builder(activity, R.style.MyPopup)
.setTitle("Commenting Rules")
.setMessage(
"I WILL BAN YOU WITHOUT HESITATION\n" +
"1. No racism\n" +
"2. No hate speech\n" +
"3. No spam\n" +
"4. No NSFW content\n" +
"6. ENGLISH ONLY\n" +
"7. No self promotion\n" +
"8. No impersonation\n" +
"9. No harassment\n" +
"10. No illegal content\n" +
"11. Anything you know you shouldn't comment\n"
)
.setPositiveButton("I Understand") { dialog, _ ->
dialog.dismiss()
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"
)
setPosButton("I Understand") {
PrefManager.setVal(PrefName.FirstComment, false)
processComment()
}
.setNegativeButton("Cancel") { dialog, _ ->
dialog.dismiss()
}
val dialog = alertDialog.show()
dialog?.window?.setDimAmount(0.8f)
setNegButton(R.string.cancel)
show()
}
}
private fun processComment() {
@@ -709,4 +709,4 @@ class CommentsFragment : Fragment() {
}
}
}
}
}

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,19 @@
package ani.dantotsu.media.manga
import android.app.AlertDialog
import android.content.Intent
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.CheckBox
import android.widget.EditText
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.NumberPicker
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getString
import androidx.core.content.ContextCompat.startActivity
import androidx.core.view.isGone
import androidx.core.view.isVisible
@@ -19,9 +22,10 @@ import androidx.recyclerview.widget.RecyclerView
import ani.dantotsu.R
import ani.dantotsu.currActivity
import ani.dantotsu.currContext
import ani.dantotsu.databinding.CustomDialogLayoutBinding
import ani.dantotsu.databinding.DialogLayoutBinding
import ani.dantotsu.databinding.ItemAnimeWatchBinding
import ani.dantotsu.databinding.ItemChipBinding
import ani.dantotsu.databinding.ItemMediaSourceBinding
import ani.dantotsu.isOnline
import ani.dantotsu.loadImage
import ani.dantotsu.media.Media
@@ -35,11 +39,14 @@ import ani.dantotsu.others.webview.CookieCatcher
import ani.dantotsu.parsers.DynamicMangaParser
import ani.dantotsu.parsers.MangaReadSources
import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.parsers.OfflineMangaParser
import ani.dantotsu.px
import ani.dantotsu.settings.FAQActivity
import ani.dantotsu.settings.saving.PrefManager
import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.toast
import ani.dantotsu.util.customAlertDialog
import com.google.android.material.chip.Chip
import eu.kanade.tachiyomi.data.notification.Notifications.CHANNEL_SUBSCRIPTION_CHECK
import eu.kanade.tachiyomi.source.online.HttpSource
@@ -55,86 +62,109 @@ class MangaReadAdapter(
) : RecyclerView.Adapter<MangaReadAdapter.ViewHolder>() {
var subscribe: MediaDetailsActivity.PopImageButton? = null
private var _binding: ItemAnimeWatchBinding? = null
private var _binding: ItemMediaSourceBinding? = null
val hiddenScanlators = mutableListOf<String>()
var scanlatorSelectionListener: ScanlatorSelectionListener? = null
var options = listOf<String>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val bind = ItemAnimeWatchBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(bind)
private fun clearCustomValsForMedia(mediaId: String, suffix: String) {
val customVals = PrefManager.getAllCustomValsForMedia("$mediaId$suffix")
customVals.forEach { (key) ->
PrefManager.removeCustomVal(key)
Log.d("PrefManager", "Removed key: $key")
}
}
private var nestedDialog: AlertDialog? = null
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val bind =
ItemMediaSourceBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ViewHolder(bind)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding = holder.binding
_binding = binding
binding.sourceTitle.setText(R.string.chaps)
//Fuck u launch
// Fuck u launch
binding.faqbutton.setOnClickListener {
val intent = Intent(fragment.requireContext(), FAQActivity::class.java)
startActivity(fragment.requireContext(), intent, null)
}
//Wrong Title
binding.animeSourceSearch.setOnClickListener {
// Wrong Title
binding.mediaSourceSearch.setOnClickListener {
SourceSearchDialogFragment().show(
fragment.requireActivity().supportFragmentManager,
null
)
}
val offline = !isOnline(binding.root.context) || PrefManager.getVal(PrefName.OfflineMode)
//for removing saved progress
binding.sourceTitle.setOnLongClickListener {
fragment.requireContext().customAlertDialog().apply {
setTitle(" Delete Progress for all chapters of ${media.nameRomaji}")
setMessage("This will delete all the locally stored progress for chapters")
setPosButton(R.string.ok) {
clearCustomValsForMedia("${media.id}", "_Chapter")
clearCustomValsForMedia("${media.id}", "_Vol")
snackString("Deleted the progress of Chapters for ${media.nameRomaji}")
}
setNegButton(R.string.no)
show()
}
true
}
binding.animeSourceNameContainer.isGone = offline
binding.animeSourceSettings.isGone = offline
binding.animeSourceSearch.isGone = offline
binding.animeSourceTitle.isGone = offline
//Source Selection
binding.mediaSourceNameContainer.isGone = offline
binding.mediaSourceSettings.isGone = offline
binding.mediaSourceSearch.isGone = offline
binding.mediaSourceTitle.isGone = offline
// Source Selection
var source =
media.selected!!.sourceIndex.let { if (it >= mangaReadSources.names.size) 0 else it }
setLanguageList(media.selected!!.langIndex, source)
if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) {
binding.animeSource.setText(mangaReadSources.names[source])
binding.mediaSource.setText(mangaReadSources.names[source])
mangaReadSources[source].apply {
binding.animeSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
binding.mediaSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.mediaSourceTitle.text = it } }
}
}
media.selected?.scanlators?.let {
hiddenScanlators.addAll(it)
}
binding.animeSource.setAdapter(
binding.mediaSource.setAdapter(
ArrayAdapter(
fragment.requireContext(),
R.layout.item_dropdown,
mangaReadSources.names
)
)
binding.animeSourceTitle.isSelected = true
binding.animeSource.setOnItemClickListener { _, _, i, _ ->
binding.mediaSourceTitle.isSelected = true
binding.mediaSource.setOnItemClickListener { _, _, i, _ ->
fragment.onSourceChange(i).apply {
binding.animeSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.animeSourceTitle.text = it } }
binding.mediaSourceTitle.text = showUserText
showUserTextListener = { MainScope().launch { binding.mediaSourceTitle.text = it } }
source = i
setLanguageList(0, i)
}
subscribeButton(false)
//invalidate if it's the last source
// Invalidate if it's the last source
val invalidate = i == mangaReadSources.names.size - 1
fragment.loadChapters(i, invalidate)
}
binding.animeSourceLanguage.setOnItemClickListener { _, _, i, _ ->
binding.mediaSourceLanguage.setOnItemClickListener { _, _, i, _ ->
// Check if 'extension' and 'selected' properties exist and are accessible
(mangaReadSources[source] as? DynamicMangaParser)?.let { ext ->
ext.sourceLanguage = i
fragment.onLangChange(i, ext.saveName)
fragment.onSourceChange(media.selected!!.sourceIndex).apply {
binding.animeSourceTitle.text = showUserText
binding.mediaSourceTitle.text = showUserText
showUserTextListener =
{ MainScope().launch { binding.animeSourceTitle.text = it } }
{ MainScope().launch { binding.mediaSourceTitle.text = it } }
setLanguageList(i, source)
}
subscribeButton(false)
@@ -143,17 +173,17 @@ class MangaReadAdapter(
}
}
//settings
binding.animeSourceSettings.setOnClickListener {
// Settings
binding.mediaSourceSettings.setOnClickListener {
(mangaReadSources[source] as? DynamicMangaParser)?.let { ext ->
fragment.openSettings(ext.extension)
}
}
//Grids
// Grids
subscribe = MediaDetailsActivity.PopImageButton(
fragment.lifecycleScope,
binding.animeSourceSubscribe,
binding.mediaSourceSubscribe,
R.drawable.ic_round_notifications_active_24,
R.drawable.ic_round_notifications_none_24,
R.color.bg_opp,
@@ -161,206 +191,216 @@ class MangaReadAdapter(
fragment.subscribed,
true
) {
fragment.onNotificationPressed(it, binding.animeSource.text.toString())
fragment.onNotificationPressed(it, binding.mediaSource.text.toString())
}
subscribeButton(false)
binding.animeSourceSubscribe.setOnLongClickListener {
binding.mediaSourceSubscribe.setOnLongClickListener {
openSettings(fragment.requireContext(), CHANNEL_SUBSCRIPTION_CHECK)
}
binding.animeNestedButton.setOnClickListener {
val dialogView =
LayoutInflater.from(fragment.requireContext()).inflate(R.layout.dialog_layout, null)
val dialogBinding = DialogLayoutBinding.bind(dialogView)
binding.mediaNestedButton.setOnClickListener {
val dialogBinding = DialogLayoutBinding.inflate(fragment.layoutInflater)
var refresh = false
var run = false
var reversed = media.selected!!.recyclerReversed
var style =
media.selected!!.recyclerStyle ?: PrefManager.getVal(PrefName.MangaDefaultView)
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
dialogBinding.animeSourceTop.setOnClickListener {
reversed = !reversed
dialogBinding.animeSourceTop.rotation = if (reversed) -90f else 90f
dialogBinding.sortText.text = if (reversed) "Down to Up" else "Up to Down"
run = true
}
dialogBinding.apply {
mediaSourceTop.rotation = if (reversed) -90f else 90f
sortText.text = if (reversed) "Down to Up" else "Up to Down"
mediaSourceTop.setOnClickListener {
reversed = !reversed
mediaSourceTop.rotation = if (reversed) -90f else 90f
sortText.text = if (reversed) "Down to Up" else "Up to Down"
run = true
}
//Grids
dialogBinding.animeSourceGrid.visibility = View.GONE
var selected = when (style) {
0 -> dialogBinding.animeSourceList
1 -> dialogBinding.animeSourceCompact
else -> dialogBinding.animeSourceList
}
when (style) {
0 -> dialogBinding.layoutText.setText(R.string.list)
1 -> dialogBinding.layoutText.setText(R.string.compact)
else -> dialogBinding.animeSourceList
}
selected.alpha = 1f
fun selected(it: ImageButton) {
selected.alpha = 0.33f
selected = it
// Grids
mediaSourceGrid.visibility = View.GONE
var selected = when (style) {
0 -> mediaSourceList
1 -> mediaSourceCompact
else -> mediaSourceList
}
when (style) {
0 -> layoutText.setText(R.string.list)
1 -> layoutText.setText(R.string.compact)
else -> mediaSourceList
}
selected.alpha = 1f
}
dialogBinding.animeSourceList.setOnClickListener {
selected(it as ImageButton)
style = 0
dialogBinding.layoutText.setText(R.string.list)
run = true
}
dialogBinding.animeSourceCompact.setOnClickListener {
selected(it as ImageButton)
style = 1
dialogBinding.layoutText.setText(R.string.compact)
run = true
}
dialogBinding.animeWebviewContainer.setOnClickListener {
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
toast(R.string.webview_not_installed)
fun selected(it: ImageButton) {
selected.alpha = 0.33f
selected = it
selected.alpha = 1f
}
//start CookieCatcher activity
if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) {
val sourceAHH = mangaReadSources[source] as? DynamicMangaParser
val sourceHttp = sourceAHH?.extension?.sources?.firstOrNull() as? HttpSource
val url = sourceHttp?.baseUrl
url?.let {
refresh = true
val intent = Intent(fragment.requireContext(), CookieCatcher::class.java)
.putExtra("url", url)
startActivity(fragment.requireContext(), intent, null)
mediaSourceList.setOnClickListener {
selected(it as ImageButton)
style = 0
layoutText.setText(R.string.list)
run = true
}
mediaSourceCompact.setOnClickListener {
selected(it as ImageButton)
style = 1
layoutText.setText(R.string.compact)
run = true
}
mediaWebviewContainer.setOnClickListener {
if (!WebViewUtil.supportsWebView(fragment.requireContext())) {
toast(R.string.webview_not_installed)
}
}
}
//Multi download
dialogBinding.downloadNo.text = "0"
dialogBinding.animeDownloadTop.setOnClickListener {
//Alert dialog asking for the number of chapters to download
val alertDialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
alertDialog.setTitle("Multi Chapter Downloader")
alertDialog.setMessage("Enter the number of chapters to download")
val input = NumberPicker(currContext())
input.minValue = 1
input.maxValue = 20
input.value = 1
alertDialog.setView(input)
alertDialog.setPositiveButton("OK") { _, _ ->
dialogBinding.downloadNo.text = "${input.value}"
}
alertDialog.setNegativeButton("Cancel") { dialog, _ -> dialog.cancel() }
val dialog = alertDialog.show()
dialog.window?.setDimAmount(0.8f)
}
//Scanlator
dialogBinding.animeScanlatorContainer.isVisible = options.count() > 1
dialogBinding.scanlatorNo.text = "${options.count()}"
dialogBinding.animeScanlatorTop.setOnClickListener {
val dialogView2 =
LayoutInflater.from(currContext()).inflate(R.layout.custom_dialog_layout, null)
val checkboxContainer =
dialogView2.findViewById<LinearLayout>(R.id.checkboxContainer)
val tickAllButton = dialogView2.findViewById<ImageButton>(R.id.toggleButton)
// Function to get the right image resource for the toggle button
fun getToggleImageResource(container: ViewGroup): Int {
var allChecked = true
var allUnchecked = true
for (i in 0 until container.childCount) {
val checkBox = container.getChildAt(i) as CheckBox
if (!checkBox.isChecked) {
allChecked = false
} else {
allUnchecked = false
// Start CookieCatcher activity
if (mangaReadSources.names.isNotEmpty() && source in 0 until mangaReadSources.names.size) {
val sourceAHH = mangaReadSources[source] as? DynamicMangaParser
val sourceHttp = sourceAHH?.extension?.sources?.firstOrNull() as? HttpSource
val url = sourceHttp?.baseUrl
url?.let {
refresh = true
val intent =
Intent(fragment.requireContext(), CookieCatcher::class.java)
.putExtra("url", url)
startActivity(fragment.requireContext(), intent, null)
}
}
return when {
allChecked -> R.drawable.untick_all_boxes
allUnchecked -> R.drawable.tick_all_boxes
else -> R.drawable.invert_all_boxes
}
}
// Dynamically add checkboxes
options.forEach { option ->
val checkBox = CheckBox(currContext()).apply {
text = option
setOnCheckedChangeListener { _, _ ->
// Update image resource when you change a checkbox
tickAllButton.setImageResource(getToggleImageResource(checkboxContainer))
}
}
// Set checked if its already selected
if (media.selected!!.scanlators != null) {
checkBox.isChecked = media.selected!!.scanlators?.contains(option) != true
scanlatorSelectionListener?.onScanlatorsSelected()
} else {
checkBox.isChecked = true
}
checkboxContainer.addView(checkBox)
}
// Create AlertDialog
val dialog = AlertDialog.Builder(currContext(), R.style.MyPopup)
.setView(dialogView2)
.setPositiveButton("OK") { _, _ ->
hiddenScanlators.clear()
for (i in 0 until checkboxContainer.childCount) {
val checkBox = checkboxContainer.getChildAt(i) as CheckBox
if (!checkBox.isChecked) {
hiddenScanlators.add(checkBox.text.toString())
// Multi download
//downloadNo.text = "0"
mediaDownloadTop.setOnClickListener {
fragment.requireContext().customAlertDialog().apply {
setTitle("Multi Chapter Downloader")
setMessage("Enter the number of chapters to download")
val input = View.inflate(currContext(), R.layout.dialog_layout, null)
val editText = input.findViewById<EditText>(R.id.downloadNo)
setCustomView(input)
setPosButton(R.string.ok) {
val value = editText.text.toString().toIntOrNull()
if (value != null && value > 0) {
downloadNo.setText(value.toString(), TextView.BufferType.EDITABLE)
fragment.multiDownload(value)
} else {
toast("Please enter a valid number")
}
}
fragment.onScanlatorChange(hiddenScanlators)
scanlatorSelectionListener?.onScanlatorsSelected()
setNegButton(R.string.cancel)
show()
}
.setNegativeButton("Cancel", null)
.show()
dialog.window?.setDimAmount(0.8f)
}
resetProgress.setOnClickListener {
fragment.requireContext().customAlertDialog().apply {
setTitle(" Delete Progress for all chapters of ${media.nameRomaji}")
setMessage("This will delete all the locally stored progress for chapters")
setPosButton(R.string.ok) {
// Usage
clearCustomValsForMedia("${media.id}", "_Chapter")
clearCustomValsForMedia("${media.id}", "_Vol")
// Standard image resource
tickAllButton.setImageResource(getToggleImageResource(checkboxContainer))
snackString("Deleted the progress of Chapters for ${media.nameRomaji}")
}
setNegButton(R.string.no)
show()
}
}
resetProgressDef.text = getString(currContext()!!, R.string.clear_stored_chapter)
// Listens to ticked checkboxes and changes image resource accordingly
tickAllButton.setOnClickListener {
// Toggle checkboxes
for (i in 0 until checkboxContainer.childCount) {
val checkBox = checkboxContainer.getChildAt(i) as CheckBox
checkBox.isChecked = !checkBox.isChecked
// Scanlator
mangaScanlatorContainer.isVisible = options.count() > 1
scanlatorNo.text = "${options.count()}"
mangaScanlatorTop.setOnClickListener {
CustomDialogLayoutBinding.inflate(fragment.layoutInflater)
val dialogView = CustomDialogLayoutBinding.inflate(fragment.layoutInflater)
val checkboxContainer = dialogView.checkboxContainer
val tickAllButton = dialogView.toggleButton
fun getToggleImageResource(container: ViewGroup): Int {
var allChecked = true
var allUnchecked = true
for (i in 0 until container.childCount) {
val checkBox = container.getChildAt(i) as CheckBox
if (!checkBox.isChecked) {
allChecked = false
} else {
allUnchecked = false
}
}
return when {
allChecked -> R.drawable.untick_all_boxes
allUnchecked -> R.drawable.tick_all_boxes
else -> R.drawable.invert_all_boxes
}
}
// Update image resource
options.forEach { option ->
val checkBox = CheckBox(currContext()).apply {
text = option
setOnCheckedChangeListener { _, _ ->
tickAllButton.setImageResource(
getToggleImageResource(
checkboxContainer
)
)
}
}
if (media.selected!!.scanlators != null) {
checkBox.isChecked =
media.selected!!.scanlators?.contains(option) != true
scanlatorSelectionListener?.onScanlatorsSelected()
} else {
checkBox.isChecked = true
}
checkboxContainer.addView(checkBox)
}
fragment.requireContext().customAlertDialog().apply {
setCustomView(dialogView.root)
setPosButton("OK") {
hiddenScanlators.clear()
for (i in 0 until checkboxContainer.childCount) {
val checkBox = checkboxContainer.getChildAt(i) as CheckBox
if (!checkBox.isChecked) {
hiddenScanlators.add(checkBox.text.toString())
}
}
fragment.onScanlatorChange(hiddenScanlators)
scanlatorSelectionListener?.onScanlatorsSelected()
}
setNegButton("Cancel")
}.show()
tickAllButton.setImageResource(getToggleImageResource(checkboxContainer))
tickAllButton.setOnClickListener {
for (i in 0 until checkboxContainer.childCount) {
val checkBox = checkboxContainer.getChildAt(i) as CheckBox
checkBox.isChecked = !checkBox.isChecked
}
tickAllButton.setImageResource(getToggleImageResource(checkboxContainer))
}
}
fragment.requireContext().customAlertDialog().apply {
setTitle("Options")
setCustomView(root)
setPosButton("OK") {
if (run) fragment.onIconPressed(style, reversed)
val value = downloadNo.text.toString().toIntOrNull()
if (value != null && value > 0) {
fragment.multiDownload(value)
}
if (refresh) fragment.loadChapters(source, true)
}
setNegButton("Cancel") {
if (refresh) fragment.loadChapters(source, true)
}
show()
}
}
nestedDialog = AlertDialog.Builder(fragment.requireContext(), R.style.MyPopup)
.setTitle("Options")
.setView(dialogView)
.setPositiveButton("OK") { _, _ ->
if (run) fragment.onIconPressed(style, reversed)
if (dialogBinding.downloadNo.text != "0") {
fragment.multiDownload(dialogBinding.downloadNo.text.toString().toInt())
}
if (refresh) fragment.loadChapters(source, true)
}
.setNegativeButton("Cancel") { _, _ ->
if (refresh) fragment.loadChapters(source, true)
}
.setOnCancelListener {
if (refresh) fragment.loadChapters(source, true)
}
.create()
nestedDialog?.show()
}
//Chapter Handling
// Chapter Handling
handleChapters()
}
@@ -368,7 +408,7 @@ class MangaReadAdapter(
subscribe?.enabled(enabled)
}
//Chips
// Chips
fun updateChips(limit: Int, names: Array<String>, arr: Array<Int>, selected: Int = 0) {
val binding = _binding
if (binding != null) {
@@ -379,13 +419,13 @@ class MangaReadAdapter(
val chip =
ItemChipBinding.inflate(
LayoutInflater.from(fragment.context),
binding.animeSourceChipGroup,
binding.mediaSourceChipGroup,
false
).root
chip.isCheckable = true
fun selected() {
chip.isChecked = true
binding.animeWatchChipScroll.smoothScrollTo(
binding.mediaWatchChipScroll.smoothScrollTo(
(chip.left - screenWidth / 2) + (chip.width / 2),
0
)
@@ -394,16 +434,16 @@ class MangaReadAdapter(
val startChapter = MediaNameAdapter.findChapterNumber(names[limit * (position)])
val endChapter = MediaNameAdapter.findChapterNumber(names[last - 1])
val startChapterString = if (startChapter != null) {
"Ch.$startChapter"
"Ch.%.1f".format(startChapter)
} else {
names[limit * (position)]
}
val endChapterString = if (endChapter != null) {
"Ch.$endChapter"
"Ch.%.1f".format(endChapter)
} else {
names[last - 1]
}
//chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
// chip.text = "${names[limit * (position)]} - ${names[last - 1]}"
val chipText = "$startChapterString - $endChapterString"
chip.text = chipText
chip.setTextColor(
@@ -417,14 +457,14 @@ class MangaReadAdapter(
selected()
fragment.onChipClicked(position, limit * (position), last - 1)
}
binding.animeSourceChipGroup.addView(chip)
binding.mediaSourceChipGroup.addView(chip)
if (selected == position) {
selected()
select = chip
}
}
if (select != null)
binding.animeWatchChipScroll.apply {
binding.mediaWatchChipScroll.apply {
post {
scrollTo(
(select.left - screenWidth / 2) + (select.width / 2),
@@ -436,7 +476,7 @@ class MangaReadAdapter(
}
fun clearChips() {
_binding?.animeSourceChipGroup?.removeAllViews()
_binding?.mediaSourceChipGroup?.removeAllViews()
}
fun handleChapters() {
@@ -444,7 +484,6 @@ class MangaReadAdapter(
val binding = _binding
if (binding != null) {
if (media.manga?.chapters != null) {
val chapters = media.manga.chapters!!.keys.toTypedArray()
val anilistEp = (media.userProgress ?: 0).plus(1)
val appEp = PrefManager.getNullableCustomVal(
"${media.id}_current_chp",
@@ -452,80 +491,104 @@ class MangaReadAdapter(
String::class.java
)
?.toIntOrNull() ?: 1
var continueEp = (if (anilistEp > appEp) anilistEp else appEp).toString()
val filteredChapters = chapters.filter { chapterKey ->
val chapter = media.manga.chapters!![chapterKey]!!
chapter.scanlator !in hiddenScanlators
val continueNumber = (if (anilistEp > appEp) anilistEp else appEp).toString()
val filteredChapters = media.manga.chapters!!.filter { chapter ->
if (mangaReadSources[media.selected!!.sourceIndex] is OfflineMangaParser) {
true
} else {
chapter.value.scanlator !in hiddenScanlators
}
}
val formattedChapters = filteredChapters.map {
MediaNameAdapter.findChapterNumber(it)?.toInt()?.toString()
MediaNameAdapter.findChapterNumber(it.value.number)?.toInt()
?.toString() to it.key
}
if (formattedChapters.contains(continueEp)) {
continueEp = chapters[formattedChapters.indexOf(continueEp)]
binding.animeSourceContinue.visibility = View.VISIBLE
if (formattedChapters.any { it.first == continueNumber }) {
var continueEp =
media.manga.chapters!![formattedChapters.first { it.first == continueNumber }.second]
binding.sourceContinue.visibility = View.VISIBLE
handleProgress(
binding.itemEpisodeProgressCont,
binding.itemEpisodeProgress,
binding.itemEpisodeProgressEmpty,
binding.itemMediaProgressCont,
binding.itemMediaProgress,
binding.itemMediaProgressEmpty,
media.id,
continueEp
continueEp!!.number
)
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight > 0.8f) {
val e = chapters.indexOf(continueEp)
if (e != -1 && e + 1 < chapters.size) {
continueEp = chapters[e + 1]
if ((binding.itemMediaProgress.layoutParams as LinearLayout.LayoutParams).weight > 0.8f) {
val numberPlusOne =
formattedChapters.indexOfFirst { it.first?.toIntOrNull() == continueNumber.toInt() + 1 }
if (numberPlusOne != -1) {
continueEp =
media.manga.chapters!![formattedChapters[numberPlusOne].second]
}
}
val ep = media.manga.chapters!![continueEp]!!
binding.itemEpisodeImage.loadImage(media.banner ?: media.cover)
binding.animeSourceContinueText.text =
binding.itemMediaImage.loadImage(media.banner ?: media.cover)
binding.mediaSourceContinueText.text =
currActivity()!!.getString(
R.string.continue_chapter,
ep.number,
if (!ep.title.isNullOrEmpty()) ep.title else ""
continueEp!!.number,
if (!continueEp.title.isNullOrEmpty()) continueEp.title else ""
)
binding.animeSourceContinue.setOnClickListener {
binding.sourceContinue.setOnClickListener {
fragment.onMangaChapterClick(continueEp)
}
if (fragment.continueEp) {
if ((binding.itemEpisodeProgress.layoutParams as LinearLayout.LayoutParams).weight < 0.8f) {
binding.animeSourceContinue.performClick()
if ((binding.itemMediaProgress.layoutParams as LinearLayout.LayoutParams).weight < 0.8f) {
binding.sourceContinue.performClick()
fragment.continueEp = false
}
}
} else {
binding.animeSourceContinue.visibility = View.GONE
binding.sourceContinue.visibility = View.GONE
}
binding.animeSourceProgressBar.visibility = View.GONE
val sourceFound = media.manga.chapters!!.isNotEmpty()
binding.animeSourceNotFound.isGone = sourceFound
binding.sourceProgressBar.visibility = View.GONE
val sourceFound = filteredChapters.isNotEmpty()
val isDownloadedSource =
mangaReadSources[media.selected!!.sourceIndex] is OfflineMangaParser
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
if (!sourceFound && PrefManager.getVal(PrefName.SearchSources)) {
if (binding.animeSource.adapter.count > media.selected!!.sourceIndex + 1) {
if (binding.mediaSource.adapter.count > media.selected!!.sourceIndex + 1) {
val nextIndex = media.selected!!.sourceIndex + 1
binding.animeSource.setText(
binding.animeSource.adapter
binding.mediaSource.setText(
binding.mediaSource.adapter
.getItem(nextIndex).toString(), false
)
fragment.onSourceChange(nextIndex).apply {
binding.animeSourceTitle.text = showUserText
binding.mediaSourceTitle.text = showUserText
showUserTextListener =
{ MainScope().launch { binding.animeSourceTitle.text = it } }
{ MainScope().launch { binding.mediaSourceTitle.text = it } }
setLanguageList(0, nextIndex)
}
subscribeButton(false)
// invalidate if it's the last source
// Invalidate if it's the last source
val invalidate = nextIndex == mangaReadSources.names.size - 1
fragment.loadChapters(nextIndex, invalidate)
}
}
} else {
binding.animeSourceContinue.visibility = View.GONE
binding.animeSourceNotFound.visibility = View.GONE
binding.sourceContinue.visibility = View.GONE
binding.sourceNotFound.visibility = View.GONE
binding.faqbutton.visibility = View.GONE
clearChips()
binding.animeSourceProgressBar.visibility = View.VISIBLE
binding.sourceProgressBar.visibility = View.VISIBLE
}
}
}
@@ -539,9 +602,9 @@ class MangaReadAdapter(
ext.sourceLanguage = lang
}
try {
binding?.animeSourceLanguage?.setText(parser.extension.sources[lang].lang)
binding?.mediaSourceLanguage?.setText(parser.extension.sources[lang].lang)
} catch (e: IndexOutOfBoundsException) {
binding?.animeSourceLanguage?.setText(
binding?.mediaSourceLanguage?.setText(
parser.extension.sources.firstOrNull()?.lang ?: "Unknown"
)
}
@@ -551,9 +614,9 @@ class MangaReadAdapter(
parser.extension.sources.map { LanguageMapper.getLanguageName(it.lang) }
)
val items = adapter.count
binding?.animeSourceLanguageContainer?.isVisible = items > 1
binding?.mediaSourceLanguageContainer?.isVisible = items > 1
binding?.animeSourceLanguage?.setAdapter(adapter)
binding?.mediaSourceLanguage?.setAdapter(adapter)
}
}
@@ -561,7 +624,7 @@ class MangaReadAdapter(
override fun getItemCount(): Int = 1
inner class ViewHolder(val binding: ItemAnimeWatchBinding) :
inner class ViewHolder(val binding: ItemMediaSourceBinding) :
RecyclerView.ViewHolder(binding.root)
}

View File

@@ -2,7 +2,6 @@ package ani.dantotsu.media.manga
import android.Manifest
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
@@ -31,7 +30,7 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import ani.dantotsu.R
import ani.dantotsu.databinding.FragmentAnimeWatchBinding
import ani.dantotsu.databinding.FragmentMediaSourceBinding
import ani.dantotsu.download.DownloadedType
import ani.dantotsu.download.DownloadsManager
import ani.dantotsu.download.DownloadsManager.Companion.compareName
@@ -53,6 +52,7 @@ import ani.dantotsu.parsers.DynamicMangaParser
import ani.dantotsu.parsers.HMangaSources
import ani.dantotsu.parsers.MangaParser
import ani.dantotsu.parsers.MangaSources
import ani.dantotsu.parsers.OfflineMangaParser
import ani.dantotsu.setNavigationTheme
import ani.dantotsu.settings.extensionprefs.MangaSourcePreferencesFragment
import ani.dantotsu.settings.saving.PrefManager
@@ -60,6 +60,7 @@ import ani.dantotsu.settings.saving.PrefName
import ani.dantotsu.snackString
import ani.dantotsu.util.StoragePermissions.Companion.accessAlertDialog
import ani.dantotsu.util.StoragePermissions.Companion.hasDirAccess
import ani.dantotsu.util.customAlertDialog
import com.google.android.material.appbar.AppBarLayout
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.source.ConfigurableSource
@@ -74,7 +75,7 @@ import kotlin.math.max
import kotlin.math.roundToInt
open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
private var _binding: FragmentAnimeWatchBinding? = null
private var _binding: FragmentMediaSourceBinding? = null
private val binding get() = _binding!!
private val model: MediaDetailsViewModel by activityViewModels()
@@ -101,7 +102,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentAnimeWatchBinding.inflate(inflater, container, false)
_binding = FragmentMediaSourceBinding.inflate(inflater, container, false)
return _binding?.root
}
@@ -121,7 +122,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
ContextCompat.RECEIVER_EXPORTED
)
binding.animeSourceRecycler.updatePadding(bottom = binding.animeSourceRecycler.paddingBottom + navBarHeight)
binding.mediaSourceRecycler.updatePadding(bottom = binding.mediaSourceRecycler.paddingBottom + navBarHeight)
screenWidth = resources.displayMetrics.widthPixels.dp
var maxGridSize = (screenWidth / 100f).roundToInt()
@@ -144,13 +145,13 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
}
}
binding.animeSourceRecycler.layoutManager = gridLayoutManager
binding.mediaSourceRecycler.layoutManager = gridLayoutManager
binding.ScrollTop.setOnClickListener {
binding.animeSourceRecycler.scrollToPosition(10)
binding.animeSourceRecycler.smoothScrollToPosition(0)
binding.mediaSourceRecycler.scrollToPosition(10)
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) {
super.onScrolled(recyclerView, dx, dy)
@@ -164,7 +165,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
}
})
model.scrolledToTop.observe(viewLifecycleOwner) {
if (it) binding.animeSourceRecycler.scrollToPosition(0)
if (it) binding.mediaSourceRecycler.scrollToPosition(0)
}
continueEp = model.continueMedia ?: false
@@ -195,11 +196,11 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
for (download in downloadManager.mangaDownloadedTypes) {
if (media.compareName(download.titleName)) {
chapterAdapter.stopDownload(download.chapterName)
chapterAdapter.stopDownload(download.uniqueName)
}
}
binding.animeSourceRecycler.adapter =
binding.mediaSourceRecycler.adapter =
ConcatAdapter(headerAdapter, chapterAdapter)
lifecycleScope.launch(Dispatchers.IO) {
@@ -214,8 +215,8 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
reload()
}
} else {
binding.animeNotSupported.visibility = View.VISIBLE
binding.animeNotSupported.text =
binding.mediaNotSupported.visibility = View.VISIBLE
binding.mediaNotSupported.text =
getString(R.string.not_supported, media.format ?: "")
}
}
@@ -231,10 +232,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
}
fun multiDownload(n: Int) {
//get last viewed chapter
// Get last viewed chapter
val selected = media.userProgress
val chapters = media.manga?.chapters?.values?.toList()
//filter by selected language
// Filter by selected language
val progressChapterIndex = (chapters?.indexOfFirst {
MediaNameAdapter.findChapterNumber(it.number)?.toInt() == selected
} ?: 0) + 1
@@ -244,12 +245,12 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
// Calculate the end index
val endIndex = minOf(progressChapterIndex + n, chapters.size)
//make sure there are enough chapters
// Make sure there are enough chapters
val chaptersToDownload = chapters.subList(progressChapterIndex, endIndex)
for (chapter in chaptersToDownload) {
onMangaChapterDownloadClick(chapter.title!!)
onMangaChapterDownloadClick(chapter)
}
}
@@ -260,9 +261,14 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
val chapters = loadedChapters[media.selected!!.sourceIndex]
if (chapters != null) {
headerAdapter.options = getScanlators(chapters)
val filteredChapters = chapters.filterNot { (_, chapter) ->
chapter.scanlator in headerAdapter.hiddenScanlators
}
val filteredChapters =
if (model.mangaReadSources?.get(media.selected!!.sourceIndex) is OfflineMangaParser) {
chapters
} else {
chapters.filterNot { (_, chapter) ->
chapter.scanlator in headerAdapter.hiddenScanlators
}
}
media.manga?.chapters = filteredChapters.toMutableMap()
@@ -386,14 +392,12 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
if (allSettings.size > 1) {
val names =
allSettings.map { LanguageMapper.getLanguageName(it.lang) }.toTypedArray()
val dialog = AlertDialog.Builder(requireContext(), R.style.MyPopup)
.setTitle("Select a Source")
.setSingleChoiceItems(names, -1) { dialog, which ->
requireContext().customAlertDialog().apply {
setTitle("Select a Source")
singleChoiceItems(names) { which ->
selectedSetting = allSettings[which]
itemSelected = true
dialog.dismiss()
// Move the fragment transaction here
val fragment =
MangaSourcePreferencesFragment().getInstance(selectedSetting.id) {
changeUIVisibility(true)
@@ -405,13 +409,14 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
.addToBackStack(null)
.commit()
}
.setOnDismissListener {
onDismiss {
if (!itemSelected) {
changeUIVisibility(true)
}
}
.show()
dialog.window?.setDimAmount(0.8f)
show()
}
} else {
// If there's only one setting, proceed with the fragment transaction
val fragment = MangaSourcePreferencesFragment().getInstance(selectedSetting.id) {
@@ -432,9 +437,9 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
}
}
fun onMangaChapterClick(i: String) {
fun onMangaChapterClick(i: MangaChapter) {
model.continueMedia = false
media.manga?.chapters?.get(i)?.let {
media.manga?.chapters?.get(i.uniqueNumber())?.let {
media.manga?.selectedChapter = i
model.saveSelected(media.id, media.selected!!)
ChapterLoaderDialog.newInstance(it, true)
@@ -442,7 +447,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
}
}
fun onMangaChapterDownloadClick(i: String) {
fun onMangaChapterDownloadClick(i: MangaChapter) {
activity?.let {
if (!isNotificationPermissionGranted()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
@@ -455,7 +460,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
}
fun continueDownload() {
model.continueMedia = false
media.manga?.chapters?.get(i)?.let { chapter ->
media.manga?.chapters?.get(i.uniqueNumber())?.let { chapter ->
val parser =
model.mangaReadSources?.get(media.selected!!.sourceIndex) as? DynamicMangaParser
parser?.let {
@@ -466,9 +471,10 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
val downloadTask = MangaDownloaderService.DownloadTask(
title = media.mainName(),
chapter = chapter.title!!,
scanlator = chapter.scanlator ?: "Unknown",
imageData = images,
sourceMedia = media,
retries = 2,
retries = 25,
simultaneousDownloads = 2
)
@@ -485,7 +491,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
// Inform the adapter that the download has started
withContext(Dispatchers.Main) {
chapterAdapter.startDownload(i)
chapterAdapter.startDownload(i.uniqueNumber())
}
}
}
@@ -516,11 +522,11 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
}
fun onMangaChapterRemoveDownloadClick(i: String) {
fun onMangaChapterRemoveDownloadClick(i: MangaChapter) {
downloadManager.removeDownload(
DownloadedType(
media.mainName(),
i,
i.number,
MediaType.MANGA
)
) {
@@ -528,7 +534,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
}
}
fun onMangaChapterStopDownloadClick(i: String) {
fun onMangaChapterStopDownloadClick(i: MangaChapter) {
val cancelIntent = Intent().apply {
action = MangaDownloaderService.ACTION_CANCEL_DOWNLOAD
putExtra(MangaDownloaderService.EXTRA_CHAPTER, i)
@@ -539,11 +545,11 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
downloadManager.removeDownload(
DownloadedType(
media.mainName(),
i,
i.number,
MediaType.MANGA
)
) {
chapterAdapter.purgeDownload(i)
chapterAdapter.purgeDownload(i.uniqueNumber())
}
}
@@ -584,9 +590,11 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
private fun reload() {
val selected = model.loadSelected(media)
//Find latest chapter for subscription
// Find latest chapter for subscription
selected.latest =
media.manga?.chapters?.values?.maxOfOrNull { it.number.toFloatOrNull() ?: 0f } ?: 0f
media.manga?.chapters?.values?.maxOfOrNull {
MediaNameAdapter.findChapterNumber(it.number) ?: 0f
} ?: 0f
selected.latest =
media.userProgress?.toFloat()?.takeIf { selected.latest < it } ?: selected.latest
@@ -618,14 +626,14 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener {
override fun onResume() {
super.onResume()
binding.mediaInfoProgressBar.visibility = progress
binding.animeSourceRecycler.layoutManager?.onRestoreInstanceState(state)
binding.mediaSourceRecycler.layoutManager?.onRestoreInstanceState(state)
requireActivity().setNavigationTheme()
}
override fun onPause() {
super.onPause()
state = binding.animeSourceRecycler.layoutManager?.onSaveInstanceState()
state = binding.mediaSourceRecycler.layoutManager?.onSaveInstanceState()
}
companion object {

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